1pub fn scaled_i128_to_decimal_str(value: i128, scale: i8) -> String {
35 if scale < 0 {
36 let factor = 10i128.pow(scale.unsigned_abs() as u32);
37 return (value * factor).to_string();
38 }
39 let scale_u = scale as u32;
40 if scale_u == 0 {
41 return value.to_string();
42 }
43 let factor = 10i128.pow(scale_u);
44 let negative = value < 0;
45 let abs = value.abs();
46 let int_part = abs / factor;
47 let frac = abs % factor;
48 format!(
49 "{sign}{int_part}.{frac:0width$}",
50 sign = if negative { "-" } else { "" },
51 width = scale_u as usize
52 )
53}
54
55pub fn decimal_str_to_scaled_i128(s: &str, scale: i8) -> Option<i128> {
56 let s = s.trim();
57 if s.is_empty() {
58 return None;
59 }
60
61 let negative = s.starts_with('-');
62 let s = if negative {
63 &s[1..]
64 } else {
65 s.trim_start_matches('+')
66 };
67
68 if scale < 0 {
69 let divisor = 10i128.pow(scale.unsigned_abs() as u32);
72 let int_val: i128 = s.split('.').next()?.trim().parse().ok()?;
73 let result = int_val.checked_div(divisor)?;
74 return Some(if negative { -result } else { result });
75 }
76
77 let scale_u = scale as u32;
78
79 let (int_part, frac_part) = if let Some(dot) = s.find('.') {
81 (&s[..dot], &s[dot + 1..])
82 } else {
83 (s, "")
84 };
85
86 let int_val: i128 = if int_part.is_empty() {
87 0
88 } else {
89 int_part.parse().ok()?
90 };
91
92 let frac_aligned: i128 = if scale_u == 0 {
93 0
94 } else if frac_part.len() < scale_u as usize {
95 let mut buf = String::with_capacity(scale_u as usize);
97 buf.push_str(frac_part);
98 for _ in 0..(scale_u as usize - frac_part.len()) {
99 buf.push('0');
100 }
101 buf.parse().ok()?
102 } else {
103 frac_part[..scale_u as usize].parse().ok()?
108 };
109
110 let scale_factor = 10i128.pow(scale_u);
111 let result = int_val
112 .checked_mul(scale_factor)?
113 .checked_add(frac_aligned)?;
114 Some(if negative { -result } else { result })
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn scaled_to_str_roundtrip_financial() {
123 assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
124 assert_eq!(scaled_i128_to_decimal_str(12345, 2), "123.45");
125 assert_eq!(scaled_i128_to_decimal_str(-123, 2), "-1.23");
126 assert_eq!(scaled_i128_to_decimal_str(10_123_456, 6), "10.123456");
127 }
128
129 #[test]
130 fn standard_financial_values() {
131 assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
132 assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
133 assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
134 assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
135 assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
136 assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
137 }
138
139 #[test]
141 fn golden_test_payment_values() {
142 let rows = [
143 ("0.10", 10i128),
144 ("0.20", 20),
145 ("999999999999.99", 99999999999999),
146 ("-100.05", -10005),
147 ];
148 let sum: i128 = rows.iter().map(|(_, v)| v).sum();
149 assert_eq!(sum, 99999999990024);
152
153 for (s, expected) in &rows {
154 assert_eq!(
155 decimal_str_to_scaled_i128(s, 2),
156 Some(*expected),
157 "mismatch for '{s}'"
158 );
159 }
160 }
161
162 #[test]
163 fn integer_valued_decimal_with_nonzero_scale() {
164 assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
165 assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
166 }
167
168 #[test]
169 fn frac_shorter_than_scale_is_right_padded() {
170 assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
172 assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
174 }
175
176 #[test]
177 fn negative_scale_represents_large_round_numbers() {
178 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
180 assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
181 }
182
183 #[test]
184 fn zero_scale_ignores_fractional_digits() {
185 assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
186 assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
187 }
188
189 #[test]
190 fn null_like_empty_string_returns_none() {
191 assert_eq!(decimal_str_to_scaled_i128("", 2), None);
192 assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
193 }
194
195 #[test]
196 fn non_numeric_string_returns_none() {
197 assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
198 assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
199 }
200
201 #[test]
202 fn large_precision_near_i128_boundary() {
203 let big = "999999999999999999"; assert_eq!(
207 decimal_str_to_scaled_i128(big, 0),
208 Some(999_999_999_999_999_999i128)
209 );
210 }
211}