Skip to main content

to_precision/
lib.rs

1//! A function that works like javascript's `toPrecision`.
2//!
3//! Internally it rounds and then uses the built-in algorithm, so it will give different results to
4//! `toPrecision`. They may converge over time.
5use std::fmt;
6
7pub trait FloatExt {
8    type Display: fmt::Display;
9    fn to_precision(self, p: u8) -> Self::Display;
10}
11
12const MAX_FRACTION_DIGITS: u8 = 21;
13
14impl FloatExt for f64 {
15    type Display = F64Display;
16    fn to_precision(self, p: u8) -> Self::Display {
17        assert!(
18            1 <= p && p <= MAX_FRACTION_DIGITS,
19            "precision must satisfy 1 <= p ({}) <= {}",
20            p,
21            MAX_FRACTION_DIGITS
22        );
23        F64Display(self, p.into())
24    }
25}
26
27// u16 should be big enough for the exponent/precision
28#[derive(Debug)]
29pub struct F64Display(f64, i32);
30
31impl fmt::Display for F64Display {
32    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33        let mut x = self.0;
34
35        if x.is_nan() {
36            return write!(f, "NaN");
37        }
38        if x == 0. {
39            return write!(f, "0");
40        }
41        if x < 0. {
42            x = -x;
43            write!(f, "-")?;
44        }
45        if !x.is_finite() {
46            return write!(f, "∞");
47        }
48        // round and defer to std impl
49        write!(f, "{}", to_sig_figs(self.0, self.1))
50    }
51}
52
53/// Round the number to the given significant figures.
54fn to_sig_figs(x: f64, sf: i32) -> f64 {
55    // println!("to_sig_figs({}, {})", x, sf); // DEBUG
56    let e = ten_power_leq(x);
57    // println!("e = {}", e); // DEBUG
58    
59    // two branches depending on the sign of e - sf + 1
60    // We need this to combat fp error: although e.g. 0.1 is representable in fp, we won't get that
61    // answer when doing 10000 * 0.000001.
62    let p = e - sf + 1;
63    if p < 0 {
64        let tens = (10.0f64).powi(-p);
65        (x * tens).round() / tens
66    } else {
67        let tens = (10.0f64).powi(p);
68        (x / tens).round() * tens
69    }
70}
71
72/// Return integer e such that `10^e <= x < 10^(e+1)`
73fn ten_power_leq(x: f64) -> i32 {
74    assert!(x != 0., "power of 10 only makes sense on nonzero numbers");
75    let x = x.abs();
76    let log10 = x.log10().floor();
77    debug_assert!(i32::MIN as f64 <= log10 && log10 <= i32::MAX as f64);
78    debug_assert_eq!(log10 as i32 as f64, log10);
79    let log10 = log10 as i32;
80    // we might be off by 1 because of fp precicision errors.
81    if 10.0f64.powi(log10 + 1) < x {
82        log10 + 1
83    } else {
84        log10
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::FloatExt as _;
91    use std::f64;
92
93    #[test]
94    fn ten_power_leq() {
95        for (input, output) in vec![(1., 0), (-1., 0), (0.1, -1), (0.99999999999, -1)] {
96            assert_eq!(super::ten_power_leq(input,), output);
97        }
98    }
99
100    #[test]
101    fn to_sig_figs() {
102        for (x, sf, expected) in vec![
103            (1., 3, 1.),
104            (100., 3, 100.),
105            (1234., 3, 1230.),
106            (9999., 4, 9999.),
107            (9999., 3, 10_000.),
108            (9999., 1, 10_000.),
109            (0.1, 3, 0.1),
110            (0.1234, 3, 0.123),
111        ] {
112            assert_eq!(
113                super::to_sig_figs(x, sf),
114                expected,
115                "to_sig_figs({}, {}) = {}, {}",
116                x,
117                sf,
118                super::to_sig_figs(x, sf),
119                expected
120            );
121        }
122    }
123
124    #[test]
125    #[should_panic]
126    fn bad_precision() {
127        1.0f64.to_precision(0);
128    }
129
130    #[test]
131    fn it_works() {
132        for (input, sf, expected) in vec![
133            (f64::NAN, 3, "NaN"),
134            (f64::INFINITY, 3, "∞"),
135            (f64::NEG_INFINITY, 3, "-∞"),
136            (0., 3, "0"),
137            (-0., 3, "0"),
138            (0.999, 3, "0.999"),
139            (0.9999, 3, "1"),
140            (0.7000000000000002, 5, "0.7"),
141            (f64::from_bits(4603579539098121012), 4, "0.6"),
142        ] {
143            assert_eq!(input.to_precision(sf).to_string(), expected);
144        }
145    }
146}