fn repr_of(value: f64) -> String {
format!("{value:?}")
}
fn parse_decimal(s: &str) -> Option<(i128, u32)> {
let s = s.trim();
if s.is_empty() || s.eq_ignore_ascii_case("nan") || s.contains("inf") {
return None;
}
let (neg, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.strip_prefix('+').unwrap_or(s)),
};
let (mantissa_str, exp) = match body.split_once(['e', 'E']) {
Some((m, e)) => (m, e.parse::<i32>().ok()?),
None => (body, 0),
};
let (int_str, frac_str) = match mantissa_str.split_once('.') {
Some((i, f)) => (i, f),
None => (mantissa_str, ""),
};
if int_str.is_empty() && frac_str.is_empty() {
return None;
}
if !int_str.bytes().all(|b| b.is_ascii_digit()) || !frac_str.bytes().all(|b| b.is_ascii_digit())
{
return None;
}
let digits: String = format!("{int_str}{frac_str}");
let mut mant = digits.parse::<i128>().ok()?;
if neg {
mant = -mant;
}
let mut scale = frac_str.len() as i32 - exp;
if scale < 0 {
mant = mant.checked_mul(10i128.checked_pow((-scale) as u32)?)?;
scale = 0;
}
Some((mant, scale as u32))
}
type DecParts = (i128, u32);
fn ratio_parts(value: DecParts, target: DecParts) -> Option<(i128, i128)> {
let (mut num, sv) = value;
let (mut den, st) = target;
debug_assert!(den != 0);
if den < 0 {
num = num.checked_neg()?;
den = den.checked_neg()?;
}
let p = st as i32 - sv as i32;
if p >= 0 {
num = num.checked_mul(10i128.checked_pow(p as u32)?)?;
} else {
den = den.checked_mul(10i128.checked_pow((-p) as u32)?)?;
}
Some((num, den))
}
fn div_round_half_even(num: i128, den: i128) -> i128 {
let q = num.div_euclid(den);
let r = num.rem_euclid(den);
match (r * 2).cmp(&den) {
std::cmp::Ordering::Greater => q + 1,
std::cmp::Ordering::Less => q,
std::cmp::Ordering::Equal => {
if q % 2 == 0 {
q
} else {
q + 1
}
}
}
}
fn div_floor(num: i128, den: i128) -> i128 {
num.div_euclid(den)
}
fn div_ceil(num: i128, den: i128) -> i128 {
-((-num).div_euclid(den))
}
fn combine(q: i128, target: DecParts) -> Option<f64> {
let (mt, st) = target;
let prod = q.checked_mul(mt)?;
Some(prod as f64 / 10f64.powi(st as i32))
}
fn target_parts(target: f64) -> Option<DecParts> {
let parts = parse_decimal(&repr_of(target))?;
(parts.0 != 0).then_some(parts)
}
pub fn round_to(value: f64, target: f64) -> f64 {
try_round_to(value, target).unwrap_or(f64::NAN)
}
pub fn try_round_to(value: f64, target: f64) -> Option<f64> {
let t = target_parts(target)?;
let v = parse_decimal(&repr_of(value))?;
let (num, den) = ratio_parts(v, t)?;
combine(div_round_half_even(num, den), t)
}
pub fn floor_to(value: f64, target: f64) -> f64 {
try_floor_to(value, target).unwrap_or(f64::NAN)
}
pub fn try_floor_to(value: f64, target: f64) -> Option<f64> {
let t = target_parts(target)?;
let v = parse_decimal(&repr_of(value))?;
let (num, den) = ratio_parts(v, t)?;
combine(div_floor(num, den), t)
}
pub fn ceil_to(value: f64, target: f64) -> f64 {
try_ceil_to(value, target).unwrap_or(f64::NAN)
}
pub fn try_ceil_to(value: f64, target: f64) -> Option<f64> {
let t = target_parts(target)?;
let v = parse_decimal(&repr_of(value))?;
let (num, den) = ratio_parts(v, t)?;
combine(div_ceil(num, den), t)
}
pub fn get_digits(value: f64) -> i32 {
let s = repr_of(value);
if let Some((_, buf)) = s.split_once("e-") {
buf.parse().unwrap_or(0)
} else if let Some((_, buf)) = s.split_once('.') {
buf.len() as i32
} else {
0
}
}