Skip to main content

bynk_syntax/
decimal.rs

1//! Compile-time `Decimal` arithmetic helpers (#2622): the comparison and sign
2//! tests the checker needs to evaluate a decimal literal against a refinement
3//! (`Positive`, `NonNegative`, `InRange`). A decimal here is an optionally
4//! `-`-prefixed coefficient string and a scale; the runtime owns the real
5//! arithmetic (`bynk-emit/runtime/src/decimal.ts`).
6
7use std::cmp::Ordering;
8
9/// The sign of a coefficient: `-1`, `0` or `1`.
10pub fn sign(coef: &str) -> i32 {
11    let (neg, mag) = split_sign(coef);
12    if mag.bytes().all(|b| b == b'0') {
13        0
14    } else if neg {
15        -1
16    } else {
17        1
18    }
19}
20
21fn split_sign(coef: &str) -> (bool, &str) {
22    match coef.strip_prefix('-') {
23        Some(rest) => (true, rest),
24        None => (false, coef),
25    }
26}
27
28/// A decimal in canonical text form: `render("120", 2) == "1.20"`,
29/// `render("-5", 3) == "-0.005"`. A scale of 0 prints no point.
30pub fn render(coef: &str, scale: u32) -> String {
31    let (neg, mag) = split_sign(coef);
32    let scale = scale as usize;
33    let mag = if mag.len() <= scale {
34        format!("{}{mag}", "0".repeat(scale + 1 - mag.len()))
35    } else {
36        mag.to_string()
37    };
38    let (int, frac) = mag.split_at(mag.len() - scale);
39    let sign = if neg { "-" } else { "" };
40    if scale == 0 {
41        format!("{sign}{int}")
42    } else {
43        format!("{sign}{int}.{frac}")
44    }
45}
46
47/// The value-canonical text of a decimal: trailing fraction zeros dropped, so
48/// `1.20` and `1.2` agree (a contract is about values, not scales).
49pub fn canonical(coef: &str, scale: u32) -> String {
50    let (neg, mag) = split_sign(coef);
51    let (mut mag, mut scale) = (mag.trim_start_matches('0').to_string(), scale);
52    while scale > 0 && mag.ends_with('0') {
53        mag.pop();
54        scale -= 1;
55    }
56    if mag.is_empty() {
57        return "0".to_string();
58    }
59    render(&format!("{}{mag}", if neg { "-" } else { "" }), scale)
60}
61
62/// The numeric order of two decimals, ignoring how each is scaled
63/// (`1.20 == 1.2`).
64pub fn cmp(a_coef: &str, a_scale: u32, b_coef: &str, b_scale: u32) -> Ordering {
65    let (sa, sb) = (sign(a_coef), sign(b_coef));
66    if sa != sb {
67        return sa.cmp(&sb);
68    }
69    if sa == 0 {
70        return Ordering::Equal;
71    }
72    let (_, ma) = split_sign(a_coef);
73    let (_, mb) = split_sign(b_coef);
74    let scale = a_scale.max(b_scale);
75    let pad = |m: &str, s: u32| {
76        let mut out = m.trim_start_matches('0').to_string();
77        out.push_str(&"0".repeat((scale - s) as usize));
78        out
79    };
80    let (pa, pb) = (pad(ma, a_scale), pad(mb, b_scale));
81    let mag = pa.len().cmp(&pb.len()).then_with(|| pa.cmp(&pb));
82    if sa < 0 { mag.reverse() } else { mag }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn orders_by_value_not_scale() {
91        assert_eq!(cmp("120", 2, "12", 1), Ordering::Equal);
92        assert_eq!(cmp("1", 0, "99", 2), Ordering::Greater);
93        assert_eq!(cmp("-5", 0, "-50", 1), Ordering::Equal);
94        assert_eq!(cmp("-5", 0, "-6", 0), Ordering::Greater);
95        assert_eq!(cmp("-1", 0, "1", 0), Ordering::Less);
96        assert_eq!(cmp("0", 0, "-0", 3), Ordering::Equal);
97    }
98
99    #[test]
100    fn renders() {
101        assert_eq!(render("120", 2), "1.20");
102        assert_eq!(render("-5", 3), "-0.005");
103        assert_eq!(render("7", 0), "7");
104        assert_eq!(render("0", 2), "0.00");
105    }
106
107    #[test]
108    fn canonicalises() {
109        assert_eq!(canonical("120", 2), "1.2");
110        assert_eq!(canonical("100", 2), "1");
111        assert_eq!(canonical("-000", 1), "0");
112        assert_eq!(canonical("5", 3), "0.005");
113    }
114
115    #[test]
116    fn signs() {
117        assert_eq!(sign("000"), 0);
118        assert_eq!(sign("-0"), 0);
119        assert_eq!(sign("-12"), -1);
120        assert_eq!(sign("12"), 1);
121    }
122}