diff --git a/base/src/constants.rs b/base/src/constants.rs index 047b4b0..4ba469c 100644 --- a/base/src/constants.rs +++ b/base/src/constants.rs @@ -12,6 +12,9 @@ pub(crate) const DEFAULT_WINDOW_WIDTH: i64 = 800; pub(crate) const LAST_COLUMN: i32 = 16_384; pub(crate) const LAST_ROW: i32 = 1_048_576; +// Excel uses 15 significant digits of precision for all numeric calculations. +pub(crate) const EXCEL_PRECISION: usize = 15; + // 693_594 is computed as: // NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2 // The 2 days offset is because of Excel 1900 bug diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 2b088af..4578ff1 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -1,9 +1,9 @@ use crate::cast::NumberOrArray; -use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::constants::{EXCEL_PRECISION, LAST_COLUMN, LAST_ROW}; use crate::expressions::parser::ArrayNode; use crate::expressions::types::CellReferenceIndex; use crate::functions::math_util::{from_roman, to_roman_with_form}; -use crate::number_format::to_precision; +use crate::number_format::{to_excel_precision, to_precision}; use crate::single_number_fn; use crate::{ calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, @@ -984,7 +984,9 @@ impl Model { }; } - let result = f64::floor(value / significance) * significance; + // Apply Excel precision to the ratio to handle floating-point rounding errors + let ratio = to_excel_precision(value / significance, EXCEL_PRECISION); + let result = f64::floor(ratio) * significance; CalcResult::Number(result) } @@ -1121,10 +1123,14 @@ impl Model { } let significance = significance.abs(); if value < 0.0 && mode != 0.0 { - let result = f64::ceil(value / significance) * significance; + // Apply Excel precision to handle floating-point rounding errors + let ratio = to_excel_precision(value / significance, EXCEL_PRECISION); + let result = f64::ceil(ratio) * significance; CalcResult::Number(result) } else { - let result = f64::floor(value / significance) * significance; + // Apply Excel precision to handle floating-point rounding errors + let ratio = to_excel_precision(value / significance, EXCEL_PRECISION); + let result = f64::floor(ratio) * significance; CalcResult::Number(result) } } @@ -1154,7 +1160,9 @@ impl Model { return CalcResult::Number(0.0); } - let result = f64::floor(value / significance) * significance; + // Apply Excel precision to handle floating-point rounding errors + let ratio = to_excel_precision(value / significance, EXCEL_PRECISION); + let result = f64::floor(ratio) * significance; CalcResult::Number(result) } diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 3e80ace..b7fd6ed 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -65,6 +65,7 @@ mod test_escape_quotes; mod test_even_odd; mod test_exp_sign; mod test_extend; +mod test_floor; mod test_fn_datevalue_timevalue; mod test_fn_fv; mod test_fn_round; diff --git a/base/src/test/test_floor.rs b/base/src/test/test_floor.rs new file mode 100644 index 0000000..3d7bd88 --- /dev/null +++ b/base/src/test/test_floor.rs @@ -0,0 +1,123 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_floor_floating_point_precision() { + // This test specifically checks the floating-point precision bug fix + // Bug: FLOOR(7.1, 0.1) was returning 7.0 instead of 7.1 + let mut model = new_empty_model(); + + // FLOOR tests + model._set("C5", "=FLOOR(7.1, 0.1)"); + model._set("H7", "=FLOOR(-7.1, -0.1)"); + + // FLOOR.PRECISE tests + model._set("C53", "=FLOOR.PRECISE(7.1, 0.1)"); + model._set("H53", "=FLOOR.PRECISE(7.1, -0.1)"); + + // FLOOR.MATH tests + model._set("C101", "=FLOOR.MATH(7.1, 0.1)"); + model._set("H101", "=FLOOR.MATH(7.1, -0.1)"); + + model.evaluate(); + + // All should return 7.1 + assert_eq!(model._get_text("C5"), *"7.1"); + assert_eq!(model._get_text("H7"), *"-7.1"); + assert_eq!(model._get_text("C53"), *"7.1"); + assert_eq!(model._get_text("H53"), *"7.1"); + assert_eq!(model._get_text("C101"), *"7.1"); + assert_eq!(model._get_text("H101"), *"7.1"); +} + +#[test] +fn test_floor_additional_precision_cases() { + let mut model = new_empty_model(); + model._set("A1", "=FLOOR(7.9, 0.1)"); + model._set("A2", "=FLOOR(2.6, 0.5)"); + model._set("A3", "=FLOOR(0.3, 0.1)"); // 0.1 + 0.2 type scenario + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"7.9"); + assert_eq!(model._get_text("A2"), *"2.5"); + assert_eq!(model._get_text("A3"), *"0.3"); +} + +#[test] +fn test_floor_basic_cases() { + let mut model = new_empty_model(); + model._set("A1", "=FLOOR(3.7, 2)"); + model._set("A2", "=FLOOR(3.2, 1)"); + model._set("A3", "=FLOOR(10, 3)"); + model._set("A4", "=FLOOR(7, 2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("A2"), *"3"); + assert_eq!(model._get_text("A3"), *"9"); + assert_eq!(model._get_text("A4"), *"6"); +} + +#[test] +fn test_floor_negative_numbers() { + let mut model = new_empty_model(); + // Both negative: rounds toward zero + model._set("A1", "=FLOOR(-2.5, -2)"); + model._set("A2", "=FLOOR(-11, -3)"); + + // Negative number, positive significance: rounds away from zero + model._set("A3", "=FLOOR(-11, 3)"); + model._set("A4", "=FLOOR(-2.5, 2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"-2"); + assert_eq!(model._get_text("A2"), *"-9"); + assert_eq!(model._get_text("A3"), *"-12"); + assert_eq!(model._get_text("A4"), *"-4"); +} + +#[test] +fn test_floor_error_cases() { + let mut model = new_empty_model(); + // Positive number with negative significance should error + model._set("A1", "=FLOOR(2.5, -2)"); + model._set("A2", "=FLOOR(10, -3)"); + + // Division by zero + model._set("A3", "=FLOOR(5, 0)"); + + // Wrong number of arguments + model._set("A4", "=FLOOR(5)"); + model._set("A5", "=FLOOR(5, 1, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + assert_eq!(model._get_text("A2"), *"#NUM!"); + assert_eq!(model._get_text("A3"), *"#DIV/0!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + assert_eq!(model._get_text("A5"), *"#ERROR!"); +} + +#[test] +fn test_floor_edge_cases() { + let mut model = new_empty_model(); + // Zero value + model._set("A1", "=FLOOR(0, 5)"); + model._set("A2", "=FLOOR(0, 0)"); + + // Exact multiples + model._set("A3", "=FLOOR(10, 5)"); + model._set("A4", "=FLOOR(9, 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("A3"), *"10"); + assert_eq!(model._get_text("A4"), *"9"); +}