Why Is Swift Giving Me Inaccurate Floating Point Arithmetic Results

Why is Swift giving me inaccurate floating point arithmetic results?

The issue is simply the different number of digits being printed out.

#include <iostream>
#include <iomanip>

int main() {
double d = 90.0 / 100.0;
float f = 90.0f / 100.0f;
std::cout << d << ' ' << f << '\n';
std::cout << std::setprecision(20) << d << ' ' << f << '\n';
}

0.9 0.9
0.9000000000000000222 0.89999997615814208984

(I wrote this example in C++, but you will get the same results in every language that uses the hardware's floating point arithmetic and allows this formatting.)

If you want to understand why finite precision floating point math does not give you exact results then:

What Every Computer Scientist Should Know About Floating-Point Arithmetic

And:

Float

Is floating point math broken?

Binary floating point math is like this. In most programming languages, it is based on the IEEE 754 standard. The crux of the problem is that numbers are represented in this format as a whole number times a power of two; rational numbers (such as 0.1, which is 1/10) whose denominator is not a power of two cannot be exactly represented.

For 0.1 in the standard binary64 format, the representation can be written exactly as

  • 0.1000000000000000055511151231257827021181583404541015625 in decimal, or
  • 0x1.999999999999ap-4 in C99 hexfloat notation.

In contrast, the rational number 0.1, which is 1/10, can be written exactly as

  • 0.1 in decimal, or
  • 0x1.99999999999999...p-4 in an analogue of C99 hexfloat notation, where the ... represents an unending sequence of 9's.

The constants 0.2 and 0.3 in your program will also be approximations to their true values. It happens that the closest double to 0.2 is larger than the rational number 0.2 but that the closest double to 0.3 is smaller than the rational number 0.3. The sum of 0.1 and 0.2 winds up being larger than the rational number 0.3 and hence disagreeing with the constant in your code.

A fairly comprehensive treatment of floating-point arithmetic issues is What Every Computer Scientist Should Know About Floating-Point Arithmetic. For an easier-to-digest explanation, see floating-point-gui.de.

Side Note: All positional (base-N) number systems share this problem with precision

Plain old decimal (base 10) numbers have the same issues, which is why numbers like 1/3 end up as 0.333333333...

You've just stumbled on a number (3/10) that happens to be easy to represent with the decimal system, but doesn't fit the binary system. It goes both ways (to some small degree) as well: 1/16 is an ugly number in decimal (0.0625), but in binary it looks as neat as a 10,000th does in decimal (0.0001)** - if we were in the habit of using a base-2 number system in our daily lives, you'd even look at that number and instinctively understand you could arrive there by halving something, halving it again, and again and again.

Of course, that's not exactly how floating-point numbers are stored in memory (they use a form of scientific notation). However, it does illustrate the point that binary floating-point precision errors tend to crop up because the "real world" numbers we are usually interested in working with are so often powers of ten - but only because we use a decimal number system day-to-day. This is also why we'll say things like 71% instead of "5 out of every 7" (71% is an approximation, since 5/7 can't be represented exactly with any decimal number).

So no: binary floating point numbers are not broken, they just happen to be as imperfect as every other base-N number system :)

Side Side Note: Working with Floats in Programming

In practice, this problem of precision means you need to use rounding functions to round your floating point numbers off to however many decimal places you're interested in before you display them.

You also need to replace equality tests with comparisons that allow some amount of tolerance, which means:

Do not do if (x == y) { ... }

Instead do if (abs(x - y) < myToleranceValue) { ... }.

where abs is the absolute value. myToleranceValue needs to be chosen for your particular application - and it will have a lot to do with how much "wiggle room" you are prepared to allow, and what the largest number you are going to be comparing may be (due to loss of precision issues). Beware of "epsilon" style constants in your language of choice. These are not to be used as tolerance values.

Mystery behind presentation of Floating Point numbers

This is purely an artifact of how an NSNumber prints itself.

JSONSerialization is implemented in Objective-C and uses Objective-C objects (NSDictionary, NSArray, NSString, NSNumber, etc.) to represent the values it deserializes from your JSON. Since the JSON contains a bare number with decimal point as the value for the "amount" key, JSONSerialization parses it as a double and wraps it in an NSNumber.

Each of these Objective-C classes implements a description method to print itself.

The object returned by JSONSerialization is an NSDictionary. String(describing:) converts the NSDictionary to a String by sending it the description method. NSDictionary implements description by sending description to each of its keys and values, including the NSNumber value for the "amount" key.

The NSNumber implementation of description formats a double value using the printf specifier %0.16g. (I checked using a disassembler.) About the g specifier, the C standard says

Finally, unless the # flag is used, any trailing zeros are removed from the fractional portion of the result and the decimal-point wide character is removed if there is no fractional portion remaining.

The closest double to 98.39 is exactly 98.3900 0000 0000 0005 6843 4188 6080 8014 8696 8994 1406 25. So %0.16g formats that as %0.14f (see the standard for why it's 14, not 16), which gives "98.39000000000000", then chops off the trailing zeros, giving "98.39".

The closest double to 98.40 is exactly 98.4000 0000 0000 0056 8434 1886 0808 0148 6968 9941 4062 5. So %0.16g formats that as %0.14f, which gives "98.40000000000001" (because of rounding), and there are no trailing zeros to chop off.

So that's why, when you print the result of JSONSerialization.jsonObject(with:options:), you get lots of fractional digits for 98.40 but only two digits for 98.39.

If you extract the amounts from the JSON object and convert them to Swift's native Double type, and then print those Doubles, you get much shorter output, because Double implements a smarter formatting algorithm that prints the shortest string that, when parsed, produces exactly the same Double.

Try this:

import Foundation

struct Price: Encodable {
let amount: Decimal
}

func printJSON(from string: String) {
let decimal = Decimal(string: string)!
let price = Price(amount: decimal)

let data = try! JSONEncoder().encode(price)
let jsonString = String(data: data, encoding: .utf8)!
let jso = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let nsNumber = jso["amount"] as! NSNumber
let double = jso["amount"] as! Double

print("""
Original string: \(string)
json: \(jsonString)
jso: \(jso)
amount as NSNumber: \(nsNumber)
amount as Double: \(double)

""")
}

printJSON(from: "98.39")
printJSON(from: "98.40")
printJSON(from: "98.99")

Result:

Original string: 98.39
json: {"amount":98.39}
jso: ["amount": 98.39]
amount as NSNumber: 98.39
amount as Double: 98.39

Original string: 98.40
json: {"amount":98.4}
jso: ["amount": 98.40000000000001]
amount as NSNumber: 98.40000000000001
amount as Double: 98.4

Original string: 98.99
json: {"amount":98.99}
jso: ["amount": 98.98999999999999]
amount as NSNumber: 98.98999999999999
amount as Double: 98.99

Notice that both the actual JSON (on the lines labeled json:) and the Swift Double versions use the fewest digits in all cases. The lines that use -[NSNumber description] (labeled jso: and amount as NSNumber:) use extra digits for some values.

Dividing two doubles gives wrong result in Xcode console

Well this is probably due to this issue: Why not use Double or Float to represent currency?. Were you thinking that Apple implemented floating point wrong? In the Java World, these questions came up quite often, and BigDecimal was the solution, you can read about that.

Loss of precision in float substraction with Swift

You should calculate by an integer to avoid the floating point precision issue. Therefore, convert the float to an integer at first.

Is what you want the following code?

func gcd(var m: Int, var n: Int) -> Int {
if m < n {
(m, n) = (n, m)
}
if n == 0 {
return m
} else if m % n == 0 {
return n
} else {
return gcd(n, m % n)
}
}

func fractionize(var quantity: Float) -> String {
var i = 0
while quantity % 1 != 0 {
quantity = quantity * 10
i += 1
}

var numerator = Int(quantity)
var denominator = Int(pow(Double(10), Double(i)))

let divisor = gcd(numerator, denominator)

numerator /= divisor
denominator /= divisor

var wholeNumber = 0
if numerator > denominator {
wholeNumber = numerator / denominator
numerator -= denominator * wholeNumber
}

if wholeNumber > 0 {
return "\(wholeNumber) \(numerator)/\(denominator)"
} else {
return "\(numerator)/\(denominator)"
}
}

println(fractionize(0.4)) // 2/5
println(fractionize(1.4)) // 1 2/5
println(fractionize(2.4)) // 2 2/5
println(fractionize(0.5)) // 1/2
println(fractionize(0.7)) // 7/10

Why does converting an integer string to float and double produce different results?

OK, please look at the floating point converter at https://www.h-schmidt.net/FloatConverter/IEEE754.html. It shows you the bits stored when you enter a number in binary and hex representation, and also gives you the error due to conversion. The issue is with the way the number gets represented in the standard. In floating point, the error indeed comes out to be -1.

Actually, any number in the range 77777772 to 77777780 gives you 77777776 as the internal representation of mantissa.



Related Topics



Leave a reply



Submit