Published on

Handling decimal values

Authors

Let's find out how to handling decimal values with explore from standard library and try 3rd party library.

Float64

Binary floating point types like float64 cannot represent decimal fractions such as 0.1 precisely due to the limitations of representing decimal numbers in binary format. This can lead to small errors that accumulate over time and cause unexpected behavior in programs.


_11
package main
_11
_11
import "fmt"
_11
_11
func main() {
_11
var n float64 = 0
_11
for i := 0; i < 1000; i++ {
_11
n += .01
_11
}
_11
fmt.Println(n)
_11
}

Can you guess the result ? You might expect that it prints out 10, but it actually prints 9.999999999999831. Over time, these small errors can really add up!

Conclusion

We cannot use float64

big.Rat

Although big.Rat is suitable for representing rational numbers, Decimal is a better choice for representing money. The reason for this is illustrated in the following example:

Suppose you are using big.Rat to represent two numbers x and y, both equal to 1/3, and then you subtract their sum from 1 to get z = 1 - x - y. When you print out the string representation of each number, the output is truncated to a finite number of digits, say 0.333, 0.333, and 0.333. However, there is a small difference between the sum of x, y, and z and the actual value of 1, which can cause inconsistencies when dealing with money.

In contrast, Decimal provides a more accurate representation of numbers. When you use Decimal to represent x and y as 0.333 (with a precision of 3), and then subtract their sum from 1, the result is exactly 0.334. This ensures that no money is unaccounted for, making it a better choice for representing monetary values.

It's important to note that even with Decimal, there can still be rounding errors when dividing a number equally among several parties. However, Decimal makes it easier to handle such errors compared to big.Rat.

main.go
output

_24
package main
_24
_24
import (
_24
"fmt"
_24
"math/big"
_24
)
_24
_24
func main() {
_24
z, _ := new(big.Rat).SetString("1")
_24
three, _ := new(big.Rat).SetString("3")
_24
x := new(big.Rat).Quo(z, three)
_24
y := new(big.Rat).Quo(z, three)
_24
_24
z = z.Sub(z, x)
_24
z = z.Sub(z, y)
_24
_24
s := new(big.Rat).Add(x, y)
_24
s.Add(s, z)
_24
_24
fmt.Println(x.FloatString(3), "+") // 0.333
_24
fmt.Println(y.FloatString(3), "+") // 0.333
_24
fmt.Println(z.FloatString(3)) // 0.333
_24
fmt.Println("=", s.FloatString(3)) // where did the other 0.001 go?
_24
}

Conclusion

It is much easier to be careful with Decimal than with big.Rat. So we cannot use big.Rat

shopspring/decimal

Package url : https://github.com/shopspring/decimal

Features

  • The zero-value is 0, and is safe to use without initialization
  • Addition, subtraction, multiplication with no loss of precision
  • Division with specified precision
  • Database/sql serialization/deserialization
  • JSON and XML serialization/deserialization

Limitations

  • Only represent numbers with a maximum of 2^31 digits after the decimal point.
  • Cannot use normal calculation operator such as "-,+,/,*" but using API from library

Example

Let's reproduce float64 example above using shopspring/decimal


_18
package main
_18
_18
import (
_18
"fmt"
_18
_18
"github.com/shopspring/decimal"
_18
)
_18
_18
func main() {
_18
n := decimal.NewFromInt(0)
_18
addition := decimal.NewFromFloat(0.01)
_18
for i := 0; i < 1000; i++ {
_18
n = n.Add(addition)
_18
}
_18
fmt.Println(n)
_18
}
_18
_18
// 10

Try it yourself at https://go.dev/play/p/u9V4xJmUKOE

Explore the documentation if need more example https://pkg.go.dev/github.com/shopspring/decimal

Conclusion

Perfect choice to handle decimal value and already have sql serialization/deserialization to use with database

mercari/go-bps

Package URL: https://github.com/mercari/go-bps

go-bps is a Go package to operate the basis point. Handling floating point numbers in programming causes rounding errors. To avoid this, all numerical calculations are done using basis points (integer only) in this package.

What's Basis Point

A per ten thousand sign or basis point (often denoted as bp, often pronounced as "bip" or "beep") is (a difference of) one hundredth of a percent or equivalently one ten thousandth. The related concept of a permyriad is literally one part per ten thousand. Figures are commonly quoted in basis points in finance, especially in fixed income markets.

from Wikipedia

One part per million(ppm) is used as the minimum unit for basis points on this package.


_1
1 ppm = 0.01 basis points = 0.0001 %

Example

Let's use same example to prove addition 0.01, the main difference the result from this types must be use Amounts function to get the decimal values.


_16
package main
_16
_16
import (
_16
"fmt"
_16
_16
"go.mercari.io/go-bps/bps"
_16
)
_16
_16
func main() {
_16
n := bps.NewFromAmount(0)
_16
addition := bps.NewFromPercentage(1)
_16
for i := 0; i < 1000; i++ {
_16
n = n.Add(addition)
_16
}
_16
fmt.Println(n.Amounts())
_16
}

Try it yourself at https://go.dev/play/p/PhtxzqlEwQZ

Conclusion

This library can handle decimal values properly with difference approach but the base code is similar with shopspring/decimal in some parts

General Conclusion

Data typeSourceHandle decimal Properly
float64standard libraryNO
big.Ratstandard libraryNO
decimal.Decimalhttps://github.com/shopspring/decimalYES
bps.BPShttps://github.com/mercari/go-bpsYES