Format f32 with correct precision

I'm a bit confused by the way floating point numbers are converted to strings. There is some conversion of data to and from strings involved (using serde_json, but I think this is not relevant). I see that some precision is lost and I'm wondering how I can do this correctly.

As an example, suppose I have this program:

fn main() {
    let f: f32 = 67108872.0;
    println!("{:.1}\n{}", f, f.to_string());
}

What it prints is

67108872.0
67108870

Note the difference in the last digit. I certainly expect the result of the first line. My problem is that I'm not able to know the precision I'd have to put in the format string (.1 in this case) beforehand. Other examples that fail to print correctly use more decimal places, this integer is just an example.

What I need is a way to print the f32 values in a way that does not loose precision.

I've also tried dtoa, but results are the same.

This is not a problem with printing. This is inherent: f32 has 24 bits of mantissa, which means the only integers it can represent exactly are those less than around 16 million in absolute value. You have 67 million, so the number you have is not exactly representable as an f32. So your expectation to print these kinds of numbers exact to 1 decimal digit (i.e., a total of 9 significant digits) is impossible to satisfy.

5 Likes

But why does the print with format {:.1} work then? This is what confuses me most. If 67108872.0 would not fit into a f32, why is it printed correctly in the first line then?

It doesn't "work", you just happened to be lucky. Or, rather, it's not printed exactly. If you try the same code with the value 67108873.0_f32, you will also get 67108872 when you ask the system to print it with excessive precision, and 67108870 when printed with automatic precision. This is because all three values round to the same internal representation in f32, and when you ask for a too high number of significant digits, then it will try to squeeze out more digits, which, however, aren't meaningful.

5 Likes

You're just lucky in that case, since a lot of numbers round to 67108872.
to_string probably rounds to the least integer number of digits of precision. Even though there is a fractional amount of precision left over, it is not reliable to print another digit.

fn main() {
    let f0: f32 = 67108872.0;
    for i in 0..10 {
        let f = f0 + (i as f32);
        println!("{} {:.1}  {}", i, f, f.to_string());
    }
}


0 67108872.0  67108870
1 67108872.0  67108870
2 67108872.0  67108870
3 67108872.0  67108870
4 67108880.0  67108880
5 67108880.0  67108880
6 67108880.0  67108880
7 67108880.0  67108880
8 67108880.0  67108880
9 67108880.0  67108880

edit: I basically said the same thing as H2CO3.

2 Likes

Try out this site: https://float.exposed/0x4c800001

You can flip the last bit of the representation and see that it changes the value by 8, not by one.

So, for example, 67108869.0 and 67108875.90215694861897 with both give you the same f32 as the 67108872.0 that you're using.

8 Likes

Ok, thank you all, especially for your thorough explanations. I just learned a lot about floating point numbers :slight_smile:

1 Like