Quick note about why this comparison works, because it is actually not that obvious.
Converting float to a string is a very complicated topic. Years of work went into this (example: github.com/ulfjack/ryu). The goal is to find the representation that converts back to the initial value. Via IEEE 754 -> 5.12 (this is the standard that defines float
and double
):
Implementations shall provide conversions between each supported binary format and external decimal character sequences such that, under roundTiesToEven, conversion from the supported format to external decimal character sequence and back recovers the original floating-point representation, except that a signaling NaN might be converted to a quiet NaN.
The actual goal is to find the shortest representation that obeys this rule. For example:
- we are converting
"1.1"
and our floating point system has 1.1
-> exact conversion, everything is well
- we are converting
"1.1"
and our floating point system has 1.0
and 1.2
, there is no 1.1
. Ideally the user should provide more digits. In this case floating point library can:
- abort
- return the closest value under the current rounding mode
In some numerical context you will see people using hexadecimal strings to specify float literals. This side-steps the problem entirely.
Anyway, comparing floats using their string representation is correct in this case, as we are looking for equality. If the value was different then the string representation would have to be different. We all knew this, but it is nice to know that this is guaranteed.
print(Double.pi.nextDown) // 3.1415926535897927
print(Double.pi) // 3.141592653589793
print(Double.pi.nextUp) // 3.1415926535897936
You can also compare them directly (==
operator), or use bitPattern
:
print(pi_best == Double.pi) // false
print(pi_atan == Double.pi) // true
print(pi_best.bitPattern == Double.pi.bitPattern) // false
print(pi_atan.bitPattern == Double.pi.bitPattern) // true
Note that the bitPattern
method depends on the floating point library. For example IEEE 754 which specifies double
and float
also specifies a vide range of decimal
floating points. You are not allowed to use decimal.bitPattern
to test for equality, as the same value may be specified in a few different ways. For example 10 = 10 * 10^0
, but also 10 = 1 * 10^1
.
How far off is 355.0 / 113
?
People tried to approximate for ages. I guess this where 355.0 / 113
comes from. How bad is it?
assert(pi_atan < pi_best)
var d = pi_atan
var counter = 0
while d != pi_best {
d = d.nextUp
counter += 1
}
print(counter) // 600699552
print(pi_best.bitPattern - pi_atan.bitPattern) // 600699552, you can to this!
355.0 / 113
was invented/discovered by Chinese mathematician and astronomer Zu Chongzhi in the 5th century. Wikipedia states that:
ā is the best rational approximation of Ļ with a denominator of four digits or fewer, being accurate to six decimal places.
@Nobody1707 used it because it is cute and short. But it is not that precise.
Note that the subtraction trick is not guaranteed to work in every floating point library, but it will work on IEEE 754 binary floating points.
Is Double.pi
even correct?
Kind of. Maybe. I guess it is "ok".
is an irrational number, meaning that it cannot be expressed as a ratio of two integers, for example 22/7. This makes it difficult to represent in a floating point system.
In reality sits between 2 representable numbers and the library authors choose which one to take. Double.pi
documentation says (emphasis mine):
This value is rounded toward zero to keep user computations with angles from inadvertently ending up in the wrong quadrant. A type that conforms to the FloatingPoint
protocol provides the value for pi at its best possible precision.
print("pi", Double.pi) // 3.141592653589793
// The actual value: 3.1415926535897932384ā¦
print("pi.nextUp", Double.pi.nextUp) // 3.1415926535897936
Is this correct? IDK.
Step count
When talking about the 355.0 / 113
we calculated the number of "steps" to get from the calculation result to the "correct" value. This is actually done in real life. Any reputable math library will state how accurate the result are.
For example IEEE 754 requires all of the basic operations (+-*/, remNear, fma
) to calculate infinitely precise result and then round using the current rounding mode. This way the result requires 0
steps to get to the correct value, which makes it as close as it could be.
For some more complicated functions it may be computationally infeasible to calculate the correct result. In such cases the tables are used. Or the library widens the margin of error, for example by stating: all of the results are correct within 2 steps (as in 2 calls to next[Up/Down]
). The lingo may be a bit different, but it boils down to this.
The reason why the "steps" are used is the distribution of the exactly representable numbers. I don't really want to go into that. Please never ever use hard-coded epsylon
to compare floating points.