1use std::cmp::Ordering;
21use std::fmt;
22
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct Decimal {
28 unscaled: i128,
29 scale: u32,
30}
31
32const MAX_PRECISION: u32 = 38;
34
35impl Decimal {
36 pub fn from_parts(unscaled: i128, scale: u32) -> Option<Self> {
39 (scale <= MAX_PRECISION).then_some(Self { unscaled, scale })
40 }
41
42 pub fn unscaled(&self) -> i128 {
43 self.unscaled
44 }
45
46 pub fn scale(&self) -> u32 {
49 self.scale
50 }
51
52 pub fn precision(&self) -> u32 {
55 let mut n = self.unscaled.unsigned_abs();
56 if n == 0 {
57 return 1;
58 }
59 let mut digits = 0;
60 while n > 0 {
61 digits += 1;
62 n /= 10;
63 }
64 digits.max(self.scale)
67 }
68
69 pub fn is_zero(&self) -> bool {
70 self.unscaled == 0
71 }
72
73 pub fn parse(s: &str) -> Option<Self> {
83 let s = s.trim();
84 if s.is_empty() {
85 return None;
86 }
87
88 let (mantissa, exp) = match s.find(['e', 'E']) {
90 Some(i) => {
91 let e: i32 = s[i + 1..].parse().ok()?;
92 (&s[..i], e)
93 }
94 None => (s, 0),
95 };
96
97 let (neg, digits) = match mantissa.strip_prefix('-') {
98 Some(rest) => (true, rest),
99 None => (false, mantissa.strip_prefix('+').unwrap_or(mantissa)),
100 };
101
102 let (int_part, frac_part) = match digits.find('.') {
103 Some(i) => (&digits[..i], &digits[i + 1..]),
104 None => (digits, ""),
105 };
106 if int_part.is_empty() && frac_part.is_empty() {
107 return None;
108 }
109 if !int_part.bytes().all(|b| b.is_ascii_digit())
110 || !frac_part.bytes().all(|b| b.is_ascii_digit())
111 {
112 return None;
113 }
114
115 let mut unscaled: i128 = 0;
117 for b in int_part.bytes().chain(frac_part.bytes()) {
118 unscaled = unscaled.checked_mul(10)?.checked_add((b - b'0') as i128)?;
119 }
120
121 let scale = i64::from(frac_part.len() as u32) - i64::from(exp);
122 let (unscaled, scale) = if scale < 0 {
123 let mut u = unscaled;
127 for _ in 0..(-scale) {
128 u = u.checked_mul(10)?;
129 }
130 (u, 0u32)
131 } else {
132 (unscaled, u32::try_from(scale).ok()?)
133 };
134 if scale > MAX_PRECISION {
135 return None;
136 }
137
138 Some(Self {
139 unscaled: if neg { -unscaled } else { unscaled },
140 scale,
141 })
142 }
143
144 pub fn rescale(&self, target: u32) -> Option<Self> {
147 if target > MAX_PRECISION {
148 return None;
149 }
150 match target.cmp(&self.scale) {
151 Ordering::Equal => Some(*self),
152 Ordering::Greater => {
153 let mut u = self.unscaled;
154 for _ in 0..(target - self.scale) {
155 u = u.checked_mul(10)?;
156 }
157 Some(Self {
158 unscaled: u,
159 scale: target,
160 })
161 }
162 Ordering::Less => {
163 let mut u = self.unscaled;
164 for _ in 0..(self.scale - target) {
165 if u % 10 != 0 {
166 return None; }
168 u /= 10;
169 }
170 Some(Self {
171 unscaled: u,
172 scale: target,
173 })
174 }
175 }
176 }
177
178 pub fn cmp_value(&self, other: &Self) -> Ordering {
186 let target = self.scale.max(other.scale);
187 if let (Some(a), Some(b)) = (self.rescale(target), other.rescale(target)) {
188 return a.unscaled.cmp(&b.unscaled);
189 }
190 digitwise_cmp(self, other)
191 }
192}
193
194fn digitwise_cmp(a: &Decimal, b: &Decimal) -> Ordering {
198 let (sa, sb) = (a.unscaled.signum(), b.unscaled.signum());
199 if sa != sb {
200 return sa.cmp(&sb);
201 }
202 let flip = sa < 0;
203 let (ai, af) = split_digits(a);
204 let (bi, bf) = split_digits(b);
205
206 let (ai, bi) = (ai.trim_start_matches('0'), bi.trim_start_matches('0'));
208 let ord = ai
209 .len()
210 .cmp(&bi.len())
211 .then_with(|| ai.cmp(bi))
212 .then_with(|| {
213 let n = af.len().max(bf.len());
215 let pad = |s: &str| {
216 let mut t = s.to_string();
217 t.extend(std::iter::repeat_n('0', n - s.len()));
218 t
219 };
220 pad(&af).cmp(&pad(&bf))
221 });
222 if flip {
223 ord.reverse()
224 } else {
225 ord
226 }
227}
228
229fn split_digits(d: &Decimal) -> (String, String) {
231 let digits = d.unscaled.unsigned_abs().to_string();
232 let scale = d.scale as usize;
233 if digits.len() > scale {
234 let (i, f) = digits.split_at(digits.len() - scale);
235 (i.to_string(), f.to_string())
236 } else {
237 let mut f = "0".repeat(scale - digits.len());
238 f.push_str(&digits);
239 ("0".into(), f)
240 }
241}
242
243impl fmt::Display for Decimal {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 let (int, frac) = split_digits(self);
249 if self.unscaled < 0 {
250 f.write_str("-")?;
251 }
252 f.write_str(&int)?;
253 if self.scale > 0 {
254 write!(f, ".{frac}")?;
255 }
256 Ok(())
257 }
258}
259
260impl PartialOrd for Decimal {
263 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
264 Some(self.cmp_value(other))
265 }
266}
267
268impl Serialize for Decimal {
272 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
273 s.collect_str(self)
274 }
275}
276
277impl<'de> Deserialize<'de> for Decimal {
278 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
279 use serde::de::Error;
280 let raw = serde_json::Value::deserialize(d)?;
281 let text = match &raw {
282 serde_json::Value::String(s) => s.clone(),
283 serde_json::Value::Number(n) if n.is_i64() || n.is_u64() => n.to_string(),
292 serde_json::Value::Number(n) => {
293 return Err(D::Error::custom(format!(
294 "decimal {n} arrived as a JSON number, which is parsed as f64 and has \
295 already lost precision; send it as a string"
296 )))
297 }
298 other => {
299 return Err(D::Error::custom(format!(
300 "decimal must be a string: {other}"
301 )))
302 }
303 };
304 Decimal::parse(&text)
305 .ok_or_else(|| D::Error::custom(format!("not an exact decimal: {text}")))
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn money_that_f64_corrupts_survives_to_the_digit() {
315 let s = "12345678901234567.89";
317 let d = Decimal::parse(s).unwrap();
318 assert_eq!(d.to_string(), s);
319 assert_eq!(d.unscaled(), 1_234_567_890_123_456_789);
320 assert_eq!(d.scale(), 2);
321 assert_ne!(s.parse::<f64>().unwrap().to_string(), s);
323 }
324
325 #[test]
326 fn scale_is_preserved_not_trimmed() {
327 let d = Decimal::parse("12.3400").unwrap();
328 assert_eq!(d.scale(), 4);
329 assert_eq!(d.to_string(), "12.3400");
330 let e = Decimal::parse("12.34").unwrap();
332 assert_eq!(d.cmp_value(&e), Ordering::Equal);
333 assert_ne!(d, e);
334 }
335
336 #[test]
337 fn parses_sign_fraction_and_exponent_exactly() {
338 assert_eq!(Decimal::parse("-0.004").unwrap().to_string(), "-0.004");
339 assert_eq!(Decimal::parse("1.5e3").unwrap().to_string(), "1500");
340 assert_eq!(Decimal::parse("15e-3").unwrap().to_string(), "0.015");
341 assert_eq!(Decimal::parse(".5").unwrap().to_string(), "0.5");
342 assert_eq!(Decimal::parse("+7").unwrap().to_string(), "7");
343 }
344
345 #[test]
346 fn rejects_what_it_cannot_represent_exactly() {
347 assert!(Decimal::parse("abc").is_none());
348 assert!(Decimal::parse("1.2.3").is_none());
349 assert!(Decimal::parse("").is_none());
350 assert!(Decimal::parse("NaN").is_none());
351 assert!(Decimal::parse(&"9".repeat(40)).is_none());
353 }
354
355 #[test]
356 fn ordering_is_exact_across_scales_and_signs() {
357 let ordered = [
358 "-100", "-2.5", "-0.001", "0", "0.001", "0.0010", "1.5", "1.50", "2.5", "100",
359 ];
360 for w in ordered.windows(2) {
361 let (a, b) = (Decimal::parse(w[0]).unwrap(), Decimal::parse(w[1]).unwrap());
362 assert!(
363 a.cmp_value(&b) != Ordering::Greater,
364 "{} should sort <= {}",
365 w[0],
366 w[1]
367 );
368 }
369 let a = Decimal::parse("100000000000000000.01").unwrap();
371 let b = Decimal::parse("100000000000000000.02").unwrap();
372 assert_eq!(a.cmp_value(&b), Ordering::Less);
373 }
374
375 #[test]
376 fn digitwise_fallback_matches_aligned_compare() {
377 let a = Decimal::from_parts(i128::MAX, 0).unwrap();
380 let b = Decimal::from_parts(1, 30).unwrap();
381 assert_eq!(a.cmp_value(&b), Ordering::Greater);
382 assert_eq!(b.cmp_value(&a), Ordering::Less);
383 let c = Decimal::from_parts(-1, 30).unwrap();
384 assert_eq!(c.cmp_value(&b), Ordering::Less);
385 }
386
387 #[test]
388 fn precision_counts_significant_digits() {
389 assert_eq!(Decimal::parse("0").unwrap().precision(), 1);
390 assert_eq!(Decimal::parse("123.45").unwrap().precision(), 5);
391 assert_eq!(Decimal::parse("0.004").unwrap().precision(), 3);
392 }
393
394 #[test]
395 fn json_is_a_string_and_round_trips() {
396 let d = Decimal::parse("12345678901234567.89").unwrap();
397 let j = serde_json::to_string(&d).unwrap();
398 assert_eq!(j, "\"12345678901234567.89\"");
399 assert_eq!(serde_json::from_str::<Decimal>(&j).unwrap(), d);
400 assert_eq!(
402 serde_json::from_str::<Decimal>("5").unwrap().to_string(),
403 "5"
404 );
405 let err = serde_json::from_str::<Decimal>("1.10")
409 .unwrap_err()
410 .to_string();
411 assert!(err.contains("send it as a string"), "{err}");
412 }
413}