Nice floating point number formatting

Python can do it.

>>> 10.0
10.0
>>> (10 ** -20)
1e-20
>>> (10 ** -20) * (1 + 2**-52)
1.0000000000000001e-20

Haskell can do it.

λ> 10 :: Double
10.0
λ> (10 ** (-20))
1.0e-20
λ> (10 ** (-20)) * (1 + (2 ** (-52)))
1.0000000000000001e-20

Why can't rust do it?

(code)

   {} -   "10" -   "0.00000000000000000001" - "0.000000000000000000010000000000000001"
 {:?} -   "10" -   "0.00000000000000000001" - "0.000000000000000000010000000000000001"
 {:e} -  "1e1" -                    "1e-20" -                 "1.0000000000000001e-20"

I'm talking about automatic switching between exponential and non-exponential form based on magnitude; printing numbers fit for human consumption. The default format is so silly that you're basically required to use exponential format in any sort of general context; but then I end up with those occassional late nights where I see 5e0 and it takes me a couple of seconds to remember whether this is larger than or less than 1.

That's just silly!


...and yes, I know what you're about to say, and I've already tried exactly that. A wrapper type doesn't cut it. You can't wrap every element of every Vec and MyOneOffStruct that you ever need to debug.

Nice should be the default.

8 Likes

In C's printf, this would be the %g format.

   g, G   The double argument is converted in style f or e (or F or E
          for G conversions).  The precision specifies the number of
          significant digits.  If the precision is missing, 6 digits are
          given; if the precision is zero, it is treated as 1.  Style e
          is used if the exponent from its conversion is less than -4 or
          greater than or equal to the precision.  Trailing zeros are
          removed from the fractional part of the result; a decimal
          point appears only if it is followed by at least one digit.
2 Likes

It's close (and my original title was "General floating number point formatting", after what I assume "g" stands for), though my preference is to default to minimal round-trip precision like our existing representations do (as well as the defaults in Haskell and Python).

I have never had a case where "{}" or "{:e}" for a float was the formatting I wanted. I have always had to work around it.

In serde_json I have been using dtoa to get reasonable printing of floats. The API is somewhat less convenient but it is faster for serde_json's use case than std::fmt, and it seems to use the same formatting as Python in your examples. [playground]

extern crate dtoa;

use std::{f64, io};

fn print_f64(f: f64) {
    dtoa::write(io::stdout(), f).unwrap();
    println!();
}

fn main() {
    print_f64(10f64);
    print_f64(10f64.powi(-20));
    print_f64(10f64.powi(-20) * (1f64 + f64::EPSILON));
}
10.0
1e-20
1.0000000000000001e-20
6 Likes

Lacking a %g equivalent is frustrating. There doesn't even seem to be a good crate for this (@dtolnay's does not appear to support displaying a particular number of significant digits). I pretty much only ever want to print a float with a specific number of significant digits, and rust does not seem to make this possible short of calling out to libc.

I've hand-rolled something like this when I've needed to set a minimum number of significant digits before: Rust Playground

2 Likes

The %g format specifier for printf is different from what current Python does. Python produces a short string which converts back to the original value. %g is lossy. But then, C does not require that string-to-floating-point is exact, either, so C isn't really a good model here (Java is much better in this regard, as far as languages with a separate specification go).