FIX: Format numbers a tad better

I still think there is some way to go, but this is closer to Excel
This commit is contained in:
Nicolás Hatcher
2025-11-19 21:50:16 +01:00
committed by Nicolás Hatcher Andrés
parent acb90fbb9d
commit dc49afa2c3
5 changed files with 55 additions and 17 deletions

View File

@@ -15,7 +15,7 @@ pub struct Formatted {
/// Returns the vector of chars of the fractional part of a *positive* number: /// Returns the vector of chars of the fractional part of a *positive* number:
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6'] /// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
fn get_fract_part(value: f64, precision: i32) -> Vec<char> { fn get_fract_part(value: f64, precision: i32, int_len: usize) -> Vec<char> {
let b = format!("{:.1$}", value.fract(), precision as usize) let b = format!("{:.1$}", value.fract(), precision as usize)
.chars() .chars()
.collect::<Vec<char>>(); .collect::<Vec<char>>();
@@ -30,6 +30,12 @@ fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
if last_non_zero < 2 { if last_non_zero < 2 {
return vec![]; 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() 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 { if value_abs as i64 == 0 {
int_part = vec![]; 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 // ln is the number of digits of the integer part of the value
let ln = int_part.len() as i32; let ln = int_part.len() as i32;
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point // digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point

View File

@@ -1,7 +1,10 @@
#[cfg(feature = "use_regex_lite")] #[cfg(feature = "use_regex_lite")]
use regex_lite as regex; 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). /// This test for exact match (modulo case).
/// * strings are not cast into bools or numbers /// * 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 { pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
match (left, right) { match (left, right) {
(CalcResult::Number(value1), CalcResult::Number(value2)) => { (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 { if (value2 - value1).abs() < f64::EPSILON {
return 0; return 0;
} }

View File

@@ -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)})` /// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)). /// 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 { pub fn to_excel_precision_str(value: f64) -> String {
to_precision_str(value, 15) 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::<f64>().unwrap_or(value)
}
pub fn to_precision_str(value: f64, precision: usize) -> String { pub fn to_precision_str(value: f64, precision: usize) -> String {
if value.is_infinite() { if !value.is_finite() {
return "inf".to_string(); if value.is_infinite() {
return "inf".to_string();
} else {
return "NaN".to_string();
}
} }
if value.is_nan() {
return "NaN".to_string(); let s = format!("{:.*e}", precision.saturating_sub(1), value);
} let parsed = s.parse::<f64>().unwrap_or(value);
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::<f64>().unwrap_or({
// TODO: do this in a way that does not require a possible error
0.0
});
// I would love to use the std library. There is not a speed concern here // I would love to use the std library. There is not a speed concern here
// problem is it doesn't do the right thing // problem is it doesn't do the right thing
// Also ryu is my favorite _modern_ algorithm // Also ryu is my favorite _modern_ algorithm
let mut buffer = ryu::Buffer::new(); 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 // The above algorithm converts 2 to 2.0 regrettably
if let Some(stripped) = text.strip_suffix(".0") { if let Some(stripped) = text.strip_suffix(".0") {
return stripped.to_string(); return stripped.to_string();

View File

@@ -8,6 +8,15 @@ fn test_simple_format() {
assert_eq!(formatted.text, "2.3".to_string()); 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] #[test]
#[ignore = "not yet implemented"] #[ignore = "not yet implemented"]
fn test_wrong_locale() { fn test_wrong_locale() {

View File

@@ -96,3 +96,14 @@ fn test_fn_tan_pi2() {
// This is consistent with IEEE 754 but inconsistent with Excel // This is consistent with IEEE 754 but inconsistent with Excel
assert_eq!(model._get_text("A1"), *"1.63312E+16"); 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");
}