1use std::cmp::Ordering;
45use std::fmt;
46use std::str::FromStr;
47
48use serde::{Deserialize, Deserializer, Serialize, Serializer};
49
50pub const MAX_DECIMAL_SCALE: u8 = 38;
56
57pub const DECIMAL_PRECISION: u8 = 38;
63
64pub const MAX_DECIMAL_MANTISSA: i128 = 99_999_999_999_999_999_999_999_999_999_999_999_999;
70
71const POW10: [i128; 39] = {
73 let mut table = [1i128; 39];
74 let mut i = 1;
75 while i < 39 {
76 table[i] = table[i - 1] * 10;
77 i += 1;
78 }
79 table
80};
81
82#[inline]
88#[must_use]
89pub fn pow10(exp: u32) -> Option<i128> {
90 POW10.get(exp as usize).copied()
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95pub enum DecimalError {
96 #[error("decimal scale {scale} exceeds the maximum of {max}")]
98 ScaleTooLarge {
99 scale: u32,
101 max: u8,
103 },
104
105 #[error("decimal mantissa {mantissa} exceeds {digits} significant digits")]
107 PrecisionOverflow {
108 mantissa: i128,
110 digits: u8,
112 },
113
114 #[error("cannot rescale {value} from scale {from} to scale {to} without losing digits")]
116 InexactRescale {
117 value: String,
119 from: u8,
121 to: u8,
123 },
124
125 #[error("decimal arithmetic overflowed 38 significant digits")]
127 Overflow,
128
129 #[error("invalid decimal literal \"{input}\": {reason}")]
131 Parse {
132 input: String,
134 reason: &'static str,
136 },
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157pub struct Decimal {
158 mantissa: i128,
160 scale: u8,
162}
163
164impl Decimal {
165 pub const fn new(mantissa: i128, scale: u8) -> Result<Self, DecimalError> {
173 if scale > MAX_DECIMAL_SCALE {
174 return Err(DecimalError::ScaleTooLarge {
175 scale: scale as u32,
176 max: MAX_DECIMAL_SCALE,
177 });
178 }
179 if mantissa > MAX_DECIMAL_MANTISSA || mantissa < -MAX_DECIMAL_MANTISSA {
180 return Err(DecimalError::PrecisionOverflow {
181 mantissa,
182 digits: DECIMAL_PRECISION,
183 });
184 }
185 Ok(Self { mantissa, scale })
186 }
187
188 pub const fn zero(scale: u8) -> Result<Self, DecimalError> {
194 Self::new(0, scale)
195 }
196
197 #[inline]
199 #[must_use]
200 pub const fn mantissa(&self) -> i128 {
201 self.mantissa
202 }
203
204 #[inline]
206 #[must_use]
207 pub const fn scale(&self) -> u8 {
208 self.scale
209 }
210
211 #[inline]
213 #[must_use]
214 pub const fn is_zero(&self) -> bool {
215 self.mantissa == 0
216 }
217
218 #[inline]
220 #[must_use]
221 pub const fn signum(&self) -> i8 {
222 if self.mantissa > 0 {
223 1
224 } else if self.mantissa < 0 {
225 -1
226 } else {
227 0
228 }
229 }
230
231 #[inline]
236 #[must_use]
237 pub const fn abs(&self) -> Self {
238 Self {
239 mantissa: self.mantissa.abs(),
240 scale: self.scale,
241 }
242 }
243
244 #[inline]
246 #[must_use]
247 pub const fn neg(&self) -> Self {
248 Self {
249 mantissa: -self.mantissa,
250 scale: self.scale,
251 }
252 }
253
254 pub fn rescale(&self, target: u8) -> Result<Self, DecimalError> {
269 if target > MAX_DECIMAL_SCALE {
270 return Err(DecimalError::ScaleTooLarge {
271 scale: u32::from(target),
272 max: MAX_DECIMAL_SCALE,
273 });
274 }
275 match target.cmp(&self.scale) {
276 Ordering::Equal => Ok(*self),
277 Ordering::Greater => {
278 let factor = pow10(u32::from(target - self.scale)).ok_or(DecimalError::Overflow)?;
279 let mantissa = self
280 .mantissa
281 .checked_mul(factor)
282 .ok_or(DecimalError::Overflow)?;
283 Self::new(mantissa, target)
284 }
285 Ordering::Less => {
286 let factor = pow10(u32::from(self.scale - target)).ok_or(DecimalError::Overflow)?;
287 if self.mantissa % factor != 0 {
288 return Err(DecimalError::InexactRescale {
289 value: self.to_string(),
290 from: self.scale,
291 to: target,
292 });
293 }
294 Self::new(self.mantissa / factor, target)
295 }
296 }
297 }
298
299 #[must_use]
304 pub fn normalized(&self, min_scale: u8) -> Self {
305 let mut out = *self;
306 while out.scale > min_scale && out.mantissa % 10 == 0 && out.mantissa != 0 {
307 out.mantissa /= 10;
308 out.scale -= 1;
309 }
310 if out.mantissa == 0 && out.scale > min_scale {
312 out.scale = min_scale;
313 }
314 out
315 }
316
317 pub fn checked_add(&self, other: &Self) -> Result<Self, DecimalError> {
324 let scale = self.scale.max(other.scale);
325 let a = self.rescale(scale)?;
326 let b = other.rescale(scale)?;
327 let mantissa = a
328 .mantissa
329 .checked_add(b.mantissa)
330 .ok_or(DecimalError::Overflow)?;
331 Self::new(mantissa, scale).map_err(|_| DecimalError::Overflow)
332 }
333
334 pub fn checked_sub(&self, other: &Self) -> Result<Self, DecimalError> {
341 self.checked_add(&other.neg())
342 }
343
344 #[inline]
349 #[must_use]
350 #[allow(clippy::cast_precision_loss)]
351 pub fn to_f64_lossy(&self) -> f64 {
352 self.mantissa as f64 / pow10(u32::from(self.scale)).unwrap_or(1) as f64
353 }
354
355 #[must_use]
363 pub fn cmp_exact(&self, other: &Self) -> Ordering {
364 if self.scale == other.scale {
365 return self.mantissa.cmp(&other.mantissa);
366 }
367 let sign = self.signum().cmp(&other.signum());
369 if sign != Ordering::Equal {
370 return sign;
371 }
372 let scale = self.scale.max(other.scale);
373 match (self.rescale(scale), other.rescale(scale)) {
374 (Ok(a), Ok(b)) => a.mantissa.cmp(&b.mantissa),
375 (Err(_), Ok(_)) => {
378 if self.signum() >= 0 {
379 Ordering::Greater
380 } else {
381 Ordering::Less
382 }
383 }
384 (Ok(_), Err(_)) => {
385 if self.signum() >= 0 {
386 Ordering::Less
387 } else {
388 Ordering::Greater
389 }
390 }
391 (Err(_), Err(_)) => {
394 let narrow = self.scale.min(other.scale);
395 let a = self.mantissa / pow10(u32::from(self.scale - narrow)).unwrap_or(1);
396 let b = other.mantissa / pow10(u32::from(other.scale - narrow)).unwrap_or(1);
397 a.cmp(&b)
398 }
399 }
400 }
401}
402
403impl PartialOrd for Decimal {
404 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
405 Some(self.cmp(other))
406 }
407}
408
409impl Ord for Decimal {
410 fn cmp(&self, other: &Self) -> Ordering {
411 self.cmp_exact(other)
412 }
413}
414
415impl fmt::Display for Decimal {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 if self.scale == 0 {
419 return write!(f, "{}", self.mantissa);
420 }
421 let negative = self.mantissa < 0;
422 let digits = self.mantissa.unsigned_abs().to_string();
425 let scale = usize::from(self.scale);
426 let (int_part, frac_part) = if digits.len() > scale {
427 let split = digits.len() - scale;
428 (digits[..split].to_string(), digits[split..].to_string())
429 } else {
430 ("0".to_string(), format!("{:0>scale$}", digits))
431 };
432 if negative {
433 write!(f, "-")?;
434 }
435 write!(f, "{int_part}.{frac_part}")
436 }
437}
438
439impl FromStr for Decimal {
440 type Err = DecimalError;
441
442 fn from_str(s: &str) -> Result<Self, Self::Err> {
451 let err = |reason: &'static str| DecimalError::Parse {
452 input: s.to_string(),
453 reason,
454 };
455 if s.is_empty() {
456 return Err(err("empty"));
457 }
458
459 let (mantissa_str, exponent) = match s.find(['e', 'E']) {
460 Some(idx) => {
461 let exp = s[idx + 1..]
462 .parse::<i32>()
463 .map_err(|_| err("malformed exponent"))?;
464 if !(-1_000..=1_000).contains(&exp) {
465 return Err(err("exponent out of range"));
466 }
467 (&s[..idx], exp)
468 }
469 None => (s, 0),
470 };
471
472 let (negative, digits) = match mantissa_str.as_bytes().first() {
473 Some(b'-') => (true, &mantissa_str[1..]),
474 Some(b'+') => (false, &mantissa_str[1..]),
475 _ => (false, mantissa_str),
476 };
477 if digits.is_empty() {
478 return Err(err("no digits"));
479 }
480
481 let (int_digits, mut frac_digits) = match digits.find('.') {
482 Some(idx) => {
483 let frac = &digits[idx + 1..];
484 if frac.contains('.') {
485 return Err(err("more than one decimal point"));
486 }
487 (&digits[..idx], frac)
488 }
489 None => (digits, ""),
490 };
491 if int_digits.is_empty() && frac_digits.is_empty() {
492 return Err(err("no digits"));
493 }
494
495 while i64::try_from(frac_digits.len()).map_err(|_| err("too many fractional digits"))?
501 - i64::from(exponent)
502 > i64::from(MAX_DECIMAL_SCALE)
503 && frac_digits.ends_with('0')
504 {
505 frac_digits = &frac_digits[..frac_digits.len() - 1];
506 }
507
508 let mut mantissa: i128 = 0;
509 for part in [int_digits, frac_digits] {
510 for byte in part.bytes() {
511 let digit = match byte {
512 b'0'..=b'9' => i128::from(byte - b'0'),
513 _ => return Err(err("non-digit character")),
514 };
515 mantissa = mantissa
516 .checked_mul(10)
517 .and_then(|m| m.checked_add(digit))
518 .ok_or(DecimalError::PrecisionOverflow {
519 mantissa: i128::MAX,
520 digits: DECIMAL_PRECISION,
521 })?;
522 }
523 }
524 if negative {
525 mantissa = -mantissa;
526 }
527
528 let raw_scale = i64::try_from(frac_digits.len())
533 .map_err(|_| err("too many fractional digits"))?
534 - i64::from(exponent);
535
536 if raw_scale < 0 {
537 let shift = u32::try_from(-raw_scale).map_err(|_| err("exponent out of range"))?;
540 let factor = pow10(shift).ok_or(DecimalError::Overflow)?;
541 let mantissa = mantissa.checked_mul(factor).ok_or(DecimalError::Overflow)?;
542 return Self::new(mantissa, 0);
543 }
544
545 if raw_scale > i64::from(MAX_DECIMAL_SCALE) {
546 return Err(DecimalError::ScaleTooLarge {
549 scale: u32::try_from(raw_scale).unwrap_or(u32::MAX),
550 max: MAX_DECIMAL_SCALE,
551 });
552 }
553
554 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
555 Self::new(mantissa, raw_scale as u8)
556 }
557}
558
559impl Serialize for Decimal {
560 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
568 if serializer.is_human_readable() {
569 serializer.serialize_str(&self.to_string())
570 } else {
571 let repr = DecimalRepr {
572 mantissa: self.mantissa,
573 scale: self.scale,
574 };
575 repr.serialize(serializer)
576 }
577 }
578}
579
580impl<'de> Deserialize<'de> for Decimal {
581 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
582 if deserializer.is_human_readable() {
583 let text = String::deserialize(deserializer)?;
584 text.parse().map_err(serde::de::Error::custom)
585 } else {
586 let repr = DecimalRepr::deserialize(deserializer)?;
587 Self::new(repr.mantissa, repr.scale).map_err(serde::de::Error::custom)
588 }
589 }
590}
591
592#[derive(Serialize, Deserialize)]
594struct DecimalRepr {
595 mantissa: i128,
596 scale: u8,
597}
598
599#[cfg(feature = "rust_decimal")]
621mod rust_decimal_interop {
622 use super::{Decimal, DecimalError, DECIMAL_PRECISION, MAX_DECIMAL_SCALE};
623
624 impl TryFrom<rust_decimal::Decimal> for Decimal {
625 type Error = DecimalError;
626
627 fn try_from(value: rust_decimal::Decimal) -> Result<Self, Self::Error> {
628 let scale = u8::try_from(value.scale()).map_err(|_| DecimalError::ScaleTooLarge {
629 scale: value.scale(),
630 max: MAX_DECIMAL_SCALE,
631 })?;
632 Self::new(value.mantissa(), scale)
633 }
634 }
635
636 impl TryFrom<Decimal> for rust_decimal::Decimal {
637 type Error = DecimalError;
638
639 fn try_from(value: Decimal) -> Result<Self, Self::Error> {
640 Self::try_from_i128_with_scale(value.mantissa(), u32::from(value.scale())).map_err(
641 |_| DecimalError::PrecisionOverflow {
642 mantissa: value.mantissa(),
643 digits: DECIMAL_PRECISION,
644 },
645 )
646 }
647 }
648
649 #[cfg(test)]
650 mod tests {
651 use super::*;
652 use std::str::FromStr;
653
654 #[test]
655 fn round_trip_through_rust_decimal() {
656 for text in ["0", "231.45", "-231.45", "1234.5678", "0.0000000001"] {
657 let theirs = rust_decimal::Decimal::from_str(text).unwrap();
658 let ours = Decimal::try_from(theirs).unwrap();
659 assert_eq!(ours.to_string(), text);
660 let back = rust_decimal::Decimal::try_from(ours).unwrap();
661 assert_eq!(back, theirs);
662 }
663 }
664
665 #[test]
666 fn a_value_too_wide_for_rust_decimal_is_refused_not_rounded() {
667 let ours: Decimal = "99999999999999999999999999999999999999".parse().unwrap();
669 assert!(rust_decimal::Decimal::try_from(ours).is_err());
670 }
671
672 #[test]
673 fn a_scale_above_28_is_refused() {
674 let ours = Decimal::new(1, 30).unwrap();
675 assert!(rust_decimal::Decimal::try_from(ours).is_err());
676 }
677 }
678}
679
680impl TryFrom<i64> for Decimal {
681 type Error = DecimalError;
682
683 fn try_from(value: i64) -> Result<Self, Self::Error> {
685 Self::new(i128::from(value), 0)
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn parse_and_render_round_trip() {
695 for text in [
696 "0",
697 "0.0",
698 "231.45",
699 "-231.45",
700 "1234.5678",
701 "-0.001",
702 "0.00000000000000000001",
703 "99999999999999999999999999999999999999",
704 ] {
705 let d: Decimal = text.parse().unwrap();
706 assert_eq!(d.to_string(), text, "round trip of {text}");
707 }
708 }
709
710 #[test]
711 fn parse_keeps_every_digit_an_f64_would_lose() {
712 let d: Decimal = "0.30000000000000004".parse().unwrap();
715 assert_eq!(d.mantissa(), 30_000_000_000_000_004);
716 assert_eq!(d.scale(), 17);
717 }
718
719 #[test]
720 fn parse_scale_and_sign() {
721 let d: Decimal = "231.45".parse().unwrap();
722 assert_eq!(d.mantissa(), 23_145);
723 assert_eq!(d.scale(), 2);
724 let d: Decimal = "-0.05".parse().unwrap();
725 assert_eq!(d.mantissa(), -5);
726 assert_eq!(d.scale(), 2);
727 let d: Decimal = "+7".parse().unwrap();
728 assert_eq!(d.mantissa(), 7);
729 assert_eq!(d.scale(), 0);
730 }
731
732 #[test]
733 fn parse_exponent() {
734 assert_eq!("1.5e3".parse::<Decimal>().unwrap().to_string(), "1500");
735 assert_eq!("15e-3".parse::<Decimal>().unwrap().to_string(), "0.015");
736 assert_eq!("1E2".parse::<Decimal>().unwrap().to_string(), "100");
737 }
738
739 #[test]
740 fn parse_rejects_junk() {
741 for bad in [
742 "", "-", ".", "1.2.3", "1,5", "abc", "1.2x", "1e", "1e999999",
743 ] {
744 assert!(bad.parse::<Decimal>().is_err(), "{bad} should not parse");
745 }
746 }
747
748 #[test]
749 fn parse_bare_fraction() {
750 let d: Decimal = ".5".parse().unwrap();
751 assert_eq!(d.to_string(), "0.5");
752 }
753
754 #[test]
755 fn parse_overflow_is_rejected_not_wrapped() {
756 let too_big = "1".repeat(40);
757 assert!(too_big.parse::<Decimal>().is_err());
758 }
759
760 #[test]
761 fn parse_strips_representable_trailing_zeros_beyond_max_scale() {
762 let text = format!("0.{}", "1".to_string() + &"0".repeat(39));
764 let d: Decimal = text.parse().unwrap();
765 assert_eq!(d.scale(), MAX_DECIMAL_SCALE);
766 assert_eq!(d.mantissa(), 10_i128.pow(37));
767 let text = format!("0.{}1", "0".repeat(40));
769 assert!(text.parse::<Decimal>().is_err());
770 }
771
772 #[test]
773 fn rescale_widens_exactly_and_refuses_to_round() {
774 let d: Decimal = "1.5".parse().unwrap();
775 assert_eq!(d.rescale(4).unwrap().to_string(), "1.5000");
776 assert_eq!(d.rescale(4).unwrap().rescale(1).unwrap(), d);
777 let d: Decimal = "1.55".parse().unwrap();
778 assert!(matches!(
779 d.rescale(1),
780 Err(DecimalError::InexactRescale { .. })
781 ));
782 }
783
784 #[test]
785 fn rescale_rejects_scale_above_max() {
786 let d: Decimal = "1".parse().unwrap();
787 assert!(matches!(
788 d.rescale(MAX_DECIMAL_SCALE + 1),
789 Err(DecimalError::ScaleTooLarge { .. })
790 ));
791 }
792
793 #[test]
794 fn new_rejects_out_of_range() {
795 assert!(matches!(
796 Decimal::new(1, MAX_DECIMAL_SCALE + 1),
797 Err(DecimalError::ScaleTooLarge { .. })
798 ));
799 assert!(matches!(
800 Decimal::new(MAX_DECIMAL_MANTISSA + 1, 0),
801 Err(DecimalError::PrecisionOverflow { .. })
802 ));
803 assert!(matches!(
804 Decimal::new(-MAX_DECIMAL_MANTISSA - 1, 0),
805 Err(DecimalError::PrecisionOverflow { .. })
806 ));
807 }
808
809 #[test]
810 fn arithmetic_is_exact() {
811 let a: Decimal = "0.1".parse().unwrap();
812 let b: Decimal = "0.2".parse().unwrap();
813 assert_eq!(a.checked_add(&b).unwrap().to_string(), "0.3");
814 let a: Decimal = "1.5".parse().unwrap();
815 let b: Decimal = "0.250".parse().unwrap();
816 assert_eq!(a.checked_add(&b).unwrap().to_string(), "1.750");
817 assert_eq!(a.checked_sub(&b).unwrap().to_string(), "1.250");
818 }
819
820 #[test]
821 fn addition_overflow_is_an_error() {
822 let a = Decimal::new(MAX_DECIMAL_MANTISSA, 0).unwrap();
823 assert!(matches!(a.checked_add(&a), Err(DecimalError::Overflow)));
824 }
825
826 #[test]
827 fn ordering_across_scales() {
828 let a: Decimal = "1.5".parse().unwrap();
829 let b: Decimal = "1.50".parse().unwrap();
830 assert_eq!(a.cmp_exact(&b), Ordering::Equal);
831 let c: Decimal = "1.51".parse().unwrap();
832 assert_eq!(a.cmp_exact(&c), Ordering::Less);
833 let neg: Decimal = "-1.5".parse().unwrap();
834 assert_eq!(neg.cmp_exact(&a), Ordering::Less);
835 assert_eq!(neg.cmp_exact(&neg), Ordering::Equal);
836 }
837
838 #[test]
839 fn ordering_when_alignment_overflows() {
840 let huge = Decimal::new(POW10[37], 0).unwrap();
843 let tiny = Decimal::new(1, MAX_DECIMAL_SCALE).unwrap();
844 assert_eq!(huge.cmp_exact(&tiny), Ordering::Greater);
845 assert_eq!(tiny.cmp_exact(&huge), Ordering::Less);
846 let huge_neg = huge.neg();
847 assert_eq!(huge_neg.cmp_exact(&tiny), Ordering::Less);
848 assert_eq!(tiny.cmp_exact(&huge_neg), Ordering::Greater);
849 }
850
851 #[test]
852 fn ordering_is_a_total_order_on_a_mixed_scale_sample() {
853 let mut values: Vec<Decimal> = [
854 "-2", "-1.5", "-1.50", "0", "0.000", "0.5", "1", "1.0000", "1.5", "2",
855 ]
856 .iter()
857 .map(|s| s.parse().unwrap())
858 .collect();
859 values.sort();
860 let rendered: Vec<String> = values.iter().map(ToString::to_string).collect();
861 assert_eq!(
862 rendered,
863 ["-2", "-1.5", "-1.50", "0", "0.000", "0.5", "1", "1.0000", "1.5", "2"]
864 .iter()
865 .map(ToString::to_string)
866 .collect::<Vec<_>>()
867 );
868 }
869
870 #[test]
871 fn normalized_strips_only_trailing_zeros() {
872 let d: Decimal = "1.2500".parse().unwrap();
873 assert_eq!(d.normalized(0).to_string(), "1.25");
874 assert_eq!(d.normalized(3).to_string(), "1.250");
875 let z: Decimal = "0.0000".parse().unwrap();
876 assert_eq!(z.normalized(0).to_string(), "0");
877 }
878
879 #[test]
880 fn json_is_a_string_and_survives_the_round_trip() {
881 let d: Decimal = "1234.5678".parse().unwrap();
882 let json = serde_json::to_string(&d).unwrap();
883 assert_eq!(json, "\"1234.5678\"");
884 let back: Decimal = serde_json::from_str(&json).unwrap();
885 assert_eq!(back, d);
886 }
887
888 #[test]
889 fn postcard_round_trip_is_the_mantissa_and_scale() {
890 let d: Decimal = "1234.5678".parse().unwrap();
891 let bytes = postcard::to_stdvec(&d).unwrap();
892 let back: Decimal = postcard::from_bytes(&bytes).unwrap();
893 assert_eq!(back, d);
894 assert_eq!(back.scale(), 4);
895 }
896
897 #[test]
898 fn to_f64_lossy_is_the_only_way_out() {
899 let d: Decimal = "231.45".parse().unwrap();
900 assert!((d.to_f64_lossy() - 231.45).abs() < 1e-9);
901 }
902
903 #[test]
904 fn signum_and_abs() {
905 let d: Decimal = "-1.5".parse().unwrap();
906 assert_eq!(d.signum(), -1);
907 assert_eq!(d.abs().to_string(), "1.5");
908 let z: Decimal = "0.00".parse().unwrap();
909 assert_eq!(z.signum(), 0);
910 assert!(z.is_zero());
911 }
912}