Hello. I am currently learning Rust.
I was looking at how the % operator works with floating point numbers in Playground.
I did this,
fn main() {
println!("{}", 0.1024 % 0.01);
}
and I got the output.
Standard Output
0.002400000000000003
OK. I can see this.
fn main() {
println!("{}", 0.1999 % 0.01);
}
Standard Output
0.00989999999999999
Good.
fn main() {
println!("{}", 0.1200 % 0.01);
}
Standard Output
0.009999999999999993
What?
I expected the output to be 0 or its approximation. However, the results are as above.
As a test, I ran the same statement by the f32 type, and got the expected results.
This means that someone forgot that 0.1 and 0.01 don't really exist in Rust (and most other contemporary languages).
Straight from Wikipedia: for example, the decimal number 0.1 is not representable in binary floating-point of any finite precision; the exact binary representation would have a "1100" sequence continuing endlessly.
Once you recall that and realize that “simple” examples which you are trying to use here are not simple at all… I guess you would be able to realize what's happening and why. Try to use numbers like 0.5 or 0.625 — these would be easier to reason about.
P.S. That's one reason which explains why COBOL is so hard to replace in banks. COBOL (unlike most modern languages) do have 0.1 and it works like you would expect. Most other languages (Rust including) only have approximations.
Specifically what happens is that 0.12 is rounded down when converted to binary with f64 precision. With more precision it is really closer to 0.11999999999999999556.
You're dealing with numerical instability. Because the % operator is discontinuous, tiny rounding errors can become very big errors.
0.12 is converted to its closest f64, which is actually *exactly*
0.11999999999999999555910790149937383830547332763671875
0.01 is converted to its closest f64, which is actually *exactly*
0.01000000000000000020816681711721685132943093776702880859375​
And thus the remainder is *exactly*
0.00999999999999999326927291320998847368173301219940185546875
which Rust shows as
0.009999999999999993
because that's enough to distinguish between the previous value
0.009999999999999992 which is sufficient to describe its exact value
0.0099999999999999915345494372331813792698085308074951171875
and the next value
0.009999999999999995 which is sufficient to describe its exact value
0.00999999999999999500399638918679556809365749359130859375​
Floating point numbers are an excellent solution to the whole "represent an infinite range of numbers using a finite number of bits" problem, but it's a leaky abstraction and sometimes you need to understand how they are implemented to know where those edge cases are.