#[derive(Debug, Clone, PartialEq)]
pub enum NumberUtilsError {
InvalidFormat(String),
OutOfRange(String),
DivisionByZero,
}
impl std::fmt::Display for NumberUtilsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NumberUtilsError::InvalidFormat(msg) => write!(f, "Invalid format: {msg}"),
NumberUtilsError::OutOfRange(msg) => write!(f, "Out of range: {msg}"),
NumberUtilsError::DivisionByZero => write!(f, "Division by zero"),
}
}
}
impl std::error::Error for NumberUtilsError {}
pub fn is_finite(n: f64) -> bool {
n.is_finite()
}
pub fn is_nan(n: f64) -> bool {
n.is_nan()
}
pub fn is_integer(n: f64) -> bool {
n.is_finite() && n.fract() == 0.0
}
pub fn is_safe_integer(n: f64) -> bool {
const MAX_SAFE_INTEGER: f64 = 9007199254740991.0; is_integer(n) && n.abs() <= MAX_SAFE_INTEGER
}
pub fn parse_float(s: &str) -> Result<f64, NumberUtilsError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(NumberUtilsError::InvalidFormat("Empty string".to_string()));
}
let mut end_idx = 0;
let mut has_dot = false;
let mut has_e = false;
let chars: Vec<char> = trimmed.chars().collect();
if !chars.is_empty() && (chars[0] == '+' || chars[0] == '-') {
end_idx = 1;
}
while end_idx < chars.len() {
let ch = chars[end_idx];
match ch {
'0'..='9' => end_idx += 1,
'.' if !has_dot && !has_e => {
has_dot = true;
end_idx += 1;
}
'e' | 'E' if !has_e && end_idx > 0 => {
has_e = true;
end_idx += 1;
if end_idx < chars.len() && (chars[end_idx] == '+' || chars[end_idx] == '-') {
end_idx += 1;
}
}
_ => break,
}
}
if end_idx == 0 || (end_idx == 1 && (chars[0] == '+' || chars[0] == '-')) {
return Err(NumberUtilsError::InvalidFormat(
"No valid number found".to_string(),
));
}
let number_str: String = chars[0..end_idx].iter().collect();
number_str
.parse::<f64>()
.map_err(|_| NumberUtilsError::InvalidFormat(format!("Cannot parse: {number_str}")))
}
pub fn parse_int(s: &str, radix: u32) -> Result<i64, NumberUtilsError> {
if !(2..=36).contains(&radix) {
return Err(NumberUtilsError::InvalidFormat(
"Radix must be between 2 and 36".to_string(),
));
}
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(NumberUtilsError::InvalidFormat("Empty string".to_string()));
}
let chars: Vec<char> = trimmed.chars().collect();
let mut start_idx = 0;
let mut is_negative = false;
if !chars.is_empty() {
match chars[0] {
'-' => {
is_negative = true;
start_idx = 1;
}
'+' => start_idx = 1,
_ => {}
}
}
let mut end_idx = start_idx;
while end_idx < chars.len() {
let ch = chars[end_idx];
let digit_value = match ch {
'0'..='9' => (ch as u32) - ('0' as u32),
'a'..='z' => (ch as u32) - ('a' as u32) + 10,
'A'..='Z' => (ch as u32) - ('A' as u32) + 10,
_ => break,
};
if digit_value >= radix {
break;
}
end_idx += 1;
}
if end_idx == start_idx {
return Err(NumberUtilsError::InvalidFormat(
"No valid digits found".to_string(),
));
}
let number_str: String = chars[start_idx..end_idx].iter().collect();
let result = i64::from_str_radix(&number_str, radix)
.map_err(|_| NumberUtilsError::InvalidFormat(format!("Cannot parse: {number_str}")))?;
Ok(if is_negative { -result } else { result })
}
pub fn to_fixed(n: f64, digits: usize) -> String {
if digits > 100 {
return format!("{n:.100}");
}
format!("{n:.digits$}")
}
pub fn to_exponential(n: f64, fraction_digits: Option<usize>) -> String {
match fraction_digits {
Some(digits) => {
let digits = digits.min(100);
format!("{n:.digits$e}")
}
None => format!("{n:e}"),
}
}
pub fn to_precision(n: f64, precision: Option<usize>) -> String {
match precision {
Some(p) if p > 0 => {
let p = p.min(100);
if n == 0.0 {
return "0".repeat(p);
}
let abs_n = n.abs();
let log10 = abs_n.log10().floor() as i32;
if log10 >= 0 && log10 < p as i32 {
let decimal_places = (p as i32 - log10 - 1).max(0) as usize;
format!("{n:.decimal_places$}")
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
} else {
format!("{n:.precision$e}", precision = p - 1)
}
}
_ => n.to_string(),
}
}
pub fn max_safe_integer() -> f64 {
9007199254740991.0 }
pub fn min_safe_integer() -> f64 {
-9007199254740991.0 }
pub fn positive_infinity() -> f64 {
f64::INFINITY
}
pub fn negative_infinity() -> f64 {
f64::NEG_INFINITY
}
pub fn clamp(value: f64, min: f64, max: f64) -> f64 {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
pub fn lerp(start: f64, end: f64, t: f64) -> f64 {
start + (end - start) * t
}
pub fn map_range(value: f64, in_min: f64, in_max: f64, out_min: f64, out_max: f64) -> f64 {
(value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
}