# Why isn't format!("{:.2}", x) accurate?

``````fn main() {
println!("{:.2}", 4.305); // 4.30 -> innaccurate
println!("{:.2}", round_number(4.305)); // 4.31 -> accurate
}

pub fn round_number(nr: f64) -> f64 {
(nr * 100f64).round() / 100f64
}
``````

Why isn't `{:.2}` accurate by default?

5 Likes

To elaborate, this demonstrates the precise behavior at play relatively well:

``````fn main() {
println!("{:.2}", 4.305); // 4.30 -> “innaccurate”
println!("{:.80}", 4.305); // 4.30 -> full accuracy of *actual* float
}
``````
``````4.30
4.30499999999999971578290569595992565155029296875000000000000000000000000000000000
``````

In case you wonder now “why does multiplying by 100 give the more ‘correct’ seeming result then?”, the answer is, that the deviation between `4.30499999999999971578290569595992565155029296875` and `4.305` is so small, that the floating point number closest to `430.499999999999971578290569595992565155029296875` turns out to be exactly `430.5`.

``````fn main() {
println!("{:.80}", 430.499999999999971578290569595992565155029296875);
}
``````
``````430.50000000000000000000000000000000000000000000000000000000000000000000000000000000
``````

Finally, if you wonder, why does `println!("{}", 4.305);` print something different than `println!("{:.80}", 4.305)`, almost as if the float in question was exactly `4.305`, the answer to that question in turn is: When printing a float without specifying a number of digits, then the printing algorithm will choose only as many digits as are necessary in order to converting the result back into a float to create an accurate result. Since turning the decimal number `4.305` into a float will correctly result in the floating point number `4.30499999999999971578290569595992565155029296875` being generated, that means that `4.305` is a fine choice for printing it.

10 Likes

And if you're interested in further details, there is this gem and its accompanying article, which brightened my whole day when I first stumbled across them: the world just seems a better place when you discover something so well done.

8 Likes

Ah, very nice! I’ve googled for similar tools quickly, but didn’t come across one where I could properly link to the number in question ^^

1 Like

On top of the other good answers, it's worth noting that floating point is not meant to be a perfect representation (not even of numbers with an exact representation in binary); they are meant to be an approximation of the infinite set of real numbers into a finite set, designed to be amenable to numerical analysis and also to get "good enough" results most of the time if you're not doing numerical analysis of your algorithms.

In turn, numerical analysis is the field of mathematics that looks at how approximations behave algorithmically, and aims to either tell you that your algorithm can't be used to get useful results with an approximation, or what the error bound is for your algorithm. For example, naïve floating point summation has an error bound that's proportional to the number of entries in the list to be summed; a good compensated summation algorithm has an error bound that's independent of the number of entries in the list, and only depends on the range of values in the list and the precision of the floating point types in use.

5 Likes

Because your example doesn't actually show what you think it does. As proof, here's the same code with a different value to "show" that `{:.2}` is accurate but `round_number` isn't:

``````fn main() {
println!("{:.2}", 4.3049999999999999); // 4.30 -> accurate
println!("{:.2}", round_number(4.3049999999999999)); // 4.31 -> inaccurate
}

pub fn round_number(nr: f64) -> f64 {
(nr * 100f64).round() / 100f64
}
``````

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=da4aedef8484d127fb24321f50529c5d

Rounding is a horrible operation, because any error in the input can be greatly magnified in the output.

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.