Maybe what you need is not floating-point numbers

2023.02.26. Tags: Golang

There are many features in programming languages that are prone to problems and should be used with caution. One of the most famous examples is the goto operator. Floating-point numbers should also be considered as such features.

Why?

I will not enumerate all possible problems of floating point numbers: most likely, you are already familiar with many of them, and if not, I recommend to read the following articles:

Such behavior creates an additional cognitive load on the developer and room for errors. How many programmers can, without thinking much, say the minimum step for changing a specific floating-point number?

Don’t get me wrong: the message of the article is not that floating point numbers should not be used. They are a tool with its own use cases. This article is about when it is better to use other tools.

So what do we do?

If you want to use floating point numbers somewhere, you can consider the following possibilities instead:

1. Integers

Identifiers, counters, ordinal numbers are the most obvious examples of values that are better represented as integers. But there are many other cases in which, if you think twice, integers turn out to be the best solution.

For example, if you want to store a fractional number of kilometers, then it may be worth presenting them not as kilometers, but as meters / centimeters / millimeters:

// ❌ it is not exactly 17.529 kilometers
distanceKilometers := 17.529

// 👍 that's better!
distanceMeters := 17529

You can do the same with other physical quantities.

2. Decimal

In cases where the needed precision is not known beforehand, but it is very important that the representation in decimal format exactly matches the stored one (eg, for money amounts), you can use special libraries for decimal numbers.

In Python, for example, there is a module decimal in the standard library:

>>> my_salary = 10.1
>>> print("{:.16f}".format(my_salary))
10.0999999999999996

>>> from decimal import *
>>> my_salary = Decimal('10.1')
>>> print("{:.16f}".format(my_salary))
10.1000000000000000

3. Rational numbers

Sometimes the numbers are divided by some known number, which may not be a multiple of ten:

import "fmt"

func main() {
    fullPageWidth := 1000
    blocksAmount := 3
    blockWidth := fullPageWidth / blocksAmount

    if (blockWidth * blocksAmount) != fullPageWidth {
        fmt.Println("We need a perfect fit!") 
    } else {
        fmt.Println("Perfect!")
    }
}

//=> We need a perfect fit!

In such cases you can use rational numbers:

import (
    "fmt"
    "math/big"
)

func main() {
    fullPageWidth := new(big.Rat).SetInt64(1000)
    blocksAmount := new(big.Rat).SetInt64(3)
    blockWidth := new(big.Rat).Quo(fullPageWidth, blocksAmount)

    if new(big.Rat).Mul(blockWidth, blocksAmount).Cmp(fullPageWidth) != 0 {
        fmt.Println("We need a perfect fit!")
    } else {
        fmt.Println("Perfect!")
    }
}

//=> Perfect!

4. Specialized types

Many databases and some languages already have predefined types for units such as time and money. It is worth learning about their availability and features: such types have already thought out the storage method and all the necessary operations.

Conclusion

If you want to use fractional numbers, floating point numbers may not be what you need.

Floating point numbers should be used when precision is not important and approximate values are fine.

In other cases, you might use integers, decimals, rational numbers, and specialized types for values such as money and time instead.