ferrovue-core 0.8.0

The primitives ferrovue's runtime crates share: Vue's HTML escaping, and numbers written as JavaScript writes them
Documentation
use super::*;

#[test]
fn numbers_are_written_as_javascript_writes_them() {
    let vectors: Vec<(String, String)> =
        serde_json::from_str(include_str!("../../tests/vectors/numbers.json"))
            .expect("number vectors");
    assert!(vectors.len() >= 300, "the vectors were not all read");
    for (input, want) in &vectors {
        let x: f64 = input.parse().unwrap_or_else(|_| panic!("{input:?} parses"));
        let mut out = String::new();
        push_number(&mut out, x);
        assert_eq!(&out, want, "String({input})");
    }
}

#[test]
fn math_is_javascripts_math() {
    let vectors: Vec<(String, String, String, String)> =
        serde_json::from_str(include_str!("../../tests/vectors/math.json")).expect("math vectors");
    assert!(vectors.len() >= 600, "the vectors were not all read");
    let num = |s: &str| -> f64 { s.parse().unwrap_or_else(|_| panic!("{s:?} parses")) };
    let text = |x: f64| {
        if x == 0.0 && x.is_sign_negative() {
            return "-0".to_owned();
        }
        let mut out = String::new();
        push_number(&mut out, x);
        out
    };
    for (op, x, arg, want) in &vectors {
        let got = match op.as_str() {
            "round" => text(js_round(num(x))),
            "toFixed" => js_to_fixed(num(x), arg.parse().unwrap()),
            "max" => text(js_max(num(x), num(arg))),
            "min" => text(js_min(num(x), num(arg))),
            other => panic!("unknown op {other}"),
        };
        assert_eq!(&got, want, "{op}({x}, {arg})");
    }
}

#[test]
fn integers_beyond_two_to_the_53_are_written_rounded_as_javascript_does() {
    for (n, want) in [
        (9_007_199_254_740_991, "9007199254740991"),
        (9_007_199_254_740_993, "9007199254740992"),
        (-9_007_199_254_740_993, "-9007199254740992"),
        (i64::MIN, "-9223372036854776000"),
    ] {
        let mut out = String::new();
        push_int(&mut out, n);
        assert_eq!(out, want, "{n}");
        assert_eq!(Js(n).to_string(), want);
    }
}

#[test]
fn integers_are_written_as_javascript_writes_them() {
    for (n, want) in [
        (0, "0"),
        (-1, "-1"),
        (42, "42"),
        (i64::MAX, "9223372036854776000"),
    ] {
        let mut out = String::new();
        push_int(&mut out, n);
        assert_eq!(out, want);
    }
}

#[test]
fn a_number_shows_in_debug() {
    assert_eq!(format!("{:?}", Js(2.5_f64)), "Js(2.5)");
}

fn push_number_exactly(out: &mut String, x: f64) {
    use std::fmt::Write;
    if x.is_nan() {
        out.push_str("NaN");
        return;
    }
    if x == 0.0 {
        out.push('0');
        return;
    }
    if x.is_infinite() {
        out.push_str(if x < 0.0 { "-Infinity" } else { "Infinity" });
        return;
    }
    if x < 0.0 {
        out.push('-');
    }
    let mut sci = String::new();
    let _ = write!(sci, "{:e}", x.abs());
    let (mantissa, exp) = sci.split_once('e').expect("`{:e}` writes an exponent");
    let mut digits: String = mantissa.chars().filter(|c| *c != '.').collect();
    let mut exact = String::new();
    let _ = write!(exact, "{:.1100e}", x.abs());
    if let Some((exact_mantissa, exact_exp)) = exact.split_once('e')
        && exact_exp == exp
    {
        let exact_digits: String = exact_mantissa.chars().filter(|c| *c != '.').collect();
        let exact_digits = exact_digits.trim_end_matches('0');
        let k = digits.len();
        if exact_digits.len() == k + 1 && exact_digits.ends_with('5') {
            let lower = &exact_digits[..k];
            let upper = increment_digits(lower);
            let even = |d: &str| {
                d.bytes()
                    .last()
                    .is_some_and(|b| (b - b'0').is_multiple_of(2))
            };
            if upper.len() == k && (digits == lower || digits == upper) {
                digits = if even(lower) { lower.to_owned() } else { upper };
            }
        }
    }
    let k = digits.len() as i32;
    let n = exp
        .parse::<i32>()
        .expect("`{:e}` writes an integer exponent")
        + 1;
    if k <= n && n <= 21 {
        out.push_str(&digits);
        out.extend(std::iter::repeat_n('0', (n - k) as usize));
    } else if 0 < n && n <= 21 {
        out.push_str(&digits[..n as usize]);
        out.push('.');
        out.push_str(&digits[n as usize..]);
    } else if -6 < n && n <= 0 {
        out.push_str("0.");
        out.extend(std::iter::repeat_n('0', (-n) as usize));
        out.push_str(&digits);
    } else {
        out.push_str(&digits[..1]);
        if k > 1 {
            out.push('.');
            out.push_str(&digits[1..]);
        }
        let e = n - 1;
        let _ = write!(out, "e{}{}", if e < 0 { '-' } else { '+' }, e.abs());
    }
}

