//! Number and text formatting shared by the SVG writer and the scripts. use std::fmt::Write; /// Append `v` rounded to 0.1 the way the JS views did (`Math.round(v * 10) / 10`): /// halves round up, no trailing `.0`, never `-0`. Non-finite values print as /// `NaN` so the checks can find them in the output. pub fn r1(out: &mut String, v: f64) { if !v.is_finite() { out.push_str("NaN"); return; } let t = (v * 10.0 + 0.5).floor() as i64; if t < 0 { out.push('-'); } let a = t.unsigned_abs(); let _ = write!(out, "{}", a / 10); if !a.is_multiple_of(10) { let _ = write!(out, ".{}", a % 10); } } /// [`r1`] into a new string. pub fn r1s(v: f64) -> String { let mut s = String::new(); r1(&mut s, v); s } /// Round to 0.1 as a number. pub fn round1(v: f64) -> f64 { (v * 10.0 + 0.5).floor() / 10.0 } /// Escape text for SVG/HTML content and attribute values. pub fn esc(s: &str) -> String { let mut o = String::with_capacity(s.len()); for ch in s.chars() { match ch { '&' => o.push_str("&"), '<' => o.push_str("<"), '>' => o.push_str(">"), '"' => o.push_str("""), c => o.push(c), } } o } /// Deterministic pseudo-random number in [0, 1) from any number (the views' /// `hash(i)`): scenes use it to vary trees, cars and delays without changing /// from one build to the next. pub fn hash(i: f64) -> f64 { let x = (i * 12.9898 + 78.233).sin() * 43758.5453; x - x.floor() } /// Whole number with thousands separators: 12345.6 -> "12,346". pub fn thousands(n: f64) -> String { if !n.is_finite() { return "NaN".into(); } let v = (n + 0.5).floor() as i64; let s = v.unsigned_abs().to_string(); let mut o = String::new(); for (i, ch) in s.chars().enumerate() { if i > 0 && (s.len() - i).is_multiple_of(3) { o.push(','); } o.push(ch); } if v < 0 { format!("-{o}") } else { o } } #[cfg(test)] mod tests { use super::*; #[test] fn rounding_matches_js() { assert_eq!(r1s(12.34), "12.3"); assert_eq!(r1s(12.35), "12.4"); assert_eq!(r1s(12.0), "12"); assert_eq!(r1s(-0.04), "0"); assert_eq!(r1s(-2.25), "-2.2"); // JS Math.round(-22.5) = -22 assert_eq!(r1s(-7.0), "-7"); assert_eq!(r1s(f64::NAN), "NaN"); } #[test] fn thousands_sep() { assert_eq!(thousands(1234567.4), "1,234,567"); assert_eq!(thousands(999.0), "999"); assert_eq!(thousands(-1500.0), "-1,500"); } #[test] fn hash_is_stable() { let a = hash(3.0); assert!((0.0..1.0).contains(&a)); assert_eq!(a, hash(3.0)); assert_ne!(a, hash(4.0)); } }