1use std::{cmp::Ordering, fmt, ops::Neg, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7
8use crate::ParseError;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Decimal {
18 units: i128,
19 scale: u8,
20}
21
22impl Decimal {
23 pub const MAX_SCALE: u8 = 18;
25 pub const ZERO: Decimal = Decimal { units: 0, scale: 0 };
27
28 pub fn new(units: i128, scale: u8) -> Option<Self> {
30 (scale <= Self::MAX_SCALE).then(|| Decimal { units, scale }.reduced())
31 }
32
33 pub fn scale(self) -> u8 {
35 self.scale
36 }
37
38 pub fn is_zero(self) -> bool {
40 self.units == 0
41 }
42
43 pub fn is_negative(self) -> bool {
45 self.units < 0
46 }
47
48 pub fn checked_add(self, other: Self) -> Option<Self> {
50 let scale = self.scale.max(other.scale);
51 let a = self.units_at(scale)?;
52 let b = other.units_at(scale)?;
53 Some(Decimal { units: a.checked_add(b)?, scale }.reduced())
54 }
55
56 pub fn checked_sub(self, other: Self) -> Option<Self> {
58 self.checked_add(-other)
59 }
60
61 fn units_at(self, scale: u8) -> Option<i128> {
63 10i128.checked_pow(u32::from(scale - self.scale))?.checked_mul(self.units)
64 }
65
66 fn reduced(self) -> Self {
68 let (mut units, mut scale) = (self.units, self.scale);
69 while scale > 0 && units % 10 == 0 {
70 units /= 10;
71 scale -= 1;
72 }
73 Decimal { units, scale }
74 }
75}
76
77impl Neg for Decimal {
78 type Output = Decimal;
79
80 fn neg(self) -> Decimal {
82 Decimal { units: -self.units, scale: self.scale }
83 }
84}
85
86impl Ord for Decimal {
87 fn cmp(&self, other: &Self) -> Ordering {
88 let scale = self.scale.max(other.scale);
89 match (self.units_at(scale), other.units_at(scale)) {
90 (Some(a), Some(b)) => a.cmp(&b),
91 _ => (self.units / 10i128.pow(u32::from(self.scale)))
93 .cmp(&(other.units / 10i128.pow(u32::from(other.scale))))
94 .then_with(|| (self.units, self.scale).cmp(&(other.units, other.scale))),
95 }
96 }
97}
98
99impl PartialOrd for Decimal {
100 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
101 Some(self.cmp(other))
102 }
103}
104
105impl FromStr for Decimal {
106 type Err = ParseError;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 let err = || ParseError::Decimal(s.to_owned());
110 let (negative, digits) = match s.strip_prefix('-') {
111 Some(rest) => (true, rest),
112 None => (false, s),
113 };
114 let (int, frac) = digits.split_once('.').unwrap_or((digits, ""));
115 let valid = |part: &str| part.bytes().all(|b| b.is_ascii_digit());
116 let canonical = (int == "0" || !int.starts_with('0'))
118 && !frac.ends_with('0')
119 && (digits.contains('.') == !frac.is_empty())
120 && !(negative && int == "0" && frac.is_empty());
121 if int.is_empty() || !valid(int) || !valid(frac) || !canonical {
122 return Err(err());
123 }
124 let scale = u8::try_from(frac.len()).ok().filter(|&n| n <= Self::MAX_SCALE).ok_or_else(err)?;
125 let units: i128 = format!("{int}{frac}").parse().map_err(|_| err())?;
126 Ok(Decimal { units: if negative { -units } else { units }, scale })
127 }
128}
129
130impl fmt::Display for Decimal {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 let sign = if self.units < 0 { "-" } else { "" };
133 let digits = self.units.unsigned_abs().to_string();
134 let scale = usize::from(self.scale);
135 if scale == 0 {
136 return write!(f, "{sign}{digits}");
137 }
138 let digits = format!("{digits:0>width$}", width = scale + 1);
139 let (int, frac) = digits.split_at(digits.len() - scale);
140 write!(f, "{sign}{int}.{frac}")
141 }
142}
143
144impl Serialize for Decimal {
145 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
146 serializer.collect_str(self)
147 }
148}
149
150impl<'de> Deserialize<'de> for Decimal {
151 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
152 String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn d(s: &str) -> Decimal {
161 s.parse().unwrap()
162 }
163
164 #[test]
165 fn parses_and_prints_its_one_form() {
166 for s in ["0", "12", "100", "-12.3", "0.05", "-0.5", "1.799", "1234567.891"] {
167 assert_eq!(d(s).to_string(), s);
168 }
169 }
170
171 #[test]
172 fn rejects_every_other_form() {
173 for s in ["", "-", ".5", "5.", "1e3", "+1", "1,5", " 1", "0.1234567890123456789"] {
174 assert!(s.parse::<Decimal>().is_err(), "{s:?}");
175 }
176 for s in ["1.50", "1.0", "01.5", "00", "-0", "-0.0", "0.10"] {
177 assert!(s.parse::<Decimal>().is_err(), "{s:?} is not in its one form");
178 }
179 }
180
181 #[test]
182 fn construction_reduces_to_the_one_form() {
183 assert_eq!(Decimal::new(150, 2).unwrap(), d("1.5"));
184 assert_eq!(Decimal::new(150, 2).unwrap().scale(), 1);
185 assert_eq!(Decimal::new(0, 5).unwrap(), Decimal::ZERO);
186 assert_eq!(-Decimal::ZERO, Decimal::ZERO);
187 }
188
189 #[test]
190 fn compares_by_value() {
191 assert!(d("-0.01") < d("0"));
192 assert!(d("2") > d("1.99"));
193 }
194
195 #[test]
196 fn adds_and_subtracts_exactly() {
197 assert_eq!(d("0.1").checked_add(d("0.2")).unwrap(), d("0.3"));
198 assert_eq!(d("0.25").checked_add(d("0.25")).unwrap().to_string(), "0.5");
199 assert_eq!(d("10").checked_sub(d("0.01")).unwrap().to_string(), "9.99");
200 assert!(Decimal::new(i128::MAX, 0).unwrap().checked_add(d("1")).is_none());
201 }
202
203 #[test]
204 fn is_a_string_in_json() {
205 assert_eq!(serde_json::to_string(&d("-12.3")).unwrap(), r#""-12.3""#);
206 assert_eq!(serde_json::from_str::<Decimal>(r#""4.2""#).unwrap(), d("4.2"));
207 assert!(serde_json::from_str::<Decimal>(r#""4.20""#).is_err());
208 assert!(serde_json::from_str::<Decimal>("4.2").is_err());
209 }
210}