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 let keep = frac_part.get(..scale_u as usize)?;
115 let dropped = frac_part.get(scale_u as usize..)?;
116 if dropped.bytes().any(|b| b != b'0') {
117 return None;
118 }
119 keep.parse().ok()?
120 };
121
122 let scale_factor = 10i128.pow(scale_u);
123 let result = int_val
124 .checked_mul(scale_factor)?
125 .checked_add(frac_aligned)?;
126 Some(if negative { -result } else { result })
127}
128
129pub fn decimal_str_to_scaled_i256(s: &str, scale: i8) -> Option<i256> {
133 let s = s.trim();
134 if s.is_empty() {
135 return None;
136 }
137 let negative = s.starts_with('-');
138 let s = if negative {
139 &s[1..]
140 } else {
141 s.trim_start_matches('+')
142 };
143
144 if scale < 0 {
145 let divisor = pow10_i256(scale.unsigned_abs() as u32)?;
146 let int_val = i256::from_string(s.split('.').next()?.trim())?;
147 let result = int_val.checked_div(divisor)?;
148 return Some(if negative { -result } else { result });
149 }
150
151 let scale_u = scale as u32;
152 let (int_part, frac_part) = match s.find('.') {
153 Some(dot) => (&s[..dot], &s[dot + 1..]),
154 None => (s, ""),
155 };
156 let int_val = if int_part.is_empty() {
157 i256::ZERO
158 } else {
159 i256::from_string(int_part)?
160 };
161 let frac_aligned = if scale_u == 0 {
162 i256::ZERO
163 } else if frac_part.len() < scale_u as usize {
164 let mut buf = String::with_capacity(scale_u as usize);
165 buf.push_str(frac_part);
166 for _ in 0..(scale_u as usize - frac_part.len()) {
167 buf.push('0');
168 }
169 i256::from_string(&buf)?
170 } else {
171 let keep = frac_part.get(..scale_u as usize)?;
175 let dropped = frac_part.get(scale_u as usize..)?;
176 if dropped.bytes().any(|b| b != b'0') {
177 return None;
178 }
179 i256::from_string(keep)?
180 };
181
182 let scale_factor = pow10_i256(scale_u)?;
183 let result = int_val
184 .checked_mul(scale_factor)?
185 .checked_add(frac_aligned)?;
186 Some(if negative { -result } else { result })
187}
188
189pub fn scale_int_to_i256(v: i128, scale: i8) -> Option<i256> {
193 if scale < 0 {
194 return None;
195 }
196 i256::from_i128(v).checked_mul(pow10_i256(scale as u32)?)
197}
198
199fn pow10_i256(n: u32) -> Option<i256> {
204 let ten = i256::from_i128(10);
205 let mut acc = i256::from_i128(1);
206 for _ in 0..n {
207 acc = acc.checked_mul(ten)?;
208 }
209 Some(acc)
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn scaled_to_str_roundtrip_financial() {
218 assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
219 assert_eq!(scaled_i128_to_decimal_str(12345, 2), "123.45");
220 assert_eq!(scaled_i128_to_decimal_str(-123, 2), "-1.23");
221 assert_eq!(scaled_i128_to_decimal_str(10_123_456, 6), "10.123456");
222 }
223
224 #[test]
225 fn scaled_zero_formats_unsigned() {
226 assert_eq!(scaled_i128_to_decimal_str(0, 2), "0.00");
230 assert_eq!(scaled_i128_to_decimal_str(0, 0), "0");
231 assert_eq!(scaled_i128_to_decimal_str(0, 6), "0.000000");
232 }
233
234 #[test]
235 fn standard_financial_values() {
236 assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
237 assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
238 assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
239 assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
240 assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
241 assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
242 }
243
244 #[test]
246 fn golden_test_payment_values() {
247 let rows = [
248 ("0.10", 10i128),
249 ("0.20", 20),
250 ("999999999999.99", 99999999999999),
251 ("-100.05", -10005),
252 ];
253 let sum: i128 = rows.iter().map(|(_, v)| v).sum();
254 assert_eq!(sum, 99999999990024);
257
258 for (s, expected) in &rows {
259 assert_eq!(
260 decimal_str_to_scaled_i128(s, 2),
261 Some(*expected),
262 "mismatch for '{s}'"
263 );
264 }
265 }
266
267 #[test]
268 fn integer_valued_decimal_with_nonzero_scale() {
269 assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
270 assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
271 }
272
273 #[test]
274 fn frac_shorter_than_scale_is_right_padded() {
275 assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
277 assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
279 }
280
281 #[test]
282 fn negative_scale_represents_large_round_numbers() {
283 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
285 assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
286 }
287
288 #[test]
289 fn zero_scale_ignores_fractional_digits() {
290 assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
291 assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
292 }
293
294 #[test]
295 fn null_like_empty_string_returns_none() {
296 assert_eq!(decimal_str_to_scaled_i128("", 2), None);
297 assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
298 }
299
300 #[test]
301 fn non_numeric_string_returns_none() {
302 assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
303 assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
304 }
305
306 #[test]
307 fn lossy_scale_downcast_fails_loudly_lossless_still_ok() {
308 assert_eq!(
313 decimal_str_to_scaled_i128("1.234567", 2),
314 None,
315 "i128 lossy"
316 );
317 assert_eq!(
318 decimal_str_to_scaled_i256("1.234567", 2),
319 None,
320 "i256 lossy"
321 );
322 assert_eq!(decimal_str_to_scaled_i128("1.2300", 2), Some(123));
324 assert_eq!(
325 decimal_str_to_scaled_i256("1.2300", 2),
326 Some(i256::from_i128(123))
327 );
328 assert_eq!(decimal_str_to_scaled_i128("1.23", 2), Some(123));
330 }
331
332 #[test]
333 fn large_precision_near_i128_boundary() {
334 let big = "999999999999999999"; assert_eq!(
338 decimal_str_to_scaled_i128(big, 0),
339 Some(999_999_999_999_999_999i128)
340 );
341 }
342
343 #[test]
348 fn value_beyond_i128_returns_none_not_panic() {
349 let too_big = format!("1{}", "0".repeat(40));
351 assert_eq!(decimal_str_to_scaled_i128(&too_big, 0), None);
352
353 let max_digits = "9".repeat(38);
355 assert!(decimal_str_to_scaled_i128(&max_digits, 0).is_some());
356 assert_eq!(decimal_str_to_scaled_i128(&max_digits, 2), None);
358
359 assert_eq!(
361 decimal_str_to_scaled_i128(&format!("{max_digits}.5"), 5),
362 None
363 );
364 }
365
366 #[test]
369 fn i256_handles_values_beyond_i128() {
370 let big = "123456789012345678901234567890123456789012345";
372 assert_eq!(decimal_str_to_scaled_i128(big, 0), None, "i128 overflows");
373 assert_eq!(
374 decimal_str_to_scaled_i256(big, 0).unwrap(),
375 i256::from_string(big).unwrap()
376 );
377 let v = decimal_str_to_scaled_i256("123456789012345678901234567890123456789012.345", 3)
379 .unwrap();
380 assert_eq!(
381 v,
382 i256::from_string("123456789012345678901234567890123456789012345").unwrap()
383 );
384 }
385
386 #[test]
387 fn i256_matches_i128_for_in_range_values() {
388 for (s, scale) in [("123.45", 2i8), ("-1.23", 2), ("0.10", 2), ("1200", -2)] {
389 let small = decimal_str_to_scaled_i128(s, scale).unwrap();
390 assert_eq!(
391 decimal_str_to_scaled_i256(s, scale).unwrap(),
392 i256::from_i128(small),
393 "i256 and i128 must agree for in-range value {s}"
394 );
395 }
396 }
397
398 #[test]
399 fn scale_int_to_i256_scales_beyond_i128() {
400 assert!(scale_int_to_i256(u64::MAX as i128, 30).is_some());
402 assert_eq!(scale_int_to_i256(5, 2), Some(i256::from_i128(500)));
403 assert_eq!(scale_int_to_i256(123, -1), None, "negative scale rejected");
404 }
405
406 #[test]
407 fn pow10_i256_matches_string_form_and_respects_ceiling() {
408 for n in [0u32, 1, 5, 18, 38, 39, 76] {
412 let expected = i256::from_string(&format!("1{}", "0".repeat(n as usize)));
413 assert_eq!(pow10_i256(n), expected, "10^{n} mismatch");
414 }
415 assert!(pow10_i256(76).is_some(), "10^76 fits i256");
416 assert!(pow10_i256(77).is_none(), "10^77 overflows i256 → None");
417 }
418}
419
420#[cfg(test)]
421mod fuzz {
422 use super::*;
423
424 proptest::proptest! {
425 #![proptest_config(proptest::prelude::ProptestConfig {
426 cases: 256, ..Default::default()
427 })]
428
429 #[test]
432 fn decimal_parsers_total_and_exact(
433 junk in ".{0,60}",
434 int_part in 0u64..1_000_000_000_000,
435 frac in 0u32..10_000,
436 frac_width in 0usize..=6,
441 neg in proptest::prelude::any::<bool>(),
442 ) {
443 let _ = decimal_str_to_scaled_i128(&junk, 4);
444 let _ = decimal_str_to_scaled_i256(&junk, 4);
445 let frac_str = format!("{frac:04}");
446 let frac_used: String = frac_str.chars().cycle().take(frac_width).collect();
447 let s = format!(
448 "{}{}{}{}",
449 if neg { "-" } else { "" },
450 int_part,
451 if frac_width > 0 { "." } else { "" },
452 frac_used
453 );
454 let dropped = frac_used.get(4..).unwrap_or("");
459 if dropped.bytes().any(|b| b != b'0') {
460 proptest::prop_assert_eq!(decimal_str_to_scaled_i128(&s, 4), None);
461 proptest::prop_assert_eq!(decimal_str_to_scaled_i256(&s, 4), None);
462 } else {
463 let aligned: String = frac_used
464 .chars()
465 .chain(std::iter::repeat('0'))
466 .take(4)
467 .collect();
468 let mag = int_part as i128 * 10_000 + aligned.parse::<i128>().unwrap();
469 let expect = if neg { -mag } else { mag };
470 proptest::prop_assert_eq!(decimal_str_to_scaled_i128(&s, 4), Some(expect));
471 proptest::prop_assert_eq!(
472 decimal_str_to_scaled_i256(&s, 4),
473 Some(arrow::datatypes::i256::from_i128(expect))
474 );
475 }
476 }
477
478 #[test]
482 fn scale_int_to_i256_scales_exactly(v in -1_000_000i128..1_000_000, scale in 0i8..10) {
483 let got = scale_int_to_i256(v, scale).expect("in range");
484 let expect = arrow::datatypes::i256::from_i128(v * 10i128.pow(scale as u32));
485 proptest::prop_assert_eq!(got, expect);
486 proptest::prop_assert_eq!(scale_int_to_i256(v, -1), None);
487 }
488 }
489}
490
491#[cfg(test)]
492mod mutant_kills {
493 use super::*;
499
500 #[test]
503 fn negative_scale_parse_keeps_the_sign_and_scales_down() {
504 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
505 assert_eq!(decimal_str_to_scaled_i128("-1200", -2), Some(-12));
506 assert_eq!(decimal_str_to_scaled_i128("-1200.99", -2), Some(-12));
507 assert_eq!(
508 decimal_str_to_scaled_i256("-3400", -2),
509 Some(arrow::datatypes::i256::from_i128(-34))
510 );
511 assert_eq!(decimal_str_to_scaled_i128("-7", 0), Some(-7));
513 assert_eq!(
514 decimal_str_to_scaled_i256("-7", 0),
515 Some(arrow::datatypes::i256::from_i128(-7))
516 );
517 }
518
519 #[test]
523 fn csv_decimal_renderer_is_exact_for_every_scale_shape() {
524 assert_eq!(scaled_i128_to_decimal_str(12, -2), "1200");
526 assert_eq!(scaled_i128_to_decimal_str(-12, -2), "-1200");
527 assert_eq!(scaled_i128_to_decimal_str(-7, 0), "-7");
529 assert_eq!(scaled_i128_to_decimal_str(105, 2), "1.05");
532 assert_eq!(scaled_i128_to_decimal_str(123, 2), "1.23");
533 assert_eq!(scaled_i128_to_decimal_str(-1, 4), "-0.0001");
534 assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
535 }
536}