110 lines
2.8 KiB
Rust
110 lines
2.8 KiB
Rust
//! 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));
|
|
}
|
|
}
|