1. Overview
In this tutorial, we’ll see how to properly compare floating point numbers in Scala. Due to the way they’re stored, their value isn’t exact, so comparisons aren’t as easy as comparing integers or strings.
2. Floating Point Numbers
Floating point numbers are a prevalent source of bugs in programming. Unlike integer numbers, where 1 + 2 is always 3, with floating points this may not happen:
scala> 1 + 2
res0: Int = 3
scala> 0.1 + 0.2
res1: Double = 0.30000000000000004
This happens due to the very definition of how floating point numbers work in computer science.
3. Comparing Floating Point Numbers
So how should we compare floating points? Since arithmetic operations don’t produce exact results, this could cause bugs:
scala> val someMagicThreshold = 0.3
someMagicThreshold: Double = 0.3
scala> def checkThreshold(a: Double, b: Double): Boolean = {
| return a + b == someMagicThreshold
| }
checkThreshold: (a: Double, b: Double)Boolean
scala> checkThreshold(0.1, 0.2)
res0: Boolean = false
There are quite a few solutions possible such as:
- Avoiding using floating point numbers if precision is strictly essential (in financial services, for instance). Scaling the units is a very common solution. Instead of representing all measurements in meters, we can represent them in millimeters, making all values integers.
- Using a specific high-precision library/class for that. Java has the BigDecimal class, which can also be used in Scala. Unfortunately it makes our code more verbose, and it may not be a performant solution if we’re writing specific low-latency applications.
- Implementing custom comparison with a given precision error.
3.1. Comparisons With Precision Error
Let’s dig more into the last one. The idea is straightforward: we give a small margin instead of doing an exact comparison. Even if the floating point arithmetic isn’t exact, the difference is usually small. The previous example returned 0.30000000000000004, which has a really small error:
def almostEqual(a: Double, b: Double, precision: Double): Boolean = {
(a - b).abs < precision
}
And now, we can use it:
scala> almostEqual(1.0, 1.001, 0.0001)
res1: Boolean = false
scala> almostEqual(1.0, 1.0001, 0.0001)
res2: Boolean = true
3.2. Custom Comparison Operator
We can make the comparison even cleaner by using Scala implicit classes and defining a new operator:
scala>
class withAlmostEquals(d:Double) {
def ~=(y: Double, precision: Double) = (d - y).abs <= precision
}
scala> implicit def add_~=(d:Double) = new withAlmostEquals(d)
scala> 0.0 ~= (0.00001, 0.0001)
res0: Boolean = true
And as the last step, we can create an implicit precision to avoid passing the precision in every call and to ensure some consistency across the codebase:
scala> case class Precision(val p:Double)
defined class Precision
scala>
class withAlmostEquals(d:Double) {
def ~=(y: Double)(implicit precision:Precision) = (d - y).abs <= precision.p
}
defined class withAlmostEquals
scala> implicit def add_~=(d:Double) = new withAlmostEquals(d)
add_$tilde$eq: (d: Double)withAlmostEquals
scala> 0.0~=0.0
<console>:18: error: could not find implicit value for parameter precision: Precision
0.0~=0.0
^
scala> implicit val precision = Precision(0.001)
precision: Precision = Precision(0.001)
scala> 0.0 ~= 0.00001
res3: Boolean = true
And now it works cleanly due to Scala’s powerful features.
4. Conclusion
In this article, we’ve learned the problems of comparing floating point numbers and possible solutions. We detailed how to leverage the Scala language’s powerful features, such as implicit conversions, to create an easy-to-use comparison method.