fn written(push: fn(&mut String, f64), x: f64) -> String {
    let mut out = String::new();
    push(&mut out, x);
    out
}

fn owned_exact_digits(x: f64) -> Option<(String, i32)> {
    few_exact_digits(x).map(|(digits, exp)| (digits.as_str().to_owned(), exp))
}

fn few_exact_digits_exactly(x: f64) -> Option<(String, i32)> {
    let exact = format!("{x:.1100e}");
    let (mantissa, exp) = exact.split_once('e').expect("`{:e}` writes an exponent");
    let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
    let digits = digits.trim_end_matches('0');
    let exp = exp.parse().expect("`{:e}` writes an integer exponent");
    (digits.len() <= 18).then(|| (digits.to_owned(), exp))
}

#[test]
fn few_exact_digits_finds_eighteen_digits_in_two_to_the_59_times_ten_to_the_22() {
    let x = 2f64.powi(59) * 1e22;
    assert_eq!(
        owned_exact_digits(x),
        Some(("576460752303423488".to_owned(), 39))
    );
    assert_eq!(owned_exact_digits(x), few_exact_digits_exactly(x));
}

proptest::proptest! {
    #![proptest_config(proptest::prelude::ProptestConfig::with_cases(20_000))]

    #[test]
    fn a_number_is_written_as_the_exact_reference_writes_it(bits in proptest::prelude::any::<u64>()) {
        let x = f64::from_bits(bits);
        proptest::prop_assert_eq!(written(push_number, x), written(push_number_exactly, x));
    }

    #[test]
    fn a_short_fraction_is_written_as_the_exact_reference_writes_it(m in 1u64..1 << 53, n in 0i32..64, negative in proptest::prelude::any::<bool>()) {
        let x = m as f64 * 2f64.powi(-n) * if negative { -1.0 } else { 1.0 };
        proptest::prop_assert_eq!(written(push_number, x), written(push_number_exactly, x));
    }

    #[test]
    fn a_large_whole_number_is_written_as_the_exact_reference_writes_it(m in 1u64..1 << 53, e in 0i32..100) {
        let x = m as f64 * 2f64.powi(e);
        proptest::prop_assert_eq!(written(push_number, x), written(push_number_exactly, x));
    }

    #[test]
    fn a_decimal_is_written_as_the_exact_reference_writes_it(digits in 0u64..100_000_000_000_000_000, point in -30i32..30) {
        let x: f64 = format!("{digits}e{point}").parse().expect("a decimal parses");
        proptest::prop_assert_eq!(written(push_number, x), written(push_number_exactly, x));
    }

    #[test]
    fn few_exact_digits_are_the_exact_expansion_when_it_is_short(m in 1u64..1 << 53, e in -60i32..100) {
        let x = m as f64 * 2f64.powi(e);
        proptest::prop_assert_eq!(owned_exact_digits(x), few_exact_digits_exactly(x));
    }

    #[test]
    fn an_integer_is_written_as_the_number_it_is(n in proptest::prelude::any::<i64>()) {
        let mut out = String::new();
        push_int(&mut out, n);
        proptest::prop_assert_eq!(&out, &written(push_number_exactly, n as f64));
        proptest::prop_assert_eq!(&Js(n).to_string(), &out);
        if n.unsigned_abs() <= MAX_SAFE_INTEGER {
            proptest::prop_assert_eq!(&out, &n.to_string());
        }
    }
}