FIX: Floating-point precision bug in FLOOR functions
Fixes #571 - Add EXCEL_PRECISION constant (15 significant digits) - Fix FLOOR(7.1, 0.1) returning 7.0 instead of 7.1 - Apply to_excel_precision to ratio before floor/ceil operations - Affects FLOOR, FLOOR.MATH, and FLOOR.PRECISE functions - Add test_floor with 6 test cases
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
c4142d4bf8
commit
2a7d59e512
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
123
base/src/test/test_floor.rs
Normal file
123
base/src/test/test_floor.rs
Normal file
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user