serpent-serializer 0.2.0

Serialize Lua values to round-trippable Lua source with cycle and shared-reference handling
Documentation
//! C `printf` number formatting for `%g` and `%e` conversions.
//!
//! Lua formats numbers through C `printf`. Rust's own float formatting differs,
//! so this module reproduces the two conversions serpent relies on: `%g` (the
//! default `%.17g`) and `%e`. Only the pieces serpent uses are implemented:
//! precision, the `%g`/`%e` conversions, and an optional field width for the
//! integer part of `%e`.

/// A parsed `printf`-style number format such as `%.17g` or `%1.16e`.
struct Spec {
    precision: usize,
    conv: Conv,
    /// True for the uppercase conversions `%G`/`%E`. Uppercase emits `E` in the
    /// exponent and `INF`/`NAN` for non-finite values.
    upper: bool,
}

enum Conv {
    G,
    E,
}

/// Parse the supported subset of a `printf` format string.
///
/// Returns `None` for formats outside the subset, which the caller treats as a
/// fallback to the default `%.17g`.
fn parse(fmt: &str) -> Option<Spec> {
    let b = fmt.as_bytes();
    if b.first() != Some(&b'%') {
        return None;
    }
    let mut i = 1;
    // Skip an optional width. Serpent uses formats like "%1.16e"; the width does
    // not change output for these values, so it is parsed and ignored.
    while i < b.len() && b[i].is_ascii_digit() {
        i += 1;
    }
    let mut precision = 6;
    if i < b.len() && b[i] == b'.' {
        i += 1;
        let start = i;
        while i < b.len() && b[i].is_ascii_digit() {
            i += 1;
        }
        precision = fmt[start..i].parse().unwrap_or(0);
    }
    let (conv, upper) = match b.get(i) {
        Some(&b'g') => (Conv::G, false),
        Some(&b'G') => (Conv::G, true),
        Some(&b'e') => (Conv::E, false),
        Some(&b'E') => (Conv::E, true),
        _ => return None,
    };
    if i + 1 != b.len() {
        return None;
    }
    Some(Spec {
        precision,
        conv,
        upper,
    })
}

/// Format `x` with the given `printf` format string.
///
/// Unsupported formats fall back to `%.17g`. Non-finite values follow C
/// `printf`, which prints `inf`, `-inf`, and `nan`. Serpent intercepts these
/// before formatting unless `nohuge` is set, in which case this path applies.
#[must_use]
pub fn format(fmt: &str, x: f64) -> String {
    let spec = parse(fmt).unwrap_or(Spec {
        precision: 17,
        conv: Conv::G,
        upper: false,
    });
    if x.is_nan() {
        return if spec.upper { "NAN" } else { "nan" }.to_string();
    }
    if x.is_infinite() {
        let word = if spec.upper { "INF" } else { "inf" };
        return if x > 0.0 {
            word.to_string()
        } else {
            format!("-{word}")
        };
    }
    let out = match spec.conv {
        Conv::G => fmt_g(x, spec.precision),
        Conv::E => fmt_e(x, spec.precision),
    };
    if spec.upper {
        out.to_uppercase()
    } else {
        out
    }
}

/// C `%e`: one digit before the point, `precision` after, `e[+-]dd` exponent
/// with at least two exponent digits.
fn fmt_e(x: f64, precision: usize) -> String {
    if x == 0.0 {
        let mantissa = if precision == 0 {
            "0".to_string()
        } else {
            format!("0.{}", "0".repeat(precision))
        };
        let sign = if x.is_sign_negative() { "-" } else { "" };
        return format!("{sign}{mantissa}e+00");
    }
    let neg = x < 0.0;
    let (digits, exp) = round_digits(x.abs(), precision + 1);
    let mut out = String::new();
    if neg {
        out.push('-');
    }
    out.push(digits.as_bytes()[0] as char);
    if precision > 0 {
        out.push('.');
        out.push_str(&digits[1..=precision]);
    }
    out.push('e');
    if exp >= 0 {
        out.push('+');
    } else {
        out.push('-');
    }
    let e = exp.abs();
    if e < 10 {
        out.push('0');
    }
    out.push_str(&e.to_string());
    out
}

/// C `%g`: shortest of `%e` and `%f` with `precision` significant digits and
/// trailing zeros stripped.
fn fmt_g(x: f64, precision: usize) -> String {
    let p = if precision == 0 { 1 } else { precision };
    if x == 0.0 {
        return if x.is_sign_negative() {
            "-0".to_string()
        } else {
            "0".to_string()
        };
    }
    let neg = x < 0.0;
    let (digits, exp) = round_digits(x.abs(), p);
    // C rule: use %e when exponent < -4 or exponent >= precision, else %f.
    let mut out = String::new();
    if neg {
        out.push('-');
    }
    if exp < -4 || exp >= p as i32 {
        // Exponential form with trailing zeros stripped.
        let mut mant = String::new();
        mant.push(digits.as_bytes()[0] as char);
        let frac = strip_zeros(&digits[1..]);
        if !frac.is_empty() {
            mant.push('.');
            mant.push_str(&frac);
        }
        out.push_str(&mant);
        out.push('e');
        if exp >= 0 {
            out.push('+');
        } else {
            out.push('-');
        }
        let e = exp.abs();
        if e < 10 {
            out.push('0');
        }
        out.push_str(&e.to_string());
    } else if exp >= 0 {
        // Fixed form, decimal point after exp+1 digits.
        let int_len = (exp + 1) as usize;
        if digits.len() <= int_len {
            let mut s = digits.clone();
            s.push_str(&"0".repeat(int_len - digits.len()));
            out.push_str(&s);
        } else {
            let (int_part, frac_part) = digits.split_at(int_len);
            let frac = strip_zeros(frac_part);
            out.push_str(int_part);
            if !frac.is_empty() {
                out.push('.');
                out.push_str(&frac);
            }
        }
    } else {
        // 0.000ddd form: exp is negative, leading zeros before significant digits.
        let zeros = (-exp - 1) as usize;
        let frac = strip_zeros(&format!("{}{}", "0".repeat(zeros), digits));
        out.push_str("0.");
        out.push_str(&frac);
    }
    out
}

fn strip_zeros(s: &str) -> String {
    s.trim_end_matches('0').to_string()
}

/// Round `x` (positive, finite, nonzero) to `sig` significant decimal digits.
///
/// Returns the digit string of length `sig` and the decimal exponent, so the
/// value is `d[0].d[1..] * 10^exp`. Rust's `{:.*e}` produces a correctly
/// rounded decimal at a fixed precision, which supplies the digits.
fn round_digits(x: f64, sig: usize) -> (String, i32) {
    // `{:.p$e}` yields "d.ddde[-]dd" with exactly p+1 significant digits,
    // correctly rounded. Request sig significant digits.
    let s = format!("{x:.*e}", sig - 1);
    let (mant, exp_str) = s.split_once('e').expect("scientific form has e");
    let exp: i32 = exp_str.parse().expect("exponent parses");
    let mut digits: Vec<u8> = mant.bytes().filter(|b| b.is_ascii_digit()).collect();
    while digits.len() < sig {
        digits.push(b'0');
    }
    digits.truncate(sig);
    (String::from_utf8(digits).expect("ascii digits"), exp)
}