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:
committed by
Nicolás Hatcher Andrés
parent
acb90fbb9d
commit
dc49afa2c3
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user