From dc49afa2c3fb143658d064c69bc31e59ee95a268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Wed, 19 Nov 2025 21:50:16 +0100 Subject: [PATCH] FIX: Format numbers a tad better I still think there is some way to go, but this is closer to Excel --- base/src/formatter/format.rs | 10 +++++++-- base/src/functions/util.rs | 7 +++++- base/src/number_format.rs | 35 +++++++++++++++++------------ base/src/test/test_number_format.rs | 9 ++++++++ base/src/test/test_trigonometric.rs | 11 +++++++++ 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/base/src/formatter/format.rs b/base/src/formatter/format.rs index 1e0865a..d966deb 100644 --- a/base/src/formatter/format.rs +++ b/base/src/formatter/format.rs @@ -15,7 +15,7 @@ pub struct Formatted { /// Returns the vector of chars of the fractional part of a *positive* number: /// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6'] -fn get_fract_part(value: f64, precision: i32) -> Vec { +fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec { let b = format!("{:.1$}", value.fract(), precision as usize) .chars() .collect::>(); @@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec { if last_non_zero < 2 { return vec![]; } + let max_len = if int_len > 15 { + 2_usize + } else { + 15_usize - int_len + 1 + }; + let last_non_zero = usize::min(last_non_zero, max_len + 1); b[2..last_non_zero].to_vec() } @@ -423,7 +429,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form if value_abs as i64 == 0 { int_part = vec![]; } - let fract_part = get_fract_part(value_abs, p.precision); + let fract_part = get_fract_part(value_abs, p.precision, int_part.len()); // ln is the number of digits of the integer part of the value let ln = int_part.len() as i32; // digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point diff --git a/base/src/functions/util.rs b/base/src/functions/util.rs index dea96e8..7ac041f 100644 --- a/base/src/functions/util.rs +++ b/base/src/functions/util.rs @@ -1,7 +1,10 @@ #[cfg(feature = "use_regex_lite")] use regex_lite as regex; -use crate::{calc_result::CalcResult, expressions::token::is_english_error_string}; +use crate::{ + calc_result::CalcResult, expressions::token::is_english_error_string, + number_format::to_excel_precision, +}; /// This test for exact match (modulo case). /// * strings are not cast into bools or numbers @@ -34,6 +37,8 @@ pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool { pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 { match (left, right) { (CalcResult::Number(value1), CalcResult::Number(value2)) => { + let value1 = to_excel_precision(*value1, 15); + let value2 = to_excel_precision(*value2, 15); if (value2 - value1).abs() < f64::EPSILON { return 0; } diff --git a/base/src/number_format.rs b/base/src/number_format.rs index 73376ae..5228da4 100644 --- a/base/src/number_format.rs +++ b/base/src/number_format.rs @@ -112,29 +112,36 @@ pub fn to_precision(value: f64, precision: usize) -> f64 { /// ``` /// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})` /// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)). -/// FIXME: There has to be a better algorithm :/ pub fn to_excel_precision_str(value: f64) -> String { to_precision_str(value, 15) } + +pub fn to_excel_precision(value: f64, precision: usize) -> f64 { + if !value.is_finite() { + return value; + } + + let s = format!("{:.*e}", precision.saturating_sub(1), value); + s.parse::().unwrap_or(value) +} + pub fn to_precision_str(value: f64, precision: usize) -> String { - if value.is_infinite() { - return "inf".to_string(); + if !value.is_finite() { + if value.is_infinite() { + return "inf".to_string(); + } else { + return "NaN".to_string(); + } } - if value.is_nan() { - return "NaN".to_string(); - } - let exponent = value.abs().log10().floor(); - let base = value / 10.0_f64.powf(exponent); - let base = format!("{0:.1$}", base, precision - 1); - let value = format!("{base}e{exponent}").parse::().unwrap_or({ - // TODO: do this in a way that does not require a possible error - 0.0 - }); + + let s = format!("{:.*e}", precision.saturating_sub(1), value); + let parsed = s.parse::().unwrap_or(value); + // I would love to use the std library. There is not a speed concern here // problem is it doesn't do the right thing // Also ryu is my favorite _modern_ algorithm let mut buffer = ryu::Buffer::new(); - let text = buffer.format(value); + let text = buffer.format(parsed); // The above algorithm converts 2 to 2.0 regrettably if let Some(stripped) = text.strip_suffix(".0") { return stripped.to_string(); diff --git a/base/src/test/test_number_format.rs b/base/src/test/test_number_format.rs index ae4cc04..e4dcb4a 100644 --- a/base/src/test/test_number_format.rs +++ b/base/src/test/test_number_format.rs @@ -8,6 +8,15 @@ fn test_simple_format() { assert_eq!(formatted.text, "2.3".to_string()); } +#[test] +fn test_maximum_zeros() { + let formatted = format_number(1.0 / 3.0, "#,##0.0000000000000000000", "en"); + assert_eq!(formatted.text, "0.3333333333333330000".to_string()); + + let formatted = format_number(1234.0 + 1.0 / 3.0, "#,##0.0000000000000000000", "en"); + assert_eq!(formatted.text, "1,234.3333333333300000000".to_string()); +} + #[test] #[ignore = "not yet implemented"] fn test_wrong_locale() { diff --git a/base/src/test/test_trigonometric.rs b/base/src/test/test_trigonometric.rs index 416776e..452d44c 100644 --- a/base/src/test/test_trigonometric.rs +++ b/base/src/test/test_trigonometric.rs @@ -96,3 +96,14 @@ fn test_fn_tan_pi2() { // This is consistent with IEEE 754 but inconsistent with Excel assert_eq!(model._get_text("A1"), *"1.63312E+16"); } + +#[test] +fn test_trigonometric_identity() { + let mut model = new_empty_model(); + model._set("A1", "=COTH(1)*CSCH(1)"); + model._set("A2", "=COSH(1)/(SINH(1))^2"); + model._set("A3", "=A1=A2"); + model.evaluate(); + + assert_eq!(model._get_text("A3"), *"TRUE"); +}