float_pretty_print/
lib.rs

1#![deny(missing_docs)]
2#![forbid(unsafe_code)]
3
4//! Round and format f64 number for showing it to humans, with configurable minimum and maximum width.
5//! It automatically switches between exponential and usual forms as it sees fit.
6//!
7//! It works by trying usual `format!`, possibly multiple times and inspecting the resulting string.
8//!
9//! Only two formatting parameters are supported:
10//!
11//! * width is minimum width
12//! * precision is maximum width
13//!
14//! `###` is printed if it can't output the number with given constraint.
15//!
16//! Example:
17//!
18//! ```
19//!   use float_pretty_print::PrettyPrintFloat;
20//!   assert_eq!(format!("{}", PrettyPrintFloat(3.45)), "3.45");
21//!   assert_eq!(format!("{}", PrettyPrintFloat(12.0)), "12.0");
22//!   assert_eq!(format!("{}", PrettyPrintFloat(120000000.0)), "1.2e8");
23//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12345.0)), "12345");
24//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12.345)), "12.35");
25//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(0.12345)), "0.123");
26//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(1234500000.0)), "1.2e9");
27//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12345.0e-19)), "1e-15");
28//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12345.0e-100)), "1e-96");
29//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12345.0e-130)), "    0");
30//!   assert_eq!(format!("{:5.5}", PrettyPrintFloat(12345.0e+130)), "1e134");
31//!   assert_eq!(format!("{:4.4}", PrettyPrintFloat(12345.0e+130)), "####");
32//!   assert_eq!(format!("{:6.6}", PrettyPrintFloat(12345.0e-130)), "1e-126");
33//! ```
34//!
35//! Supports even Rust 1.23
36
37use std::fmt::{Display, Formatter, Result};
38
39const DEBUG : bool = false;
40
41/// `f64` wrapper to use with formatting code.
42/// See the crate-level doc for details.
43///
44/// ```
45///   use float_pretty_print::PrettyPrintFloat;
46///   assert_eq!(format!("{:4.4}", PrettyPrintFloat(0.00005)), "5e-5");
47/// ```
48pub struct PrettyPrintFloat(pub f64);
49
50#[derive(PartialEq, Eq, Debug)]
51enum NumberClass {
52    Big,
53    Medium,
54    Small,
55    Zero,
56    Special,
57    Unprintable,
58}
59
60impl PrettyPrintFloat {
61    fn cls(&self) -> NumberClass {
62        let mut x = self.0;
63        if !x.is_finite() {
64            return NumberClass::Special;
65        }
66        if x < 0.0 {
67            x = -x;
68        }
69        if x == 0.0 {
70            return NumberClass::Zero;
71        }
72        if x > 99999.0 {
73            return NumberClass::Big;
74        }
75        if x < 0.001 {
76            return NumberClass::Small;
77        }
78        return NumberClass::Medium;
79    }
80}
81
82impl Display for PrettyPrintFloat {
83    fn fmt(&self, fmt: &mut Formatter) -> Result {
84        let mut width_min = fmt.width().unwrap_or(3);
85        let mut width_max = fmt.precision().unwrap_or(12);
86        let x = self.0;
87
88        if width_min == 0 {
89            width_min = 1;
90        }
91
92        if width_max == 0 {
93            return Ok(())
94        }
95
96        if width_min > width_max {
97            width_max = width_min;
98        }
99
100        use NumberClass::*;
101        let mut c = self.cls();
102
103        if DEBUG { eprintln!("Number {} classified as {:?}", x, c); }
104
105        if c == Special {
106            let q = format!("{}", x);
107            return if q.len() <= width_max {
108                write!(fmt, "{:w$}", q, w=width_min)
109            } else {
110                write!(fmt, "{:.p$}", "########", p=width_max)
111            }
112        }
113        if c == Zero {
114            return if width_max < 3 || width_min < 3 {
115                write!(fmt, "{:w$}", "0", w=width_min)
116            } else {
117                write!(fmt, "{:.p$}", 0.0, p=(width_min-2))
118            };
119        }
120
121        let probe_for_medium_mode;
122        if c == Medium {
123            probe_for_medium_mode = format!("{:.0}", x);
124            let length_of_integer_part = probe_for_medium_mode.len();
125
126            if DEBUG { eprintln!(
127                "Probe=`{}`; length of integer part is {}, which width_max is {}",
128                probe_for_medium_mode,
129                length_of_integer_part,
130                width_max,
131            ); }
132
133            match length_of_integer_part {
134                l if l > width_max => {
135                    if DEBUG { eprintln!("Too large, switching to Big"); }
136                    c = Big;
137                },
138                l if l + 1 >= width_max => {
139                    if DEBUG { eprintln!("Almost too large, checking zeroness"); }
140                    if probe_for_medium_mode != "0" {
141                        if DEBUG { eprintln!("Seems to be OK to print as integer"); }
142                        // print as integer
143                        return write!(fmt, "{:w$.0}", x, w=width_min);
144                    } else {
145                        if DEBUG { eprintln!("Refusing to print it"); }
146                        c = Unprintable;
147                    }
148                },
149                _ => {
150                    // Enouch room to try fractional part
151                    // Check if it would be all zeroes
152                    if DEBUG { eprintln!("Enough room to consider fractional part"); }
153
154                    let probe = format!(
155                        "{:.p$}",
156                        x,
157                        p=(width_max - 1 - length_of_integer_part),
158                    );
159
160                    let mut num_zeroes = 0;
161                    let mut num_digits = 0;
162                    let mut significant_zeroes = false;
163
164                    for c in probe.chars() {
165                        match c {
166                            '0' => {
167                                num_digits += 1;
168                                if ! significant_zeroes {
169                                    num_zeroes += 1;
170                                }
171                            },
172                            '.' => {
173                                
174                            }
175                            '-' => {
176
177                            },
178                            _ => {
179                                num_digits += 1;
180                                significant_zeroes = true;
181                            },
182                        }
183                    }
184                    if DEBUG { eprintln!(
185                        "{} zero of {} digits in the test print",
186                        num_zeroes,
187                        num_digits,
188                    ); }
189
190                    assert!(num_digits > 0);
191
192                    if (num_zeroes * 100 / num_digits) > 80 {
193                        if DEBUG { eprintln!("Too small to print normally, switching to Small"); }
194                        // Too many zeroes, too few actual digits
195                        c = Small;
196                    }
197                },
198            }
199
200            if c == Medium {
201                if DEBUG { eprintln!("Medium mode confirmed"); }
202                // b fits max_width, but there may be opportunities to chip off zeroes
203                let mut b = format!("{:.p$}", x, p=(width_max-1-length_of_integer_part));
204                if DEBUG { eprintln!("Intermediate result: {}", &b); }
205                let first_digit_of_probe = probe_for_medium_mode.bytes().into_iter().next();
206                if first_digit_of_probe == Some(b'1') && first_digit_of_probe != b.bytes().into_iter().next() {
207                    if DEBUG { eprintln!("Looks like we have overestimated the integer part size");  }
208
209                    let b2 = format!("{:.p$}", x, p=(width_max-1-length_of_integer_part+1));
210                    if b2.len() <= width_max {
211                        b = b2;
212                    }
213                }
214                let mut end = b.len();
215                if b.contains('.') {
216                    loop {
217                        if end <= width_min { break }
218                        if end < 3 { break }
219                        if !b[0..end].ends_with('0') { break }
220                        if b[0..(end-1)].ends_with('.') { 
221                            // protect one zero after '.'
222                            break
223                        }
224                        if DEBUG { eprintln!("Chipped away some zero"); }
225                        end -= 1;
226                    }
227                }
228                let b = &b[0..end];
229                for _ in b.len()..width_min {
230                    write!(fmt, " ")?;
231                }
232                return write!(fmt, "{}", b);
233            }
234        }
235
236        match c {
237            Zero | Special | Medium => unreachable!(),
238            Big | Small => {
239                let probe = format!("{:.0e}", x);
240                if DEBUG { eprintln!("First probe: {}", &probe); }
241                let mut minimum = probe.len();
242                if minimum > width_max {
243                    if DEBUG { eprintln!("Can't fit it"); }
244                    if c == Big {
245                        c = Unprintable;
246                    } else {
247                        if DEBUG { eprintln!("Just print zero"); }
248                        return write!(fmt, "{:w$}", 0.0, w=width_min);
249                    }
250                } else if minimum == width_max {
251                    if DEBUG { eprintln!("Fits just right"); }
252                    return write!(fmt, "{}", probe);
253                } else if minimum == width_max-1 {
254                    if DEBUG { eprintln!("Fits almost just right"); }
255                    // Can't increase precision because of we need to add a `.` as well
256                    return write!(fmt, " {}", probe);
257                } else {
258                    if DEBUG { eprintln!("There is some space to be more precise"); }
259                    let probe2 = format!("{:.p$e}", x, p=(width_max - minimum - 1) );
260                    if DEBUG { eprintln!("Second probe: {}", &probe2); }
261                    if probe2.len() > width_max {
262                        minimum += probe2.len() - width_max;
263                    }
264                    let mut zeroes_before_e = 0;
265                    let mut zeroes_in_a_row = 0;
266                    for c in probe2.chars() { match c {
267                        '0' => zeroes_in_a_row += 1,
268                        'e' | 'E' => {
269                            zeroes_before_e = zeroes_in_a_row;
270                        },
271                        _ => zeroes_in_a_row = 0,
272                    } }
273                    if DEBUG { eprintln!("{} zeroes before E", zeroes_before_e); }
274                    let zeroes_to_chip_away = zeroes_before_e.min(width_max-width_min);
275                    if DEBUG { eprintln!("{} zeroes to be removed", zeroes_to_chip_away); }
276                    return write!(fmt, "{:.p$e}", x, p=(width_max - minimum - 1 - zeroes_to_chip_away) );
277                }
278            },
279            Unprintable => (),
280        }
281        let _ = c;
282
283        write!(fmt, "{:.p$}", "##################################", p=width_min)
284    }
285}
286
287#[cfg(test)]
288mod test;