1pub fn decimal_str_to_scaled_i128(s: &str, scale: i8) -> Option<i128> {
34 let s = s.trim();
35 if s.is_empty() {
36 return None;
37 }
38
39 let negative = s.starts_with('-');
40 let s = if negative {
41 &s[1..]
42 } else {
43 s.trim_start_matches('+')
44 };
45
46 if scale < 0 {
47 let divisor = 10i128.pow(scale.unsigned_abs() as u32);
50 let int_val: i128 = s.split('.').next()?.trim().parse().ok()?;
51 let result = int_val.checked_div(divisor)?;
52 return Some(if negative { -result } else { result });
53 }
54
55 let scale_u = scale as u32;
56
57 let (int_part, frac_part) = if let Some(dot) = s.find('.') {
59 (&s[..dot], &s[dot + 1..])
60 } else {
61 (s, "")
62 };
63
64 let int_val: i128 = if int_part.is_empty() {
65 0
66 } else {
67 int_part.parse().ok()?
68 };
69
70 let frac_aligned: i128 = if scale_u == 0 {
71 0
72 } else if frac_part.len() < scale_u as usize {
73 let mut buf = String::with_capacity(scale_u as usize);
75 buf.push_str(frac_part);
76 for _ in 0..(scale_u as usize - frac_part.len()) {
77 buf.push('0');
78 }
79 buf.parse().ok()?
80 } else {
81 frac_part[..scale_u as usize].parse().ok()?
86 };
87
88 let scale_factor = 10i128.pow(scale_u);
89 let result = int_val
90 .checked_mul(scale_factor)?
91 .checked_add(frac_aligned)?;
92 Some(if negative { -result } else { result })
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn standard_financial_values() {
101 assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
102 assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
103 assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
104 assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
105 assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
106 assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
107 }
108
109 #[test]
111 fn golden_test_payment_values() {
112 let rows = [
113 ("0.10", 10i128),
114 ("0.20", 20),
115 ("999999999999.99", 99999999999999),
116 ("-100.05", -10005),
117 ];
118 let sum: i128 = rows.iter().map(|(_, v)| v).sum();
119 assert_eq!(sum, 99999999990024);
122
123 for (s, expected) in &rows {
124 assert_eq!(
125 decimal_str_to_scaled_i128(s, 2),
126 Some(*expected),
127 "mismatch for '{s}'"
128 );
129 }
130 }
131
132 #[test]
133 fn integer_valued_decimal_with_nonzero_scale() {
134 assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
135 assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
136 }
137
138 #[test]
139 fn frac_shorter_than_scale_is_right_padded() {
140 assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
142 assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
144 }
145
146 #[test]
147 fn negative_scale_represents_large_round_numbers() {
148 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
150 assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
151 }
152
153 #[test]
154 fn zero_scale_ignores_fractional_digits() {
155 assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
156 assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
157 }
158
159 #[test]
160 fn null_like_empty_string_returns_none() {
161 assert_eq!(decimal_str_to_scaled_i128("", 2), None);
162 assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
163 }
164
165 #[test]
166 fn non_numeric_string_returns_none() {
167 assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
168 assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
169 }
170
171 #[test]
172 fn large_precision_near_i128_boundary() {
173 let big = "999999999999999999"; assert_eq!(
177 decimal_str_to_scaled_i128(big, 0),
178 Some(999_999_999_999_999_999i128)
179 );
180 }
181}