1use super::*;
2
3#[derive(Debug, PartialEq, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)]
4pub struct Decimal {
5 pub value: u128,
6 pub scale: u8,
7}
8
9impl Decimal {
10 pub fn to_integer(self, divisibility: u8) -> Result<u128> {
11 match divisibility.checked_sub(self.scale) {
12 Some(difference) => Ok(
13 self
14 .value
15 .checked_mul(
16 10u128
17 .checked_pow(u32::from(difference))
18 .context("divisibility out of range")?,
19 )
20 .context("amount out of range")?,
21 ),
22 None => bail!("excessive precision"),
23 }
24 }
25}
26
27impl Display for Decimal {
28 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
29 let magnitude = 10u128.checked_pow(self.scale.into()).ok_or(fmt::Error)?;
30
31 let integer = self.value / magnitude;
32 let mut fraction = self.value % magnitude;
33
34 write!(f, "{integer}")?;
35
36 if fraction > 0 {
37 let mut width = self.scale.into();
38
39 while fraction % 10 == 0 {
40 fraction /= 10;
41 width -= 1;
42 }
43
44 write!(f, ".{fraction:0>width$}", width = width)?;
45 }
46
47 Ok(())
48 }
49}
50
51impl FromStr for Decimal {
52 type Err = Error;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 if let Some((integer, decimal)) = s.split_once('.') {
56 if integer.is_empty() && decimal.is_empty() {
57 bail!("empty decimal");
58 }
59
60 let integer = if integer.is_empty() {
61 0
62 } else {
63 integer.parse::<u128>()?
64 };
65
66 let (decimal, scale) = if decimal.is_empty() {
67 (0, 0)
68 } else {
69 let trailing_zeros = decimal.chars().rev().take_while(|c| *c == '0').count();
70 let significant_digits = decimal.chars().count() - trailing_zeros;
71 let decimal = decimal.parse::<u128>()?
72 / 10u128
73 .checked_pow(u32::try_from(trailing_zeros).unwrap())
74 .context("excessive trailing zeros")?;
75 (decimal, u8::try_from(significant_digits).unwrap())
76 };
77
78 Ok(Self {
79 value: integer * 10u128.pow(u32::from(scale)) + decimal,
80 scale,
81 })
82 } else {
83 Ok(Self {
84 value: s.parse::<u128>()?,
85 scale: 0,
86 })
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn from_str() {
97 #[track_caller]
98 fn case(s: &str, value: u128, scale: u8) {
99 assert_eq!(s.parse::<Decimal>().unwrap(), Decimal { value, scale });
100 }
101
102 assert_eq!(
103 ".".parse::<Decimal>().unwrap_err().to_string(),
104 "empty decimal",
105 );
106
107 assert_eq!(
108 "a.b".parse::<Decimal>().unwrap_err().to_string(),
109 "invalid digit found in string",
110 );
111
112 assert_eq!(
113 " 0.1 ".parse::<Decimal>().unwrap_err().to_string(),
114 "invalid digit found in string",
115 );
116
117 case("0", 0, 0);
118 case("0.00000", 0, 0);
119 case("1.0", 1, 0);
120 case("1.1", 11, 1);
121 case("1.11", 111, 2);
122 case("1.", 1, 0);
123 case(".1", 1, 1);
124 case("1.10", 11, 1);
125 }
126
127 #[test]
128 fn to_amount() {
129 #[track_caller]
130 fn case(s: &str, divisibility: u8, amount: u128) {
131 assert_eq!(
132 s.parse::<Decimal>()
133 .unwrap()
134 .to_integer(divisibility)
135 .unwrap(),
136 amount,
137 );
138 }
139
140 assert_eq!(
141 Decimal { value: 0, scale: 0 }
142 .to_integer(255)
143 .unwrap_err()
144 .to_string(),
145 "divisibility out of range"
146 );
147
148 assert_eq!(
149 Decimal {
150 value: u128::MAX,
151 scale: 0,
152 }
153 .to_integer(1)
154 .unwrap_err()
155 .to_string(),
156 "amount out of range",
157 );
158
159 assert_eq!(
160 Decimal { value: 1, scale: 1 }
161 .to_integer(0)
162 .unwrap_err()
163 .to_string(),
164 "excessive precision",
165 );
166
167 case("1", 0, 1);
168 case("1.0", 0, 1);
169 case("1.0", 1, 10);
170 case("1.2", 1, 12);
171 case("1.2", 2, 120);
172 case("123.456", 3, 123456);
173 case("123.456", 6, 123456000);
174 }
175
176 #[test]
177 fn to_string() {
178 #[track_caller]
179 fn case(decimal: Decimal, string: &str) {
180 assert_eq!(decimal.to_string(), string);
181 assert_eq!(decimal, string.parse::<Decimal>().unwrap());
182 }
183
184 case(Decimal { value: 0, scale: 0 }, "0");
185 case(Decimal { value: 1, scale: 0 }, "1");
186 case(Decimal { value: 1, scale: 1 }, "0.1");
187 case(
188 Decimal {
189 value: 101,
190 scale: 2,
191 },
192 "1.01",
193 );
194 case(
195 Decimal {
196 value: 1234,
197 scale: 6,
198 },
199 "0.001234",
200 );
201 case(
202 Decimal {
203 value: 12,
204 scale: 0,
205 },
206 "12",
207 );
208 case(
209 Decimal {
210 value: 12,
211 scale: 1,
212 },
213 "1.2",
214 );
215 case(
216 Decimal {
217 value: 12,
218 scale: 2,
219 },
220 "0.12",
221 );
222 case(
223 Decimal {
224 value: 123456,
225 scale: 3,
226 },
227 "123.456",
228 );
229 case(
230 Decimal {
231 value: 123456789,
232 scale: 6,
233 },
234 "123.456789",
235 );
236 }
237}