Skip to main content

sda_lib/
number.rs

1use num_bigint::BigInt;
2use num_rational::BigRational;
3use num_traits::{One, Signed, Zero};
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
8pub struct ExactNum(BigRational);
9
10#[derive(Debug, Clone)]
11pub enum ParseNumError {
12    InvalidFormat,
13    InvalidDigits,
14    ZeroDenominator,
15}
16
17impl fmt::Display for ParseNumError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::InvalidFormat => write!(f, "invalid numeric format"),
21            Self::InvalidDigits => write!(f, "invalid numeric digits"),
22            Self::ZeroDenominator => write!(f, "zero denominator"),
23        }
24    }
25}
26
27impl std::error::Error for ParseNumError {}
28
29impl ExactNum {
30    pub fn parse_literal(src: &str) -> Result<Self, ParseNumError> {
31        let src = src.trim();
32        if src.is_empty() {
33            return Err(ParseNumError::InvalidFormat);
34        }
35
36        let (sign, unsigned) = if let Some(rest) = src.strip_prefix('-') {
37            (-1i8, rest)
38        } else if let Some(rest) = src.strip_prefix('+') {
39            (1i8, rest)
40        } else {
41            (1i8, src)
42        };
43
44        let (mantissa, exponent) = if let Some((mantissa, exponent)) = unsigned.split_once(['e', 'E']) {
45            let parsed_exp = exponent.parse::<i64>().map_err(|_| ParseNumError::InvalidFormat)?;
46            (mantissa, parsed_exp)
47        } else {
48            (unsigned, 0i64)
49        };
50
51        let (int_part, frac_part) = if let Some((int_part, frac_part)) = mantissa.split_once('.') {
52            (int_part, frac_part)
53        } else {
54            (mantissa, "")
55        };
56
57        if int_part.is_empty() && frac_part.is_empty() {
58            return Err(ParseNumError::InvalidFormat);
59        }
60
61        if !int_part.chars().all(|c| c.is_ascii_digit()) || !frac_part.chars().all(|c| c.is_ascii_digit()) {
62            return Err(ParseNumError::InvalidDigits);
63        }
64
65        let digits = format!("{int_part}{frac_part}");
66        let mut numerator = if digits.is_empty() {
67            BigInt::zero()
68        } else {
69            BigInt::from_str(&digits).map_err(|_| ParseNumError::InvalidDigits)?
70        };
71
72        let scale = frac_part.len() as i64 - exponent;
73        let denominator = if scale <= 0 {
74            numerator *= pow10((-scale) as u32);
75            BigInt::one()
76        } else {
77            pow10(scale as u32)
78        };
79
80        if sign < 0 {
81            numerator = -numerator;
82        }
83
84        Ok(Self(BigRational::new(numerator, denominator)))
85    }
86
87    pub fn parse_canonical(src: &str) -> Result<Self, ParseNumError> {
88        if let Some((numerator, denominator)) = src.split_once('/') {
89            let numerator = BigInt::from_str(numerator).map_err(|_| ParseNumError::InvalidDigits)?;
90            let denominator = BigInt::from_str(denominator).map_err(|_| ParseNumError::InvalidDigits)?;
91            if denominator.is_zero() {
92                return Err(ParseNumError::ZeroDenominator);
93            }
94            Ok(Self(BigRational::new(numerator, denominator)))
95        } else {
96            Self::parse_literal(src)
97        }
98    }
99
100    pub fn from_usize(value: usize) -> Self {
101        Self(BigRational::from_integer(BigInt::from(value)))
102    }
103
104    pub fn is_zero(&self) -> bool {
105        self.0.is_zero()
106    }
107
108    pub fn add(&self, other: &Self) -> Self {
109        Self(self.0.clone() + other.0.clone())
110    }
111
112    pub fn sub(&self, other: &Self) -> Self {
113        Self(self.0.clone() - other.0.clone())
114    }
115
116    pub fn mul(&self, other: &Self) -> Self {
117        Self(self.0.clone() * other.0.clone())
118    }
119
120    pub fn div(&self, other: &Self) -> Self {
121        Self(self.0.clone() / other.0.clone())
122    }
123
124    pub fn neg(&self) -> Self {
125        Self(-self.0.clone())
126    }
127
128    pub fn to_json_value(&self) -> serde_json::Value {
129        if let Some(decimal) = self.finite_decimal_string() {
130            if let Ok(number) = serde_json::Number::from_str(&decimal) {
131                return serde_json::Value::Number(number);
132            }
133        }
134
135        serde_json::json!({
136            "$type": "num",
137            "$value": self.canonical_string(),
138        })
139    }
140
141    pub fn canonical_string(&self) -> String {
142        if let Some(decimal) = self.finite_decimal_string() {
143            decimal
144        } else {
145            format!("{}/{}", self.0.numer(), self.0.denom())
146        }
147    }
148
149    fn finite_decimal_string(&self) -> Option<String> {
150        let numerator = self.0.numer().clone();
151        let mut denominator = self.0.denom().clone();
152
153        if denominator == BigInt::one() {
154            return Some(numerator.to_string());
155        }
156
157        let two = BigInt::from(2u8);
158        let five = BigInt::from(5u8);
159        let mut twos = 0u32;
160        let mut fives = 0u32;
161
162        while (&denominator % &two).is_zero() {
163            denominator /= &two;
164            twos += 1;
165        }
166
167        while (&denominator % &five).is_zero() {
168            denominator /= &five;
169            fives += 1;
170        }
171
172        if denominator != BigInt::one() {
173            return None;
174        }
175
176        let scale = twos.max(fives);
177        let mut adjusted = numerator.abs();
178        if scale > twos {
179            adjusted *= five.pow(scale - twos);
180        }
181        if scale > fives {
182            adjusted *= two.pow(scale - fives);
183        }
184
185        let digits = adjusted.to_string();
186        let negative = numerator.is_negative();
187        let sign = if negative { "-" } else { "" };
188
189        if scale == 0 {
190            return Some(format!("{sign}{digits}"));
191        }
192
193        let scale = scale as usize;
194        if digits.len() <= scale {
195            let padding = "0".repeat(scale - digits.len());
196            Some(format!("{sign}0.{padding}{digits}"))
197        } else {
198            let split = digits.len() - scale;
199            Some(format!("{sign}{}.{}", &digits[..split], &digits[split..]))
200        }
201    }
202}
203
204impl fmt::Display for ExactNum {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "{}", self.canonical_string())
207    }
208}
209
210fn pow10(exp: u32) -> BigInt {
211    BigInt::from(10u8).pow(exp)
212}