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 to 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 some 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.