pub const MAX_DECIMAL_SCALE: u8 = 18;
#[inline]
pub const fn pow10(scale: u8) -> i64 {
debug_assert!(scale <= MAX_DECIMAL_SCALE);
10i64.pow(scale as u32)
}
pub fn parse_decimal_text(s: &str) -> Option<(i128, u8)> {
let (neg, body) = match s.as_bytes().first()? {
b'-' => (true, &s[1..]),
b'+' => (false, &s[1..]),
_ => (false, s),
};
let (int, frac) = body.split_once('.').unwrap_or((body, ""));
if int.is_empty() && frac.is_empty() || frac.len() > 38 {
return None;
}
let mut v: i128 = 0;
for b in int.bytes().chain(frac.bytes()) {
v = v.checked_mul(10)?.checked_add((b as char).to_digit(10)?.into())?;
}
Some((if neg { -v } else { v }, frac.len() as u8))
}
pub fn decimal_of_number_text(s: &str) -> Option<(i128, u8)> {
let (mantissa, exp) = match s.split_once(['e', 'E']) {
Some((m, e)) => (m, e.parse::<i32>().ok()?),
None => (s, 0),
};
let (mut v, scale) = parse_decimal_text(mantissa)?;
let mut scale = i32::from(scale).checked_sub(exp)?;
if scale < 0 {
v = v.checked_mul(10i128.checked_pow(scale.unsigned_abs())?)?;
scale = 0;
}
while scale > 0 && v % 10 == 0 {
v /= 10;
scale -= 1;
}
Some((v, u8::try_from(scale).ok().filter(|s| *s <= 38)?))
}
pub fn format_decimal(v: i128, scale: u8) -> String {
let scale = scale as usize;
let digits = v.unsigned_abs().to_string();
let mut out = String::with_capacity(digits.len().max(scale + 1) + 2);
if v < 0 {
out.push('-');
}
for _ in digits.len()..=scale {
out.push('0');
}
out.push_str(&digits);
if scale > 0 {
out.insert(out.len() - scale, '.');
}
out
}
#[cfg(test)]
#[path = "tests/decimal.rs"]
mod tests;