use arrow::datatypes::i256;
pub fn scaled_i128_to_decimal_str(value: i128, scale: i8) -> String {
if scale < 0 {
let factor = 10i128.pow(scale.unsigned_abs() as u32);
return (value * factor).to_string();
}
let scale_u = scale as u32;
if scale_u == 0 {
return value.to_string();
}
let factor = 10i128.pow(scale_u);
let negative = value < 0;
let abs = value.abs();
let int_part = abs / factor;
let frac = abs % factor;
format!(
"{sign}{int_part}.{frac:0width$}",
sign = if negative { "-" } else { "" },
width = scale_u as usize
)
}
pub fn decimal_str_to_scaled_i128(s: &str, scale: i8) -> Option<i128> {
let s = s.trim();
if s.is_empty() {
return None;
}
let negative = s.starts_with('-');
let s = if negative {
&s[1..]
} else {
s.trim_start_matches('+')
};
if scale < 0 {
let divisor = 10i128.pow(scale.unsigned_abs() as u32);
let int_val: i128 = s.split('.').next()?.trim().parse().ok()?;
let result = int_val.checked_div(divisor)?;
return Some(if negative { -result } else { result });
}
let scale_u = scale as u32;
let (int_part, frac_part) = if let Some(dot) = s.find('.') {
(&s[..dot], &s[dot + 1..])
} else {
(s, "")
};
let int_val: i128 = if int_part.is_empty() {
0
} else {
int_part.parse().ok()?
};
let frac_aligned: i128 = if scale_u == 0 {
0
} else if frac_part.len() < scale_u as usize {
let mut buf = String::with_capacity(scale_u as usize);
buf.push_str(frac_part);
for _ in 0..(scale_u as usize - frac_part.len()) {
buf.push('0');
}
buf.parse().ok()?
} else {
frac_part[..scale_u as usize].parse().ok()?
};
let scale_factor = 10i128.pow(scale_u);
let result = int_val
.checked_mul(scale_factor)?
.checked_add(frac_aligned)?;
Some(if negative { -result } else { result })
}
pub fn decimal_str_to_scaled_i256(s: &str, scale: i8) -> Option<i256> {
let s = s.trim();
if s.is_empty() {
return None;
}
let negative = s.starts_with('-');
let s = if negative {
&s[1..]
} else {
s.trim_start_matches('+')
};
if scale < 0 {
let divisor = pow10_i256(scale.unsigned_abs() as u32)?;
let int_val = i256::from_string(s.split('.').next()?.trim())?;
let result = int_val.checked_div(divisor)?;
return Some(if negative { -result } else { result });
}
let scale_u = scale as u32;
let (int_part, frac_part) = match s.find('.') {
Some(dot) => (&s[..dot], &s[dot + 1..]),
None => (s, ""),
};
let int_val = if int_part.is_empty() {
i256::ZERO
} else {
i256::from_string(int_part)?
};
let frac_aligned = if scale_u == 0 {
i256::ZERO
} else if frac_part.len() < scale_u as usize {
let mut buf = String::with_capacity(scale_u as usize);
buf.push_str(frac_part);
for _ in 0..(scale_u as usize - frac_part.len()) {
buf.push('0');
}
i256::from_string(&buf)?
} else {
i256::from_string(&frac_part[..scale_u as usize])?
};
let scale_factor = pow10_i256(scale_u)?;
let result = int_val
.checked_mul(scale_factor)?
.checked_add(frac_aligned)?;
Some(if negative { -result } else { result })
}
pub fn scale_int_to_i256(v: i128, scale: i8) -> Option<i256> {
if scale < 0 {
return None;
}
i256::from_i128(v).checked_mul(pow10_i256(scale as u32)?)
}
fn pow10_i256(n: u32) -> Option<i256> {
i256::from_string(&format!("1{}", "0".repeat(n as usize)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaled_to_str_roundtrip_financial() {
assert_eq!(scaled_i128_to_decimal_str(10, 2), "0.10");
assert_eq!(scaled_i128_to_decimal_str(12345, 2), "123.45");
assert_eq!(scaled_i128_to_decimal_str(-123, 2), "-1.23");
assert_eq!(scaled_i128_to_decimal_str(10_123_456, 6), "10.123456");
}
#[test]
fn standard_financial_values() {
assert_eq!(decimal_str_to_scaled_i128("0.10", 2), Some(10));
assert_eq!(decimal_str_to_scaled_i128("0.20", 2), Some(20));
assert_eq!(decimal_str_to_scaled_i128("0.30", 2), Some(30));
assert_eq!(decimal_str_to_scaled_i128("123.45", 2), Some(12345));
assert_eq!(decimal_str_to_scaled_i128("-1.23", 2), Some(-123));
assert_eq!(decimal_str_to_scaled_i128("-100.05", 2), Some(-10005));
}
#[test]
fn golden_test_payment_values() {
let rows = [
("0.10", 10i128),
("0.20", 20),
("999999999999.99", 99999999999999),
("-100.05", -10005),
];
let sum: i128 = rows.iter().map(|(_, v)| v).sum();
assert_eq!(sum, 99999999990024);
for (s, expected) in &rows {
assert_eq!(
decimal_str_to_scaled_i128(s, 2),
Some(*expected),
"mismatch for '{s}'"
);
}
}
#[test]
fn integer_valued_decimal_with_nonzero_scale() {
assert_eq!(decimal_str_to_scaled_i128("100", 2), Some(10000));
assert_eq!(decimal_str_to_scaled_i128("0", 2), Some(0));
}
#[test]
fn frac_shorter_than_scale_is_right_padded() {
assert_eq!(decimal_str_to_scaled_i128("0.1", 3), Some(100));
assert_eq!(decimal_str_to_scaled_i128("5.4", 6), Some(5_400_000));
}
#[test]
fn negative_scale_represents_large_round_numbers() {
assert_eq!(decimal_str_to_scaled_i128("1200", -2), Some(12));
assert_eq!(decimal_str_to_scaled_i128("50000", -2), Some(500));
}
#[test]
fn zero_scale_ignores_fractional_digits() {
assert_eq!(decimal_str_to_scaled_i128("42", 0), Some(42));
assert_eq!(decimal_str_to_scaled_i128("42.0", 0), Some(42));
}
#[test]
fn null_like_empty_string_returns_none() {
assert_eq!(decimal_str_to_scaled_i128("", 2), None);
assert_eq!(decimal_str_to_scaled_i128(" ", 2), None);
}
#[test]
fn non_numeric_string_returns_none() {
assert_eq!(decimal_str_to_scaled_i128("NaN", 2), None);
assert_eq!(decimal_str_to_scaled_i128("Infinity", 2), None);
}
#[test]
fn large_precision_near_i128_boundary() {
let big = "999999999999999999"; assert_eq!(
decimal_str_to_scaled_i128(big, 0),
Some(999_999_999_999_999_999i128)
);
}
#[test]
fn value_beyond_i128_returns_none_not_panic() {
let too_big = format!("1{}", "0".repeat(40));
assert_eq!(decimal_str_to_scaled_i128(&too_big, 0), None);
let max_digits = "9".repeat(38);
assert!(decimal_str_to_scaled_i128(&max_digits, 0).is_some());
assert_eq!(decimal_str_to_scaled_i128(&max_digits, 2), None);
assert_eq!(
decimal_str_to_scaled_i128(&format!("{max_digits}.5"), 5),
None
);
}
#[test]
fn i256_handles_values_beyond_i128() {
let big = "123456789012345678901234567890123456789012345";
assert_eq!(decimal_str_to_scaled_i128(big, 0), None, "i128 overflows");
assert_eq!(
decimal_str_to_scaled_i256(big, 0).unwrap(),
i256::from_string(big).unwrap()
);
let v = decimal_str_to_scaled_i256("123456789012345678901234567890123456789012.345", 3)
.unwrap();
assert_eq!(
v,
i256::from_string("123456789012345678901234567890123456789012345").unwrap()
);
}
#[test]
fn i256_matches_i128_for_in_range_values() {
for (s, scale) in [("123.45", 2i8), ("-1.23", 2), ("0.10", 2), ("1200", -2)] {
let small = decimal_str_to_scaled_i128(s, scale).unwrap();
assert_eq!(
decimal_str_to_scaled_i256(s, scale).unwrap(),
i256::from_i128(small),
"i256 and i128 must agree for in-range value {s}"
);
}
}
#[test]
fn scale_int_to_i256_scales_beyond_i128() {
assert!(scale_int_to_i256(u64::MAX as i128, 30).is_some());
assert_eq!(scale_int_to_i256(5, 2), Some(i256::from_i128(500)));
assert_eq!(scale_int_to_i256(123, -1), None, "negative scale rejected");
}
}