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, or0x1.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, or0x1.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 Double
s, 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
How to Delay a Return-Statement in Swift
Why Does User Defaults Publisher Trigger Multiple Times
Swift Initialization Rule Confusion
How to Convert an Array to List in Realm
Swiftui Map Overlays Without UIviewrepresentable
How to Change The Default Skscene That Displays on Startup
Cropping Cgrect from Avcapturephotooutput (Resizeaspectfill)
Swift: Strange Behavior About Unwrapping
Way to Check If Up or Down Button Is Pressed with Nsstepper
Why Do I Get Rgb Values of (0,0,0) for an Nsimage with a Transparent Background
Reduce Float Precision Using Regexp in Swift
Appdelegate#Applicationdidfinishlaunching Not Called for Swift 4 Macos App Built from Command Line
Cannot Authenticate User for Aws Appsync with Swift Sdk
Can Not Get Correct Position of View in Swiftui
Flattened Objects in JSON File to Nested Object Structure in Swift