#[inline]
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "Intentional for parsing, value range validated"
)]
fn clamp_precision_with_log(len: usize, context: &str, input: &str) -> u8 {
if len > u8::MAX as usize {
log::debug!(
"{} precision clamped from {} to {} for input: {}",
context,
len,
u8::MAX,
input
);
}
len.min(u8::MAX as usize) as u8
}
fn precision_from_scientific(s: &str, trim_trailing_zeros: bool, strict: bool) -> Option<u8> {
let e_pos = s.find('e')?;
let mantissa = &s[..e_pos];
let exponent_str = &s[e_pos + 1..];
let frac_digits = mantissa.split_once('.').map_or(0, |(_, frac)| {
if trim_trailing_zeros {
frac.trim_end_matches('0').len()
} else {
frac.len()
}
});
let exponent: i64 = if let Ok(v) = exponent_str.parse::<i64>() {
v
} else {
let (digits, is_negative) = exponent_str
.strip_prefix('-')
.map(|rest| (rest, true))
.or_else(|| exponent_str.strip_prefix('+').map(|rest| (rest, false)))
.unwrap_or((exponent_str, false));
if digits.is_empty() {
assert!(
!strict,
"Invalid scientific notation format: missing exponent value"
);
return None;
}
if digits.chars().all(|c| c.is_ascii_digit()) {
return Some(if is_negative { u8::MAX } else { 0 });
}
assert!(
!strict,
"Invalid scientific notation exponent '{exponent_str}': must be a valid number"
);
return None;
};
let precision = i64::try_from(frac_digits)
.unwrap_or(i64::MAX)
.saturating_sub(exponent)
.clamp(0, i64::from(u8::MAX));
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "clamped to 0..=u8::MAX above"
)]
let precision = precision as u8;
Some(precision)
}
#[must_use]
pub fn precision_from_str(s: &str) -> u8 {
let s = s.trim().to_ascii_lowercase();
if s.contains('e') {
return precision_from_scientific(&s, false, true)
.expect("precision_from_scientific should return Some in strict mode");
}
if let Some((_, decimal_part)) = s.split_once('.') {
clamp_precision_with_log(decimal_part.len(), "Decimal", &s)
} else {
0
}
}
#[must_use]
pub fn min_increment_precision_from_str(s: &str) -> u8 {
let s = s.trim().to_ascii_lowercase();
if s.contains('e') {
return precision_from_scientific(&s, true, false).unwrap_or(0);
}
if let Some(dot_pos) = s.find('.') {
let decimal_part = &s[dot_pos + 1..];
if decimal_part.chars().any(|c| c != '0') {
let trimmed_len = decimal_part.trim_end_matches('0').len();
return clamp_precision_with_log(trimmed_len, "Minimum increment", &s);
}
clamp_precision_with_log(decimal_part.len(), "Decimal", &s)
} else {
0
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("", 0)]
#[case("0", 0)]
#[case("1.0", 1)]
#[case("1.00", 2)]
#[case("1.23456789", 8)]
#[case("123456.789101112", 9)]
#[case("0.000000001", 9)]
#[case("1e-1", 1)]
#[case("1e-2", 2)]
#[case("1e-3", 3)]
#[case("1e8", 0)]
#[case("-1.23", 2)]
#[case("-1e-2", 2)]
#[case("1E-2", 2)]
#[case("1.5e-2", 3)]
#[case("1.23e-2", 4)]
#[case("1.5e2", 0)]
#[case("1.5E2", 0)]
#[case("1.5e+2", 0)]
#[case("1.5e0", 1)]
#[case("1.5e-1", 2)]
#[case(" 1.23", 2)]
#[case("1.23 ", 2)]
fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) {
let result = precision_from_str(s);
assert_eq!(result, expected);
}
#[rstest]
#[case("", 0)]
#[case("0", 0)]
#[case("1.0", 1)]
#[case("1.00", 2)]
#[case("1.23456789", 8)]
#[case("123456.789101112", 9)]
#[case("0.000000001", 9)]
#[case("1e-1", 1)]
#[case("1e-2", 2)]
#[case("1e-3", 3)]
#[case("1e8", 0)]
#[case("-1.23", 2)]
#[case("-1e-2", 2)]
#[case("1E-2", 2)]
#[case("1.5e-2", 3)]
#[case("1.23e-2", 4)]
#[case("1.5e2", 0)]
#[case("1.50e-2", 3)]
#[case("1.0e-2", 2)]
#[case(" 1.23", 2)]
#[case("1.23 ", 2)]
#[case("1.010", 2)]
#[case("1.00100", 3)]
#[case("0.0001000", 4)]
#[case("1.000000000", 9)]
fn test_min_increment_precision_from_str(#[case] s: &str, #[case] expected: u8) {
let result = min_increment_precision_from_str(s);
assert_eq!(result, expected);
}
#[rstest]
fn test_precision_from_str_large_exponent_clamped() {
let result = precision_from_str("1e-999");
assert_eq!(result, 255);
}
#[rstest]
fn test_precision_from_str_very_large_exponent_clamped() {
let result = precision_from_str("1e-300");
assert_eq!(result, 255);
let result = precision_from_str("1e-1000000");
assert_eq!(result, 255);
}
#[rstest]
#[should_panic(expected = "Invalid scientific notation exponent")]
fn test_precision_from_str_invalid_exponent_not_numeric() {
let _ = precision_from_str("1e-abc");
}
#[rstest]
#[should_panic(expected = "missing exponent value")]
fn test_precision_from_str_malformed_scientific_notation() {
let _ = precision_from_str("1e-");
}
#[rstest]
fn test_precision_from_str_edge_case_max_u8() {
let result = precision_from_str("1e-255");
assert_eq!(result, 255);
}
#[rstest]
fn test_precision_from_str_just_above_max_u8() {
let result = precision_from_str("1e-256");
assert_eq!(result, 255);
}
#[rstest]
fn test_precision_from_str_u32_overflow() {
let result = precision_from_str("1e-4294967296");
assert_eq!(result, 255);
}
#[rstest]
fn test_precision_from_str_u64_overflow() {
let result = precision_from_str("1e-99999999999999999999");
assert_eq!(result, 255);
}
#[rstest]
fn test_min_increment_precision_from_str_large_exponent() {
let result = min_increment_precision_from_str("1e-300");
assert_eq!(result, 255);
}
#[rstest]
fn test_min_increment_precision_from_str_very_large_exponent() {
let result = min_increment_precision_from_str("1e-99999999999999999999");
assert_eq!(result, 255);
}
#[rstest]
fn test_min_increment_precision_from_str_consistency() {
let input = "1e-1000";
let precision = precision_from_str(input);
let min_precision = min_increment_precision_from_str(input);
assert_eq!(precision, min_precision);
assert_eq!(precision, 255);
}
#[rstest]
fn test_precision_from_str_i64_min_exponent_clamped() {
let result = precision_from_str("1e-9223372036854775808");
assert_eq!(result, 255);
}
#[rstest]
fn test_min_increment_precision_from_str_empty_exponent() {
let result = min_increment_precision_from_str("1e-");
assert_eq!(result, 0);
}
}