Skip to main content

ferrovue_core/
numbers.rs

1const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
2
3const DIGIT_PAIRS: &[u8; 200] = b"\
4    0001020304050607080910111213141516171819\
5    2021222324252627282930313233343536373839\
6    4041424344454647484950515253545556575859\
7    6061626364656667686970717273747576777879\
8    8081828384858687888990919293949596979899";
9
10fn decimal(buf: &mut [u8; 20], mut n: u64) -> usize {
11    let mut i = buf.len();
12    while n >= 100 {
13        let pair = (n % 100) as usize * 2;
14        n /= 100;
15        i -= 2;
16        buf[i..i + 2].copy_from_slice(&DIGIT_PAIRS[pair..pair + 2]);
17    }
18    if n >= 10 {
19        let pair = n as usize * 2;
20        i -= 2;
21        buf[i..i + 2].copy_from_slice(&DIGIT_PAIRS[pair..pair + 2]);
22    } else {
23        i -= 1;
24        buf[i] = b'0' + n as u8;
25    }
26    i
27}
28
29fn push_decimal(out: &mut String, n: u64) {
30    if n < 10 {
31        out.push(char::from(b'0' + n as u8));
32        return;
33    }
34    let mut buf = [0; 20];
35    let start = decimal(&mut buf, n);
36    out.reserve(buf.len() - start);
37    for &b in &buf[start..] {
38        out.push(char::from(b));
39    }
40}
41
42/// `String(n)` for an integer, which is what `toDisplayString` and `escapeHtml` make of a number.
43///
44/// Exact within ±(2⁵³ − 1); beyond that it is written as JavaScript writes the rounded number.
45///
46/// # Example
47///
48/// ```
49/// # use ferrovue_core as ferrovue;
50/// let mut out = String::new();
51/// ferrovue::push_int(&mut out, 42);
52/// out.push(' ');
53/// // Beyond 2⁵³ the browser has the rounded number, so that is what is written.
54/// ferrovue::push_int(&mut out, 9_007_199_254_740_993);
55/// assert_eq!(out, "42 9007199254740992");
56/// ```
57pub fn push_int(out: &mut String, n: i64) {
58    if n.unsigned_abs() <= MAX_SAFE_INTEGER {
59        if n < 0 {
60            out.push('-');
61        }
62        push_decimal(out, n.unsigned_abs());
63    } else {
64        push_number(out, n as f64);
65    }
66}
67
68/// ASCII text on the stack, long enough for any `u128` in decimal: what `push_number` writes and
69/// reads digits in, so writing a fraction allocates nothing.
70struct Short {
71    buf: [u8; 48],
72    len: usize,
73}
74
75impl Short {
76    const fn new() -> Short {
77        Short {
78            buf: [0; 48],
79            len: 0,
80        }
81    }
82
83    fn of(digits: &str) -> Short {
84        let mut short = Short::new();
85        short.extend(digits.bytes());
86        short
87    }
88
89    fn extend(&mut self, bytes: impl Iterator<Item = u8>) {
90        for b in bytes {
91            self.buf[self.len] = b;
92            self.len += 1;
93        }
94    }
95
96    fn as_str(&self) -> &str {
97        std::str::from_utf8(&self.buf[..self.len]).expect("only ASCII is written")
98    }
99}
100
101impl std::fmt::Write for Short {
102    fn write_str(&mut self, s: &str) -> std::fmt::Result {
103        let end = self.len + s.len();
104        self.buf
105            .get_mut(self.len..end)
106            .ok_or(std::fmt::Error)?
107            .copy_from_slice(s.as_bytes());
108        self.len = end;
109        Ok(())
110    }
111}
112
113/// `Number.prototype.toString()`: JavaScript's shortest round-trip digits, laid out as ECMAScript
114/// lays them out: `0.30000000000000004`, `1e+21`, `1.5e-7`, `NaN`, `-Infinity`.
115///
116/// # Example
117///
118/// ```
119/// # use ferrovue_core as ferrovue;
120/// let written = |x: f64| {
121///     let mut out = String::new();
122///     ferrovue::push_number(&mut out, x);
123///     out
124/// };
125/// assert_eq!(written(0.1 + 0.2), "0.30000000000000004");
126/// assert_eq!(written(1e21), "1e+21");
127/// assert_eq!(written(-0.0), "0");
128/// assert_eq!(written(f64::NAN), "NaN");
129/// ```
130pub fn push_number(out: &mut String, x: f64) {
131    use std::fmt::Write;
132    if x.is_nan() {
133        out.push_str("NaN");
134        return;
135    }
136    if x == 0.0 {
137        out.push('0');
138        return;
139    }
140    if x.is_infinite() {
141        out.push_str(if x < 0.0 { "-Infinity" } else { "Infinity" });
142        return;
143    }
144    if x < 0.0 {
145        out.push('-');
146    }
147    let x = x.abs();
148    if x.fract() == 0.0 && x <= MAX_SAFE_INTEGER as f64 {
149        push_decimal(out, x as u64);
150        return;
151    }
152    let (digits, exp) = shortest_digits(x);
153    let digits = digits.as_str();
154    let k = digits.len() as i32;
155    let n = exp + 1;
156    if k <= n && n <= 21 {
157        out.push_str(digits);
158        out.extend(std::iter::repeat_n('0', (n - k) as usize));
159    } else if 0 < n && n <= 21 {
160        out.push_str(&digits[..n as usize]);
161        out.push('.');
162        out.push_str(&digits[n as usize..]);
163    } else if -6 < n && n <= 0 {
164        out.push_str("0.");
165        out.extend(std::iter::repeat_n('0', (-n) as usize));
166        out.push_str(digits);
167    } else {
168        out.push_str(&digits[..1]);
169        if k > 1 {
170            out.push('.');
171            out.push_str(&digits[1..]);
172        }
173        let e = n - 1;
174        let _ = write!(out, "e{}{}", if e < 0 { '-' } else { '+' }, e.abs());
175    }
176}
177
178fn shortest_digits(x: f64) -> (Short, i32) {
179    use std::fmt::Write;
180    let mut sci = Short::new();
181    let _ = write!(sci, "{x:e}");
182    let (mantissa, exp) = sci
183        .as_str()
184        .split_once('e')
185        .expect("`{:e}` writes an exponent");
186    let exp: i32 = exp.parse().expect("`{:e}` writes an integer exponent");
187    let mut digits = Short::new();
188    digits.extend(mantissa.bytes().filter(|b| *b != b'.'));
189    if let Some((exact, exact_exp)) = few_exact_digits(x)
190        && exact_exp == exp
191    {
192        let exact = exact.as_str();
193        let k = digits.len;
194        if exact.len() == k + 1 && exact.ends_with('5') {
195            let lower = &exact[..k];
196            let upper = increment_digits(lower);
197            let even = |d: &str| {
198                d.bytes()
199                    .last()
200                    .is_some_and(|b| (b - b'0').is_multiple_of(2))
201            };
202            if upper.len() == k && (digits.as_str() == lower || digits.as_str() == upper) {
203                digits = Short::of(if even(lower) { lower } else { &upper });
204            }
205        }
206    }
207    (digits, exp)
208}
209
210fn few_exact_digits(x: f64) -> Option<(Short, i32)> {
211    use std::fmt::Write;
212    let bits = x.to_bits();
213    let biased = ((bits >> 52) & 0x7ff) as i32;
214    let fraction = bits & ((1 << 52) - 1);
215    let (m, e) = if biased == 0 {
216        (fraction, -1074)
217    } else {
218        (fraction | 1 << 52, biased - 1075)
219    };
220    let shift = m.trailing_zeros();
221    let (m, e) = (m >> shift, e + shift as i32);
222    let (value, scale) = if e < 0 {
223        if -e >= 26 {
224            return None;
225        }
226        (u128::from(m) * 5u128.pow(e.unsigned_abs()), e)
227    } else if e <= 74 {
228        (u128::from(m) << e, 0)
229    } else if e < 82 {
230        let mut s = String::new();
231        let _ = write!(s, "{x:.40e}");
232        let (mantissa, exp) = s.split_once('e')?;
233        let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
234        let digits = digits.trim_end_matches('0');
235        let exp = exp.parse().ok()?;
236        return (digits.len() <= 18).then(|| (Short::of(digits), exp));
237    } else {
238        return None;
239    };
240    let mut digits = Short::new();
241    let _ = write!(digits, "{value}");
242    let all = digits.len;
243    digits.len = digits.as_str().trim_end_matches('0').len();
244    (digits.len <= 18).then_some((digits, all as i32 - 1 + scale))
245}
246
247/// `Math.round`: the nearest integer, a half rounding up toward +∞ (`-2.5` to `-2`, where Rust's
248/// `f64::round` gives `-3`), and `-0` from `-0.5` up to zero, which `1 / Math.round(x)` shows.
249///
250/// # Example
251///
252/// ```
253/// # use ferrovue_core as ferrovue;
254/// use ferrovue::js_round;
255///
256/// assert_eq!(js_round(2.5), 3.0);
257/// assert_eq!(js_round(-2.5), -2.0); // `f64::round` gives -3
258/// assert_eq!(js_round(-2.6), -3.0);
259/// assert!(js_round(-0.2).is_sign_negative()); // -0, as JavaScript gives
260/// assert!(js_round(f64::NAN).is_nan());
261/// ```
262#[must_use]
263pub fn js_round(x: f64) -> f64 {
264    let f = x.floor();
265    let r = if x - f >= 0.5 { f + 1.0 } else { f };
266    if r == 0.0 && x.is_sign_negative() {
267        -0.0
268    } else {
269        r
270    }
271}
272
273/// `Math.sign`: 1 or -1 by the sign of a number other than zero, and `NaN`, `0` and `-0` as they
274/// are, where Rust's `f64::signum` gives 1 for `0`.
275///
276/// # Example
277///
278/// ```
279/// # use ferrovue_core as ferrovue;
280/// use ferrovue::js_sign;
281///
282/// assert_eq!(js_sign(-7.5), -1.0);
283/// assert_eq!(js_sign(1e-300), 1.0);
284/// assert!(js_sign(-0.0).is_sign_negative()); // -0, as JavaScript gives
285/// assert_eq!(js_sign(0.0), 0.0); // `f64::signum` gives 1
286/// assert!(js_sign(f64::NAN).is_nan());
287/// ```
288#[must_use]
289pub fn js_sign(x: f64) -> f64 {
290    if x > 0.0 {
291        1.0
292    } else if x < 0.0 {
293        -1.0
294    } else {
295        x
296    }
297}
298
299/// `Math.max` of two numbers: `NaN` if either is, where Rust's `f64::max` ignores a `NaN`.
300///
301/// `+0` is larger than `-0`, as in JavaScript.
302///
303/// # Example
304///
305/// ```
306/// # use ferrovue_core as ferrovue;
307/// use ferrovue::js_max;
308///
309/// assert_eq!(js_max(2.0, 3.5), 3.5);
310/// assert!(js_max(1.0, f64::NAN).is_nan()); // `f64::max` gives 1
311/// assert!(js_max(-0.0, 0.0).is_sign_positive());
312/// ```
313#[must_use]
314pub fn js_max(a: f64, b: f64) -> f64 {
315    if a.is_nan() || b.is_nan() {
316        f64::NAN
317    } else if a > b || (a == b && b.is_sign_negative()) {
318        a
319    } else {
320        b
321    }
322}
323
324/// `Math.min` of two numbers: `NaN` if either is, where Rust's `f64::min` ignores a `NaN`.
325///
326/// `-0` is smaller than `+0`, as in JavaScript.
327///
328/// # Example
329///
330/// ```
331/// # use ferrovue_core as ferrovue;
332/// use ferrovue::js_min;
333///
334/// assert_eq!(js_min(2.0, 3.5), 2.0);
335/// assert!(js_min(1.0, f64::NAN).is_nan()); // `f64::min` gives 1
336/// assert!(js_min(0.0, -0.0).is_sign_negative());
337/// ```
338#[must_use]
339pub fn js_min(a: f64, b: f64) -> f64 {
340    if a.is_nan() || b.is_nan() {
341        f64::NAN
342    } else if a < b || (a == b && a.is_sign_negative()) {
343        a
344    } else {
345        b
346    }
347}
348
349/// `Number.prototype.toFixed(digits)`: the number rounded to `digits` places, from its exact binary
350/// value. An exact tie rounds away from zero, where Rust's formatting rounds it to even; at 10²¹ and
351/// beyond, JavaScript writes the number as `String(x)` does.
352///
353/// # Example
354///
355/// ```
356/// # use ferrovue_core as ferrovue;
357/// use ferrovue::js_to_fixed;
358///
359/// assert_eq!(js_to_fixed(2.5, 0), "3"); // an exact tie, away from zero; Rust's `{:.0}` gives "2"
360/// assert_eq!(js_to_fixed(1.005, 2), "1.00"); // 1.005 is a little less as a double
361/// assert_eq!(js_to_fixed(-0.04, 1), "-0.0");
362/// assert_eq!(js_to_fixed(1e21, 2), "1e+21");
363/// ```
364#[must_use]
365pub fn js_to_fixed(x: f64, digits: u32) -> String {
366    if x.is_nan() {
367        return "NaN".to_owned();
368    }
369    if x.abs() >= 1e21 || x.is_infinite() {
370        let mut s = String::new();
371        push_number(&mut s, x);
372        return s;
373    }
374    let d = digits as usize;
375    let (exact, exp) = exact_digits(x);
376    let scaled = match usize::try_from(exp + 1 + digits as i32) {
377        Ok(0) => if exact.as_bytes()[0] >= b'5' {
378            "1"
379        } else {
380            "0"
381        }
382        .to_owned(),
383        Ok(keep) => rounded_half_up(&exact, keep),
384        Err(_) => "0".to_owned(),
385    };
386    let mut out = String::with_capacity(d + 3);
387    if x < 0.0 {
388        out.push('-');
389    }
390    out.extend(std::iter::repeat_n(
391        '0',
392        (d + 1).saturating_sub(scaled.len()),
393    ));
394    out.push_str(&scaled);
395    if d > 0 {
396        out.insert(out.len() - d, '.');
397    }
398    out
399}
400
401/// `Number.prototype.toPrecision(precision)`: the number rounded to `precision` significant
402/// digits, from its exact binary value, an exact tie rounding away from zero. It is written with
403/// an exponent (`1.2e+5`, `1.2e-7`) when the exponent is below -6 or at least `precision`, and
404/// without one otherwise; `NaN` and the infinities are written as `String(x)` writes them.
405/// `toPrecision()` without an argument is `String(x)`, which [`push_number`] writes.
406///
407/// # Panics
408///
409/// When `precision` is 0 or above 100 and `x` is finite, where JavaScript throws a `RangeError`.
410///
411/// # Example
412///
413/// ```
414/// # use ferrovue_core as ferrovue;
415/// use ferrovue::js_to_precision;
416///
417/// assert_eq!(js_to_precision(123.456, 4), "123.5");
418/// assert_eq!(js_to_precision(123_456.0, 2), "1.2e+5");
419/// assert_eq!(js_to_precision(0.000_001_5, 2), "0.0000015");
420/// assert_eq!(js_to_precision(2.5, 1), "3"); // an exact tie, away from zero
421/// assert_eq!(js_to_precision(-0.0, 3), "0.00");
422/// ```
423#[must_use]
424pub fn js_to_precision(x: f64, precision: u32) -> String {
425    use std::fmt::Write;
426    if !x.is_finite() {
427        let mut s = String::new();
428        push_number(&mut s, x);
429        return s;
430    }
431    assert!(
432        (1..=100).contains(&precision),
433        "toPrecision() takes a precision from 1 to 100: {precision} was given"
434    );
435    let p = precision as usize;
436    let (digits, e) = significant(x, p);
437    let mut out = String::with_capacity(p + 8);
438    if x < 0.0 {
439        out.push('-');
440    }
441    if e < -6 || e >= precision as i32 {
442        push_mantissa(&mut out, &digits);
443        let _ = write!(out, "e{}{}", if e < 0 { '-' } else { '+' }, e.abs());
444    } else if let Ok(point) = usize::try_from(e) {
445        let whole = point + 1;
446        out.push_str(&digits[..whole]);
447        if whole < p {
448            out.push('.');
449            out.push_str(&digits[whole..]);
450        }
451    } else {
452        out.push_str("0.");
453        out.extend(std::iter::repeat_n('0', (-e - 1) as usize));
454        out.push_str(&digits);
455    }
456    out
457}
458
459/// `Number.prototype.toExponential(fractionDigits)`: the number written with one digit before the
460/// point and an exponent, `1.23e+5`. Given `Some(f)`, the digits are rounded to `f` places after
461/// the point from the number's exact binary value, an exact tie rounding away from zero; given
462/// `None`, as `toExponential()` without an argument, they are as many as it takes to tell the
463/// number from every other, the digits `String(x)` writes. `NaN` and the infinities are written as
464/// `String(x)` writes them.
465///
466/// # Panics
467///
468/// When `fraction_digits` is above 100 and `x` is finite, where JavaScript throws a `RangeError`.
469///
470/// # Example
471///
472/// ```
473/// # use ferrovue_core as ferrovue;
474/// use ferrovue::js_to_exponential;
475///
476/// assert_eq!(js_to_exponential(123_456.0, Some(2)), "1.23e+5");
477/// assert_eq!(js_to_exponential(0.000_15, None), "1.5e-4");
478/// assert_eq!(js_to_exponential(1.25, Some(1)), "1.3e+0"); // an exact tie, away from zero
479/// assert_eq!(js_to_exponential(-0.0, None), "0e+0");
480/// ```
481#[must_use]
482pub fn js_to_exponential(x: f64, fraction_digits: Option<u32>) -> String {
483    use std::fmt::Write;
484    if !x.is_finite() {
485        let mut s = String::new();
486        push_number(&mut s, x);
487        return s;
488    }
489    let (digits, e) = match fraction_digits {
490        Some(f) => {
491            assert!(
492                f <= 100,
493                "toExponential() takes from 0 to 100 fraction digits: {f} were given"
494            );
495            significant(x, f as usize + 1)
496        }
497        None if x == 0.0 => ("0".to_owned(), 0),
498        None => {
499            let (digits, e) = shortest_digits(x.abs());
500            (digits.as_str().to_owned(), e)
501        }
502    };
503    let mut out = String::with_capacity(digits.len() + 8);
504    if x < 0.0 {
505        out.push('-');
506    }
507    push_mantissa(&mut out, &digits);
508    let _ = write!(out, "e{}{}", if e < 0 { '-' } else { '+' }, e.abs());
509    out
510}
511
512fn push_mantissa(out: &mut String, digits: &str) {
513    out.push_str(&digits[..1]);
514    if digits.len() > 1 {
515        out.push('.');
516        out.push_str(&digits[1..]);
517    }
518}
519
520fn significant(x: f64, keep: usize) -> (String, i32) {
521    let (exact, e) = exact_digits(x);
522    let mut digits = rounded_half_up(&exact, keep);
523    if digits.len() > keep {
524        digits.truncate(keep);
525        (digits, e + 1)
526    } else {
527        (digits, e)
528    }
529}
530
531fn exact_digits(x: f64) -> (String, i32) {
532    use std::fmt::Write;
533    let mut s = String::new();
534    let _ = write!(s, "{:.800e}", x.abs());
535    let (mantissa, exp) = s.split_once('e').expect("`{:e}` writes an exponent");
536    let exp = exp.parse().expect("`{:e}` writes an integer exponent");
537    let mut digits: String = mantissa.chars().filter(|c| *c != '.').collect();
538    digits.truncate(digits.trim_end_matches('0').len().max(1));
539    (digits, exp)
540}
541
542fn rounded_half_up(exact: &str, keep: usize) -> String {
543    match exact.as_bytes().get(keep) {
544        None => {
545            let mut digits = exact.to_owned();
546            digits.extend(std::iter::repeat_n('0', keep - exact.len()));
547            digits
548        }
549        Some(&next) if next >= b'5' => increment_digits(&exact[..keep]),
550        Some(_) => exact[..keep].to_owned(),
551    }
552}
553
554fn increment_digits(digits: &str) -> String {
555    let mut bytes: Vec<u8> = digits.bytes().collect();
556    for b in bytes.iter_mut().rev() {
557        if *b == b'9' {
558            *b = b'0';
559        } else {
560            *b += 1;
561            return String::from_utf8(bytes).expect("ASCII digits");
562        }
563    }
564    bytes.insert(0, b'1');
565    String::from_utf8(bytes).expect("ASCII digits")
566}
567
568/// A number written as JavaScript writes it, for `format!` in generated code: `${n}` in a template
569/// literal, `"#" + n`, `n.toString()`.
570///
571/// `Display` is implemented for `Js<i64>`, written as [`push_int`] writes it, and `Js<f64>`,
572/// written as [`push_number`] writes it.
573///
574/// # Example
575///
576/// ```
577/// # use ferrovue_core as ferrovue;
578/// use ferrovue::Js;
579///
580/// assert_eq!(format!("{} items", Js(3_i64)), "3 items");
581/// assert_eq!(format!("{}", Js(1.5e-7_f64)), "1.5e-7");
582/// ```
583#[derive(Debug, Clone, Copy)]
584pub struct Js<T>(pub T);
585
586impl std::fmt::Display for Js<i64> {
587    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588        if self.0.unsigned_abs() > MAX_SAFE_INTEGER {
589            return Js(self.0 as f64).fmt(f);
590        }
591        let mut buf = [0; 20];
592        let start = decimal(&mut buf, self.0.unsigned_abs());
593        if self.0 < 0 {
594            f.write_str("-")?;
595        }
596        f.write_str(std::str::from_utf8(&buf[start..]).expect("ASCII digits"))
597    }
598}
599
600impl std::fmt::Display for Js<f64> {
601    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602        let mut s = String::new();
603        push_number(&mut s, self.0);
604        f.write_str(&s)
605    }
606}
607
608#[cfg(test)]
609mod tests;