Would this test pass or fail?:
@Test
fun `adding one tenth ten times equals one`() {
var result: Double = 0.0
repeat(10) {
result += 0.1
}
assert(result == 1.0)
}
It fails! But why?
Non-decimal base
Floating-point numbers like Float
and Double
are internally represented like this:
mantissa * base ^ exponent
For humans, the base of 10
is very common. We use the decimal system.
In the decimal system 0.1
can be easily represented like this:
1 * 10^(-1) = 0.1
Computers don’t use a base of 10
but a base of 2
.
Also, the mantissa
is stored in a binary format.
So for the computer 0.1
looks more like this:
1.600000023842… * 2^(-4) = 0.100000001490…
We see that 0.1
is not a “clean number” for the computer.
In fact, the computer cannot precisely represent 0.1
.
There is a small error.
But it’s big enough so that summing up 0.1
ten times doesn’t equal 1.0
exactly.
There we can already see the second problem: precision.
Precision
Floating-point numbers are not represented with infinite precision. Instead, it’s actually very limited.
Look at this example: What will it print?
var x: Float = 100_000f
repeat(1_000) {
x += 0.001f
}
println(x)
Math is telling us:
100_000 + 0.001 * 1_000 = 100_001
However, if we run the code it prints:
100000.0
Why is that?
It’s because we exceed Float
’s precision already with the first addition:
100_000 + 0.001 = 100_000.001
. Float
cannot store so many digits, so the least significant digits are cut.
As a result, we end up with 100_000
again after the addition.
That game repeats 1000 times.
And we eventually end up with the same number as we started.
We could convert Float
to Double
because it has more precision:
var x: Double = 100_000.0
repeat(1_000) {
x += 0.001
}
println(x)
It prints:
100001.00000000384
So it worked now, but it has a very small error (the additional 0.00000000384
) because of the non-decimal base.
However, Double
suffers from the same problem.
The only difference is that the distance of the numbers has to be larger for the problem to surface.
Try 100_000_000_000_000.0
as staring number and you’ll see we end up with the same precision problem.
Equality of floating-point numbers
The problems of non-decimal base and limited precision are inherent in floating-point numbers. Because of them, floating-point numbers are never used when dealing with money in software. It depends on your specific problem if such errors are acceptable. There are alternatives, like fixed-point numbers, however, they have other limitations.
If your domain can accept the limitations of floating-point numbers, you might still need to compare two numbers. The trick here is to not compare equality but to check if the two numbers differ within an acceptable range. You have to define a precision that is appropriate to your domain.
Coming back to the very first example: Let’s assume we’re dealing with lengths in meters here. In our domain an error of 1mm is acceptable, our test could look like this:
@Test
fun `adding 10cm ten times equals 1m`() {
var lengthInMeters: Double = 0.0
repeat(10) {
lengthInMeters += 0.1 // = 10cm
}
val expectedLengthInMeters = 1.0
val precisionInMeters = 0.001 // = 1mm
if (abs(expectedLengthInMeters - lengthInMeters) < precisionInMeters) {
// `expectedLengthInMeters` and `lengthInMeters` are equal
// (within `precisionInMeters`)
} else {
fail("$expectedLengthInMeters and $lengthInMeters are different!")
}
}
We can also extract the “equality logic” into a dedicated function:
fun Double.equals(other: Double, precision: Double) =
abs(this - other) < precision
It can be used like:
if (expectedLengthInMeters.equals(lengthInMeters, precision = precisionInMeters)) { …
Note: the abs
is necessary so it doesn’t matter which value is bigger and which is smaller. Then the check also works correctly when this
and other
are swapped, as the difference will never be negative.
I hope this post helped you to understand why it’s a bad idea to compare two floating-point numbers using ==
and how to avoid errors in that regard.
Also, have a look at this Floating Point Converter/Calculator.
It can be used to understand the binary representation of Floats even better.