Skip to main content

meta_language/translation/
decimal.rs

1//! Arbitrary-precision integer literals as decimal strings.
2//!
3//! The translator only compares, negates and re-bases literals (the JavaScript runtime uses
4//! `BigInt` for the same steps), so a signed decimal string is enough.
5
6use std::cmp::Ordering;
7use std::fmt::Write as _;
8
9/// A signed integer of any size, kept as its canonical decimal digits.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Decimal {
12    pub negative: bool,
13    /// Canonical digits: no leading zeros, `"0"` for zero.
14    pub digits: String,
15}
16
17impl Decimal {
18    /// Parses what `BigInt(text)` accepts for the translator's literals:
19    /// decimal digits, or `0x`/`0o`/`0b` followed by digits of that radix.
20    #[must_use]
21    pub fn parse(text: &str) -> Option<Self> {
22        let text = text.trim();
23        let (negative, body) = text
24            .strip_prefix('-')
25            .map_or((false, text), |rest| (true, rest));
26        let lower = body.to_ascii_lowercase();
27        let (radix, digits) = [("0x", 16), ("0o", 8), ("0b", 2)]
28            .into_iter()
29            .find_map(|(prefix, radix)| lower.strip_prefix(prefix).map(|rest| (radix, rest)))
30            .unwrap_or((10, lower.as_str()));
31        if digits.is_empty() || (radix != 10 && negative) {
32            return None;
33        }
34        // Little-endian base-10^9 limbs.
35        let mut limbs: Vec<u64> = vec![0];
36        for ch in digits.chars() {
37            let digit = u64::from(ch.to_digit(radix)?);
38            let mut carry = digit;
39            for limb in &mut limbs {
40                let value = *limb * u64::from(radix) + carry;
41                *limb = value % 1_000_000_000;
42                carry = value / 1_000_000_000;
43            }
44            while carry > 0 {
45                limbs.push(carry % 1_000_000_000);
46                carry /= 1_000_000_000;
47            }
48        }
49        let mut out = String::new();
50        for (index, limb) in limbs.iter().rev().enumerate() {
51            if index == 0 {
52                out.push_str(&limb.to_string());
53            } else {
54                let _ = write!(out, "{limb:09}");
55            }
56        }
57        let zero = out == "0";
58        Some(Self {
59            negative: negative && !zero,
60            digits: out,
61        })
62    }
63
64    #[must_use]
65    pub fn from_i128(value: i128) -> Self {
66        Self {
67            negative: value < 0,
68            digits: value.unsigned_abs().to_string(),
69        }
70    }
71
72    #[must_use]
73    pub fn from_u128(value: u128) -> Self {
74        Self {
75            negative: false,
76            digits: value.to_string(),
77        }
78    }
79
80    #[must_use]
81    pub fn negate(mut self) -> Self {
82        if self.digits != "0" {
83            self.negative = !self.negative;
84        }
85        self
86    }
87
88    #[must_use]
89    pub const fn is_negative(&self) -> bool {
90        self.negative
91    }
92
93    /// The value as an `i128`, when it fits.
94    #[must_use]
95    pub fn to_i128(&self) -> Option<i128> {
96        let text = self.to_string();
97        text.parse().ok()
98    }
99}
100
101impl std::fmt::Display for Decimal {
102    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        if self.negative {
104            formatter.write_str("-")?;
105        }
106        formatter.write_str(&self.digits)
107    }
108}
109
110fn magnitude(left: &str, right: &str) -> Ordering {
111    left.len().cmp(&right.len()).then_with(|| left.cmp(right))
112}
113
114impl Ord for Decimal {
115    fn cmp(&self, other: &Self) -> Ordering {
116        match (self.negative, other.negative) {
117            (false, true) => Ordering::Greater,
118            (true, false) => Ordering::Less,
119            (false, false) => magnitude(&self.digits, &other.digits),
120            (true, true) => magnitude(&other.digits, &self.digits),
121        }
122    }
123}
124
125impl PartialOrd for Decimal {
126    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
127        Some(self.cmp(other))
128    }
129}