1use std::cmp::Ordering;
2use std::fmt::Display;
3use std::num::ParseIntError;
4use std::str::FromStr;
5
6use thiserror::Error;
7
8use crate::{Decimal, ScaledInteger};
9
10impl<I, const D: u8> Display for Decimal<I, D>
11where
12 I: ScaledInteger<D>,
13{
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 #[allow(clippy::arithmetic_side_effects)]
17 let integer = self.0 / I::SCALING_FACTOR;
18 #[allow(clippy::arithmetic_side_effects)]
20 let fractional = self.0 % I::SCALING_FACTOR;
21
22 let is_negative = self.0 < I::ZERO;
23 let sign = match is_negative && integer == I::ZERO {
25 true => "-",
26 false => "",
27 };
28 let fractional = match is_negative {
31 true => (!fractional).wrapping_add(&I::ONE),
32 false => fractional,
33 };
34
35 write!(f, "{sign}{integer}.{fractional:0>decimals$}", decimals = D as usize)
36 }
37}
38
39impl<I, const D: u8> FromStr for Decimal<I, D>
40where
41 I: ScaledInteger<D>,
42{
43 type Err = ParseDecimalError<I>;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 if s.is_empty() {
47 return Err(ParseDecimalError::EmptyString);
48 }
49
50 let unsigned_s = s.strip_prefix('-').unwrap_or(s);
52
53 let Some((integer_s, fractional_s)) = unsigned_s.split_once('.') else {
55 let integer = I::from_str(s)?;
58
59 return Decimal::try_from_scaled(integer, 0)
60 .ok_or(ParseDecimalError::Overflow(integer, I::ZERO));
61 };
62 let integer = I::from_str(integer_s)?;
63 let fractional = I::from_str(fractional_s)?;
64
65 let scaled_integer = integer
66 .checked_mul(&I::SCALING_FACTOR)
67 .ok_or(ParseDecimalError::Overflow(integer, fractional))?;
68
69 let fractional_s_len = fractional_s.len();
70 let fractional = match fractional_s_len.cmp(&(D as usize)) {
71 Ordering::Equal => fractional,
72 Ordering::Less => {
73 #[allow(clippy::arithmetic_side_effects)]
75 let shortfall = D as usize - fractional_s_len;
76
77 fractional
79 .checked_mul(&I::pow(I::TEN, shortfall.try_into().unwrap()))
80 .unwrap()
81 }
82 Ordering::Greater => return Err(ParseDecimalError::PrecisionLoss(fractional_s.len())),
83 };
84 let unsigned = scaled_integer
85 .checked_add(&fractional)
86 .ok_or(ParseDecimalError::Overflow(integer, fractional))?;
87
88 Ok(match unsigned_s.len() == s.len() {
90 true => Decimal(unsigned),
91 false => {
92 debug_assert_eq!(unsigned_s.len().checked_add(1).unwrap(), s.len());
93
94 Decimal((!unsigned).wrapping_add(&I::ONE))
95 }
96 })
97 }
98}
99
100#[derive(Debug, PartialEq, Eq, Error)]
101pub enum ParseDecimalError<I>
102where
103 I: Display,
104{
105 #[error("Empty string provided")]
106 EmptyString,
107 #[error("Missing decimal point")]
108 MissingDecimalPoint,
109 #[error("Resultant decimal overflowed; integer={0}; fractional={1}")]
110 Overflow(I, I),
111 #[error("Failed to parse integer; err={0}")]
112 ParseInt(#[from] ParseIntError),
113 #[error("Could not parse without precision loss; decimals={0}")]
114 PrecisionLoss(usize),
115 #[error("The radix must be 10 in `from_str_radix`")]
116 RadixMustBe10,
117}
118
119#[cfg(test)]
120mod tests {
121 use expect_test::expect;
122 use proptest::prelude::Arbitrary;
123 use proptest::proptest;
124 use proptest::test_runner::TestRunner;
125
126 use super::*;
127 use crate::{Int64_9, Uint64_9};
128
129 #[test]
130 fn uint64_9_to_string() {
131 assert_eq!(Uint64_9::ONE.to_string(), "1.000000000");
132 assert_eq!(Uint64_9::try_from_scaled(123, 9).unwrap().to_string(), "0.000000123");
133 assert_eq!(
134 (Uint64_9::ONE + Uint64_9::try_from_scaled(123, 9).unwrap()).to_string(),
135 "1.000000123"
136 );
137 }
138
139 #[test]
140 fn uint64_9_from_str() {
141 assert_eq!("".parse::<Uint64_9>(), Err(ParseDecimalError::EmptyString));
142 expect![[r#"
143 Err(
144 ParseInt(
145 ParseIntError {
146 kind: Empty,
147 },
148 ),
149 )
150 "#]]
151 .assert_debug_eq(&"1.".parse::<Uint64_9>());
152 assert_eq!("1.0".parse::<Uint64_9>(), Ok(Uint64_9::ONE));
153 assert_eq!("10.0".parse::<Uint64_9>(), Ok(Uint64_9::try_from_scaled(10, 0).unwrap()));
154 assert_eq!("10".parse::<Uint64_9>(), Ok(Uint64_9::try_from_scaled(10, 0).unwrap()));
155 assert_eq!("0.1".parse::<Uint64_9>(), Ok(Decimal(10u64.pow(8))));
156 assert_eq!("0.123456789".parse::<Uint64_9>(), Ok(Decimal(123_456_789)));
157 assert_eq!("0.012345678".parse::<Uint64_9>(), Ok(Decimal(12_345_678)));
158 assert_eq!("0.000000001".parse::<Uint64_9>(), Ok(Decimal(1)));
159
160 assert_eq!("0.0000000001".parse::<Uint64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
161 assert_eq!(
162 format!("{}.0", u64::MAX).parse::<Uint64_9>(),
163 Err(ParseDecimalError::Overflow(u64::MAX, 0))
164 );
165 assert_eq!(
166 format!("{}.0", u64::MAX / Uint64_9::SCALING_FACTOR).parse::<Uint64_9>(),
167 Ok(Decimal(u64::MAX / Uint64_9::SCALING_FACTOR * Uint64_9::SCALING_FACTOR))
168 );
169 assert_eq!("18446744073.709551615".to_string().parse::<Uint64_9>(), Ok(Decimal::MAX));
170 assert_eq!(
171 "18446744073.709551616".to_string().parse::<Uint64_9>(),
172 Err(ParseDecimalError::Overflow(18_446_744_073, 709_551_616)),
173 );
174 }
175
176 #[test]
177 fn int64_9_to_string() {
178 assert_eq!(Int64_9::ZERO.to_string(), "0.000000000");
179 assert_eq!(Int64_9::ONE.to_string(), "1.000000000");
180 assert_eq!(Int64_9::try_from_scaled(123, 9).unwrap().to_string(), "0.000000123");
181 assert_eq!(
182 (Int64_9::ONE + Int64_9::try_from_scaled(123, 9).unwrap()).to_string(),
183 "1.000000123"
184 );
185 assert_eq!((-Int64_9::ONE).to_string(), "-1.000000000");
186 assert_eq!((-Int64_9::try_from_scaled(123, 9).unwrap()).to_string(), "-0.000000123");
187 assert_eq!(
188 (-Int64_9::ONE + -Int64_9::try_from_scaled(123, 9).unwrap()).to_string(),
189 "-1.000000123"
190 );
191 }
192
193 #[test]
194 fn signed_min_to_string() {
195 assert_eq!(Decimal::<i8, 1>::MIN.to_string(), "-12.8");
196 }
197
198 #[test]
199 fn int64_9_from_str() {
200 assert_eq!("".parse::<Int64_9>(), Err(ParseDecimalError::EmptyString));
201 expect![[r#"
202 Err(
203 ParseInt(
204 ParseIntError {
205 kind: Empty,
206 },
207 ),
208 )
209 "#]]
210 .assert_debug_eq(&"1.".parse::<Int64_9>());
211 assert_eq!("1.0".parse::<Int64_9>(), Ok(Int64_9::ONE));
212 assert_eq!("0.1".parse::<Int64_9>(), Ok(Decimal(10i64.pow(8))));
213 assert_eq!("0.123456789".parse::<Int64_9>(), Ok(Decimal(123_456_789)));
214 assert_eq!("0.012345678".parse::<Int64_9>(), Ok(Decimal(12_345_678)));
215 assert_eq!("0.000000001".parse::<Int64_9>(), Ok(Decimal(1)));
216 assert_eq!("0.0000000001".parse::<Int64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
217 assert_eq!("-1.0".parse::<Int64_9>(), Ok(-Int64_9::ONE));
218 assert_eq!("-10.0".parse::<Int64_9>(), Ok(Int64_9::try_from_scaled(-10, 0).unwrap()));
219 assert_eq!("-10".parse::<Int64_9>(), Ok(Int64_9::try_from_scaled(-10, 0).unwrap()));
220 assert_eq!("-0.1".parse::<Int64_9>(), Ok(-Decimal(10i64.pow(8))));
221 assert_eq!("-0.123456789".parse::<Int64_9>(), Ok(-Decimal(123_456_789)));
222 assert_eq!("-0.012345678".parse::<Int64_9>(), Ok(-Decimal(12_345_678)));
223 assert_eq!("-0.000000001".parse::<Int64_9>(), Ok(-Decimal(1)));
224 assert_eq!("-0.0000000001".parse::<Int64_9>(), Err(ParseDecimalError::PrecisionLoss(10)));
225 }
226
227 #[test]
231 fn uint64_9_round_trip() {
232 decimal_round_trip::<9, u64>();
233 }
234
235 #[test]
236 fn int64_9_round_trip() {
237 decimal_round_trip::<9, i64>();
238 }
239
240 #[test]
241 fn uint128_18_round_trip() {
242 decimal_round_trip::<9, u64>();
243 }
244
245 #[test]
246 fn int128_18_round_trip() {
247 decimal_round_trip::<9, i64>();
248 }
249
250 fn decimal_round_trip<const D: u8, I>()
251 where
252 I: ScaledInteger<D> + Arbitrary,
253 {
254 let mut runner = TestRunner::default();
255 let input = Decimal::arbitrary();
256
257 runner
258 .run(&input, |decimal: Decimal<I, D>| {
259 let round_trip = decimal.to_string().parse().unwrap();
260
261 assert_eq!(decimal, round_trip);
262
263 Ok(())
264 })
265 .unwrap();
266 }
267
268 #[test]
269 fn uint64_9_parse_no_panic() {
270 decimal_parse_no_panic::<9, u64>();
271 }
272
273 #[test]
274 fn int64_9_parse_no_panic() {
275 decimal_parse_no_panic::<9, i64>();
276 }
277
278 #[test]
279 fn uint128_18_parse_no_panic() {
280 decimal_parse_no_panic::<9, u64>();
281 }
282
283 #[test]
284 fn int128_18_parse_no_panic() {
285 decimal_parse_no_panic::<9, i64>();
286 }
287
288 fn decimal_parse_no_panic<const D: u8, I>()
289 where
290 I: ScaledInteger<D>,
291 {
292 proptest!(|(decimal_s: String)| {
293 let _ = decimal_s.parse::<Decimal<I, D>>();
294 });
295 }
296
297 #[test]
298 fn uint64_9_parse_numeric_no_panic() {
299 decimal_parse_numeric_no_panic::<9, u64>();
300 }
301
302 #[test]
303 fn int64_9_parse_numeric_no_panic() {
304 decimal_parse_numeric_no_panic::<9, i64>();
305 }
306
307 #[test]
308 fn uint128_18_parse_numeric_no_panic() {
309 decimal_parse_numeric_no_panic::<9, u64>();
310 }
311
312 #[test]
313 fn int128_18_parse_numeric_no_panic() {
314 decimal_parse_numeric_no_panic::<9, i64>();
315 }
316
317 fn decimal_parse_numeric_no_panic<const D: u8, I>()
318 where
319 I: ScaledInteger<D>,
320 {
321 proptest!(|(decimal_s in "[0-9]{0,24}\\.[0-9]{0,24}")| {
322 let _ = decimal_s.parse::<Decimal<I, D>>();
323 });
324 }
325}