use super::{CalcValue, CalcVertex, Value};
use crate::calc_engine::connector::Connector;
use logisheets_parser::ast;
fn calc<C, F>(args: Vec<CalcVertex>, fetcher: &mut C, f: &F) -> CalcVertex
where
C: Connector,
F: Fn(f64, i32) -> f64,
{
assert_or_return!(args.len() == 2, ast::Error::Unspecified);
let mut args_iter = args.into_iter();
let first = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(num, first);
let second = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(digits, second);
let r = f(num, digits.trunc() as i32);
CalcVertex::from_number(r)
}
#[derive(Clone, Copy)]
enum Mode {
HalfAway,
Away,
Toward,
}
fn round_decimal(num: f64, digits: i32, mode: Mode) -> f64 {
if !num.is_finite() || num == 0. {
return num;
}
let s = format!("{:.14e}", num.abs());
let Some((mantissa, exp)) = s.split_once('e') else {
return num;
};
let Ok(exp) = exp.parse::<i32>() else {
return num;
};
let ds: Vec<u8> = mantissa
.bytes()
.filter(u8::is_ascii_digit)
.map(|b| b - b'0')
.collect();
let keep = exp + digits + 1;
if keep >= ds.len() as i32 {
return num;
}
let (kept, dropped) = if keep <= 0 {
(&[][..], &ds[..])
} else {
ds.split_at(keep as usize)
};
let mut n = kept.iter().fold(0u64, |acc, d| acc * 10 + *d as u64);
let round_away = match mode {
Mode::HalfAway => keep >= 0 && dropped.first().is_some_and(|d| *d >= 5),
Mode::Away => dropped.iter().any(|d| *d != 0),
Mode::Toward => false,
};
if round_away {
n += 1;
}
if n == 0 {
return 0f64.copysign(num);
}
let mut text = n.to_string();
if digits > 0 {
let point = digits as usize;
if text.len() <= point {
text = format!("{}{}", "0".repeat(point - text.len() + 1), text);
}
text.insert(text.len() - point, '.');
} else {
text.push_str(&"0".repeat((-digits) as usize));
}
if num < 0. {
text.insert(0, '-');
}
text.parse().unwrap_or(num)
}
pub fn calc_mround<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
assert_or_return!(args.len() == 2, ast::Error::Unspecified);
let mut args_iter = args.into_iter();
let first = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(num, first);
let second = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(multiple, second);
assert_or_return!(multiple * num >= 0., ast::Error::Num);
if num == 0. {
return CalcVertex::from_number(0.);
}
let n = (num / multiple).round();
CalcVertex::from_number(n * multiple)
}
pub fn calc_round<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
let f = |num: f64, digits: i32| -> f64 { round_decimal(num, digits, Mode::HalfAway) };
calc(args, fetcher, &f)
}
pub fn calc_rounddown<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
let f = |num: f64, digits: i32| -> f64 { round_decimal(num, digits, Mode::Toward) };
calc(args, fetcher, &f)
}
pub fn calc_roundup<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
let f = |num: f64, digits: i32| -> f64 { round_decimal(num, digits, Mode::Away) };
calc(args, fetcher, &f)
}
pub fn calc_trunc<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
assert_or_return!(args.len() == 1 || args.len() == 2, ast::Error::Unspecified);
let mut args_iter = args.into_iter();
let first = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(num, first);
let digits = if let Some(arg) = args_iter.next() {
let v = fetcher.get_calc_value(arg);
assert_f64_from_calc_value!(d, v);
d.trunc() as i32
} else {
0
};
let shift = 10_f64.powi(digits);
CalcVertex::from_number((num * shift).trunc() / shift)
}
pub fn calc_ceiling<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
calc_to_multiple(args, fetcher, true)
}
pub fn calc_floor<C>(args: Vec<CalcVertex>, fetcher: &mut C) -> CalcVertex
where
C: Connector,
{
calc_to_multiple(args, fetcher, false)
}
fn calc_to_multiple<C>(args: Vec<CalcVertex>, fetcher: &mut C, up: bool) -> CalcVertex
where
C: Connector,
{
assert_or_return!(args.len() == 2, ast::Error::Unspecified);
let mut args_iter = args.into_iter();
let first = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(num, first);
let second = fetcher.get_calc_value(args_iter.next().unwrap());
assert_f64_from_calc_value!(sig, second);
if sig == 0. || num == 0. {
return CalcVertex::from_number(0.);
}
assert_or_return!((num > 0.) == (sig > 0.), ast::Error::Num);
let q = num / sig;
let rounded = if up { q.ceil() } else { q.floor() };
CalcVertex::from_number(rounded * sig)
}
#[cfg(test)]
mod tests {
use super::{Mode, round_decimal};
fn round(num: f64, digits: i32) -> f64 {
round_decimal(num, digits, Mode::HalfAway)
}
#[test]
fn half_way_decimals_round_away_from_zero_like_excel() {
assert_eq!(round(4.935, 2), 4.94);
assert_eq!(round(1.005, 2), 1.01);
assert_eq!(round(2.675, 2), 2.68);
assert_eq!(round(-4.935, 2), -4.94);
assert_eq!(round(2.5, 0), 3.0);
assert_eq!(round(-2.5, 0), -3.0);
}
#[test]
fn the_same_number_rounds_the_same_way_however_it_arrived() {
let computed = 9.87 / 2.0;
assert_eq!(computed, 4.935, "the double is the same either way");
assert_eq!(round(computed, 2), round(4.935, 2));
assert_eq!(round(computed, 2), 4.94);
}
#[test]
fn negative_digits_round_to_tens_and_hundreds() {
assert_eq!(round(1250.0, -2), 1300.0);
assert_eq!(round(1234.5, -2), 1200.0);
assert_eq!(round(-1250.0, -2), -1300.0);
assert_eq!(round(4.935, -3), 0.0);
}
#[test]
fn a_value_under_half_a_unit_stays_put() {
assert_eq!(round(0.0005, 2), 0.0);
assert_eq!(round_decimal(0.0005, 2, Mode::Away), 0.01);
assert_eq!(round_decimal(0.0005, 2, Mode::Toward), 0.0);
assert_eq!(round(0.005, 2), 0.01);
assert_eq!(round(0.05, 1), 0.1);
assert_eq!(round(0.04, 1), 0.0);
}
#[test]
fn rounding_toward_and_away_do_not_lose_an_exact_value() {
assert_eq!(round_decimal(2.1, 2, Mode::Toward), 2.1);
assert_eq!(round_decimal(2.1, 2, Mode::Away), 2.1);
assert_eq!(round_decimal(3.14159, 3, Mode::Away), 3.142);
assert_eq!(round_decimal(-3.14159, 3, Mode::Toward), -3.141);
assert_eq!(round_decimal(-3.14159, 3, Mode::Away), -3.142);
}
#[test]
fn asking_for_more_places_than_the_number_has_changes_nothing() {
assert_eq!(round(4.935, 10), 4.935);
assert_eq!(round(0.0, 2), 0.0);
assert_eq!(round(1.0e20, 2), 1.0e20);
assert!(round(f64::NAN, 2).is_nan());
assert_eq!(round(f64::INFINITY, 2), f64::INFINITY);
}
}