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> {
185 i256::from_string(&format!("1{}", "0".repeat(n as usize)))
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn scaled_to_str_roundtrip_financial() {
194 assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
195 assert_eq!(scaled_i128_to_decimal_str(12345, 2), "123.45");
196 assert_eq!(scaled_i128_to_decimal_str(-123, 2), "-1.23");
197 assert_eq!(scaled_i128_to_decimal_str(10_123_456, 6), "10.123456");
198 }
199
200 #[test]
201 fn standard_financial_values() {
202 assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
203 assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
204 assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
205 assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
206 assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
207 assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
208 }
209
210 #[test]
212 fn golden_test_payment_values() {
213 let rows = [
214 ("0.10", 10i128),
215 ("0.20", 20),
216 ("999999999999.99", 99999999999999),
217 ("-100.05", -10005),
218 ];
219 let sum: i128 = rows.iter().map(|(_, v)| v).sum();
220 assert_eq!(sum, 99999999990024);
223
224 for (s, expected) in &rows {
225 assert_eq!(
226 decimal_str_to_scaled_i128(s, 2),
227 Some(*expected),
228 "mismatch for '{s}'"
229 );
230 }
231 }
232
233 #[test]
234 fn integer_valued_decimal_with_nonzero_scale() {
235 assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
236 assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
237 }
238
239 #[test]
240 fn frac_shorter_than_scale_is_right_padded() {
241 assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
243 assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
245 }
246
247 #[test]
248 fn negative_scale_represents_large_round_numbers() {
249 assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
251 assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
252 }
253
254 #[test]
255 fn zero_scale_ignores_fractional_digits() {
256 assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
257 assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
258 }
259
260 #[test]
261 fn null_like_empty_string_returns_none() {
262 assert_eq!(decimal_str_to_scaled_i128("", 2), None);
263 assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
264 }
265
266 #[test]
267 fn non_numeric_string_returns_none() {
268 assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
269 assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
270 }
271
272 #[test]
273 fn large_precision_near_i128_boundary() {
274 let big = "999999999999999999"; assert_eq!(
278 decimal_str_to_scaled_i128(big, 0),
279 Some(999_999_999_999_999_999i128)
280 );
281 }
282
283 #[test]
288 fn value_beyond_i128_returns_none_not_panic() {
289 let too_big = format!("1{}", "0".repeat(40));
291 assert_eq!(decimal_str_to_scaled_i128(&too_big, 0), None);
292
293 let max_digits = "9".repeat(38);
295 assert!(decimal_str_to_scaled_i128(&max_digits, 0).is_some());
296 assert_eq!(decimal_str_to_scaled_i128(&max_digits, 2), None);
298
299 assert_eq!(
301 decimal_str_to_scaled_i128(&format!("{max_digits}.5"), 5),
302 None
303 );
304 }
305
306 #[test]
309 fn i256_handles_values_beyond_i128() {
310 let big = "123456789012345678901234567890123456789012345";
312 assert_eq!(decimal_str_to_scaled_i128(big, 0), None, "i128 overflows");
313 assert_eq!(
314 decimal_str_to_scaled_i256(big, 0).unwrap(),
315 i256::from_string(big).unwrap()
316 );
317 let v = decimal_str_to_scaled_i256("123456789012345678901234567890123456789012.345", 3)
319 .unwrap();
320 assert_eq!(
321 v,
322 i256::from_string("123456789012345678901234567890123456789012345").unwrap()
323 );
324 }
325
326 #[test]
327 fn i256_matches_i128_for_in_range_values() {
328 for (s, scale) in [("123.45", 2i8), ("-1.23", 2), ("0.10", 2), ("1200", -2)] {
329 let small = decimal_str_to_scaled_i128(s, scale).unwrap();
330 assert_eq!(
331 decimal_str_to_scaled_i256(s, scale).unwrap(),
332 i256::from_i128(small),
333 "i256 and i128 must agree for in-range value {s}"
334 );
335 }
336 }
337
338 #[test]
339 fn scale_int_to_i256_scales_beyond_i128() {
340 assert!(scale_int_to_i256(u64::MAX as i128, 30).is_some());
342 assert_eq!(scale_int_to_i256(5, 2), Some(i256::from_i128(500)));
343 assert_eq!(scale_int_to_i256(123, -1), None, "negative scale rejected");
344 }
345}