Skip to main content

serpent_serializer/
numfmt.rs

1//! C `printf` number formatting for `%g` and `%e` conversions.
2//!
3//! Lua formats numbers through C `printf`. Rust's own float formatting differs,
4//! so this module reproduces the two conversions serpent relies on: `%g` (the
5//! default `%.17g`) and `%e`. Only the pieces serpent uses are implemented:
6//! precision, the `%g`/`%e` conversions, and an optional field width for the
7//! integer part of `%e`.
8
9/// A parsed `printf`-style number format such as `%.17g` or `%1.16e`.
10struct Spec {
11    precision: usize,
12    conv: Conv,
13    /// True for the uppercase conversions `%G`/`%E`. Uppercase emits `E` in the
14    /// exponent and `INF`/`NAN` for non-finite values.
15    upper: bool,
16}
17
18enum Conv {
19    G,
20    E,
21}
22
23/// Parse the supported subset of a `printf` format string.
24///
25/// Returns `None` for formats outside the subset, which the caller treats as a
26/// fallback to the default `%.17g`.
27fn parse(fmt: &str) -> Option<Spec> {
28    let b = fmt.as_bytes();
29    if b.first() != Some(&b'%') {
30        return None;
31    }
32    let mut i = 1;
33    // Skip an optional width. Serpent uses formats like "%1.16e"; the width does
34    // not change output for these values, so it is parsed and ignored.
35    while i < b.len() && b[i].is_ascii_digit() {
36        i += 1;
37    }
38    let mut precision = 6;
39    if i < b.len() && b[i] == b'.' {
40        i += 1;
41        let start = i;
42        while i < b.len() && b[i].is_ascii_digit() {
43            i += 1;
44        }
45        precision = fmt[start..i].parse().unwrap_or(0);
46    }
47    let (conv, upper) = match b.get(i) {
48        Some(&b'g') => (Conv::G, false),
49        Some(&b'G') => (Conv::G, true),
50        Some(&b'e') => (Conv::E, false),
51        Some(&b'E') => (Conv::E, true),
52        _ => return None,
53    };
54    if i + 1 != b.len() {
55        return None;
56    }
57    Some(Spec {
58        precision,
59        conv,
60        upper,
61    })
62}
63
64/// Format `x` with the given `printf` format string.
65///
66/// Unsupported formats fall back to `%.17g`. Non-finite values follow C
67/// `printf`, which prints `inf`, `-inf`, and `nan`. Serpent intercepts these
68/// before formatting unless `nohuge` is set, in which case this path applies.
69#[must_use]
70pub fn format(fmt: &str, x: f64) -> String {
71    let spec = parse(fmt).unwrap_or(Spec {
72        precision: 17,
73        conv: Conv::G,
74        upper: false,
75    });
76    if x.is_nan() {
77        return if spec.upper { "NAN" } else { "nan" }.to_string();
78    }
79    if x.is_infinite() {
80        let word = if spec.upper { "INF" } else { "inf" };
81        return if x > 0.0 {
82            word.to_string()
83        } else {
84            format!("-{word}")
85        };
86    }
87    let out = match spec.conv {
88        Conv::G => fmt_g(x, spec.precision),
89        Conv::E => fmt_e(x, spec.precision),
90    };
91    if spec.upper {
92        out.to_uppercase()
93    } else {
94        out
95    }
96}
97
98/// C `%e`: one digit before the point, `precision` after, `e[+-]dd` exponent
99/// with at least two exponent digits.
100fn fmt_e(x: f64, precision: usize) -> String {
101    if x == 0.0 {
102        let mantissa = if precision == 0 {
103            "0".to_string()
104        } else {
105            format!("0.{}", "0".repeat(precision))
106        };
107        let sign = if x.is_sign_negative() { "-" } else { "" };
108        return format!("{sign}{mantissa}e+00");
109    }
110    let neg = x < 0.0;
111    let (digits, exp) = round_digits(x.abs(), precision + 1);
112    let mut out = String::new();
113    if neg {
114        out.push('-');
115    }
116    out.push(digits.as_bytes()[0] as char);
117    if precision > 0 {
118        out.push('.');
119        out.push_str(&digits[1..=precision]);
120    }
121    out.push('e');
122    if exp >= 0 {
123        out.push('+');
124    } else {
125        out.push('-');
126    }
127    let e = exp.abs();
128    if e < 10 {
129        out.push('0');
130    }
131    out.push_str(&e.to_string());
132    out
133}
134
135/// C `%g`: shortest of `%e` and `%f` with `precision` significant digits and
136/// trailing zeros stripped.
137fn fmt_g(x: f64, precision: usize) -> String {
138    let p = if precision == 0 { 1 } else { precision };
139    if x == 0.0 {
140        return if x.is_sign_negative() {
141            "-0".to_string()
142        } else {
143            "0".to_string()
144        };
145    }
146    let neg = x < 0.0;
147    let (digits, exp) = round_digits(x.abs(), p);
148    // C rule: use %e when exponent < -4 or exponent >= precision, else %f.
149    let mut out = String::new();
150    if neg {
151        out.push('-');
152    }
153    if exp < -4 || exp >= p as i32 {
154        // Exponential form with trailing zeros stripped.
155        let mut mant = String::new();
156        mant.push(digits.as_bytes()[0] as char);
157        let frac = strip_zeros(&digits[1..]);
158        if !frac.is_empty() {
159            mant.push('.');
160            mant.push_str(&frac);
161        }
162        out.push_str(&mant);
163        out.push('e');
164        if exp >= 0 {
165            out.push('+');
166        } else {
167            out.push('-');
168        }
169        let e = exp.abs();
170        if e < 10 {
171            out.push('0');
172        }
173        out.push_str(&e.to_string());
174    } else if exp >= 0 {
175        // Fixed form, decimal point after exp+1 digits.
176        let int_len = (exp + 1) as usize;
177        if digits.len() <= int_len {
178            let mut s = digits.clone();
179            s.push_str(&"0".repeat(int_len - digits.len()));
180            out.push_str(&s);
181        } else {
182            let (int_part, frac_part) = digits.split_at(int_len);
183            let frac = strip_zeros(frac_part);
184            out.push_str(int_part);
185            if !frac.is_empty() {
186                out.push('.');
187                out.push_str(&frac);
188            }
189        }
190    } else {
191        // 0.000ddd form: exp is negative, leading zeros before significant digits.
192        let zeros = (-exp - 1) as usize;
193        let frac = strip_zeros(&format!("{}{}", "0".repeat(zeros), digits));
194        out.push_str("0.");
195        out.push_str(&frac);
196    }
197    out
198}
199
200fn strip_zeros(s: &str) -> String {
201    s.trim_end_matches('0').to_string()
202}
203
204/// Round `x` (positive, finite, nonzero) to `sig` significant decimal digits.
205///
206/// Returns the digit string of length `sig` and the decimal exponent, so the
207/// value is `d[0].d[1..] * 10^exp`. Rust's `{:.*e}` produces a correctly
208/// rounded decimal at a fixed precision, which supplies the digits.
209fn round_digits(x: f64, sig: usize) -> (String, i32) {
210    // `{:.p$e}` yields "d.ddde[-]dd" with exactly p+1 significant digits,
211    // correctly rounded. Request sig significant digits.
212    let s = format!("{x:.*e}", sig - 1);
213    let (mant, exp_str) = s.split_once('e').expect("scientific form has e");
214    let exp: i32 = exp_str.parse().expect("exponent parses");
215    let mut digits: Vec<u8> = mant.bytes().filter(|b| b.is_ascii_digit()).collect();
216    while digits.len() < sig {
217        digits.push(b'0');
218    }
219    digits.truncate(sig);
220    (String::from_utf8(digits).expect("ascii digits"), exp)
221}