1use arrow::datatypes::i256;
14
15pub fn scaled_i128_to_decimal_str(value: i128, scale: i8) -> String {
38 if scale < 0 {
39 let factor = 10i128.pow(scale.unsigned_abs() as u32);
40 return (value * factor).to_string();
41 }
42 let scale_u = scale as u32;
43 if scale_u == 0 {
44 return value.to_string();
45 }
46 let factor = 10i128.pow(scale_u);
47 let negative = value < 0;
48 let abs = value.abs();
49 let int_part = abs / factor;
50 let frac = abs % factor;
51 format!(
52 "{sign}{int_part}.{frac:0width$}",
53 sign = if negative { "-" } else { "" },
54 width = scale_u as usize
55 )
56}
57
58pub fn decimal_str_to_scaled_i128(s: &str, scale: i8) -> Option<i128> {
59 let s = s.trim();
60 if s.is_empty() {
61 return None;
62 }
63
64 let negative = s.starts_with('-');
65 let s = if negative {
66 &s[1..]
67 } else {
68 s.trim_start_matches('+')
69 };
70
71 if scale < 0 {
72 let divisor = 10i128.pow(scale.unsigned_abs() as u32);
75 let int_val: i128 = s.split('.').next()?.trim().parse().ok()?;
76 let result = int_val.checked_div(divisor)?;
77 return Some(if negative { -result } else { result });
78 }
79
80 let scale_u = scale as u32;
81
82 let (int_part, frac_part) = if let Some(dot) = s.find('.') {
84 (&s[..dot], &s[dot + 1..])
85 } else {
86 (s, "")
87 };
88
89 let int_val: i128 = if int_part.is_empty() {
90 0
91 } else {
92 int_part.parse().ok()?
93 };
94
95 let frac_aligned: i128 = if scale_u == 0 {
96 0
97 } else if frac_part.len() < scale_u as usize {
98 let mut buf = String::with_capacity(scale_u as usize);
100 buf.push_str(frac_part);
101 for _ in 0..(scale_u as usize - frac_part.len()) {
102 buf.push('0');
103 }
104 buf.parse().ok()?
105 } else {
106 frac_part[..scale_u as usize].parse().ok()?
111 };
112
113 let scale_factor = 10i128.pow(scale_u);
114 let result = int_val
115 .checked_mul(scale_factor)?
116 .checked_add(frac_aligned)?;
117 Some(if negative { -result } else { result })
118}
119
120pub fn decimal_str_to_scaled_i256(s: &str, scale: i8) -> Option<i256> {
124 let s = s.trim();
125 if s.is_empty() {
126 return None;
127 }
128 let negative = s.starts_with('-');
129 let s = if negative {
130 &s[1..]
131 } else {
132 s.trim_start_matches('+')
133 };
134
135 if scale < 0 {
136 let divisor = pow10_i256(scale.unsigned_abs() as u32)?;
137 let int_val = i256::from_string(s.split('.').next()?.trim())?;
138 let result = int_val.checked_div(divisor)?;
139 return Some(if negative { -result } else { result });
140 }
141
142 let scale_u = scale as u32;
143 let (int_part, frac_part) = match s.find('.') {
144 Some(dot) => (&s[..dot], &s[dot + 1..]),
145 None => (s, ""),
146 };
147 let int_val = if int_part.is_empty() {
148 i256::ZERO
149 } else {
150 i256::from_string(int_part)?
151 };
152 let frac_aligned = if scale_u == 0 {
153 i256::ZERO
154 } else if frac_part.len() < scale_u as usize {
155 let mut buf = String::with_capacity(scale_u as usize);
156 buf.push_str(frac_part);
157 for _ in 0..(scale_u as usize - frac_part.len()) {
158 buf.push('0');
159 }
160 i256::from_string(&buf)?
161 } else {
162 i256::from_string(&frac_part[..scale_u as usize])?
163 };
164
165 let scale_factor = pow10_i256(scale_u)?;
166 let result = int_val
167 .checked_mul(scale_factor)?
168 .checked_add(frac_aligned)?;
169 Some(if negative { -result } else { result })
170}
171
172pub fn scale_int_to_i256(v: i128, scale: i8) -> Option<i256> {
176 if scale < 0 {
177 return None;
178 }
179 i256::from_i128(v).checked_mul(pow10_i256(scale as u32)?)
180}
181
182fn pow10_i256(n: u32) -> Option<i256> {
187 let ten = i256::from_i128(10);
188 let mut acc = i256::from_i128(1);
189 for _ in 0..n {
190 acc = acc.checked_mul(ten)?;
191 }
192 Some(acc)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn scaled_to_str_roundtrip_financial() {
201 assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
202 assert_eq!(scaled_i128_to_decimal_str(12345, 2), "123.45");
203 assert_eq!(scaled_i128_to_decimal_str(-123, 2), "-1.23");
204 assert_eq!(scaled_i128_to_decimal_str(10_123_456, 6), "10.123456");
205 }
206
207 #[test]
208 fn standard_financial_values() {
209 assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
210 assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
211 assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
212 assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
213 assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
214 assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
215 }
216
217 #[test]
219 fn golden_test_payment_values() {
220 let rows = [
221 ("0.10", 10i128),
222 ("0.20", 20),
223 ("999999999999.99", 99999999999999),
224 ("-100.05", -10005),
225 ];
226 let sum: i128 = rows.iter().map(|(_, v)| v).sum();
227 assert_eq!(sum, 99999999990024);
230
231 for (s, expected) in &rows {
232 assert_eq!(
233 decimal_str_to_scaled_i128(s, 2),
234 Some(*expected),
235 "mismatch for '{s}'"
236 );
237 }
238 }
239
240 #[test]
241 fn integer_valued_decimal_with_nonzero_scale() {
242 assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
243 assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
244 }
245
246 #[test]
247 fn frac_shorter_than_scale_is_right_padded() {
248 assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
250 assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
252 }
253
254 #[test]
255 fn negative_scale_represents_large_round_numbers() {
256 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
258 assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
259 }
260
261 #[test]
262 fn zero_scale_ignores_fractional_digits() {
263 assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
264 assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
265 }
266
267 #[test]
268 fn null_like_empty_string_returns_none() {
269 assert_eq!(decimal_str_to_scaled_i128("", 2), None);
270 assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
271 }
272
273 #[test]
274 fn non_numeric_string_returns_none() {
275 assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
276 assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
277 }
278
279 #[test]
280 fn large_precision_near_i128_boundary() {
281 let big = "999999999999999999"; assert_eq!(
285 decimal_str_to_scaled_i128(big, 0),
286 Some(999_999_999_999_999_999i128)
287 );
288 }
289
290 #[test]
295 fn value_beyond_i128_returns_none_not_panic() {
296 let too_big = format!("1{}", "0".repeat(40));
298 assert_eq!(decimal_str_to_scaled_i128(&too_big, 0), None);
299
300 let max_digits = "9".repeat(38);
302 assert!(decimal_str_to_scaled_i128(&max_digits, 0).is_some());
303 assert_eq!(decimal_str_to_scaled_i128(&max_digits, 2), None);
305
306 assert_eq!(
308 decimal_str_to_scaled_i128(&format!("{max_digits}.5"), 5),
309 None
310 );
311 }
312
313 #[test]
316 fn i256_handles_values_beyond_i128() {
317 let big = "123456789012345678901234567890123456789012345";
319 assert_eq!(decimal_str_to_scaled_i128(big, 0), None, "i128 overflows");
320 assert_eq!(
321 decimal_str_to_scaled_i256(big, 0).unwrap(),
322 i256::from_string(big).unwrap()
323 );
324 let v = decimal_str_to_scaled_i256("123456789012345678901234567890123456789012.345", 3)
326 .unwrap();
327 assert_eq!(
328 v,
329 i256::from_string("123456789012345678901234567890123456789012345").unwrap()
330 );
331 }
332
333 #[test]
334 fn i256_matches_i128_for_in_range_values() {
335 for (s, scale) in [("123.45", 2i8), ("-1.23", 2), ("0.10", 2), ("1200", -2)] {
336 let small = decimal_str_to_scaled_i128(s, scale).unwrap();
337 assert_eq!(
338 decimal_str_to_scaled_i256(s, scale).unwrap(),
339 i256::from_i128(small),
340 "i256 and i128 must agree for in-range value {s}"
341 );
342 }
343 }
344
345 #[test]
346 fn scale_int_to_i256_scales_beyond_i128() {
347 assert!(scale_int_to_i256(u64::MAX as i128, 30).is_some());
349 assert_eq!(scale_int_to_i256(5, 2), Some(i256::from_i128(500)));
350 assert_eq!(scale_int_to_i256(123, -1), None, "negative scale rejected");
351 }
352
353 #[test]
354 fn pow10_i256_matches_string_form_and_respects_ceiling() {
355 for n in [0u32, 1, 5, 18, 38, 39, 76] {
359 let expected = i256::from_string(&format!("1{}", "0".repeat(n as usize)));
360 assert_eq!(pow10_i256(n), expected, "10^{n} mismatch");
361 }
362 assert!(pow10_i256(76).is_some(), "10^76 fits i256");
363 assert!(pow10_i256(77).is_none(), "10^77 overflows i256 → None");
364 }
365}