From 2a5f001361b42dcb5716a2c8e4fc5fc8c3a45047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Sat, 12 Jul 2025 17:57:46 +0200 Subject: [PATCH] UPDATE: Adds LOG10 and LN for Elsa --- base/src/expressions/lexer/mod.rs | 3 ++ .../src/expressions/lexer/test/test_common.rs | 27 ++++++++++ .../src/expressions/parser/static_analysis.rs | 6 +++ base/src/expressions/utils/test.rs | 2 + base/src/functions/mathematical.rs | 51 ++++++++++++++++++ base/src/functions/mod.rs | 18 ++++++- base/src/test/mod.rs | 3 ++ base/src/test/test_ln.rs | 17 ++++++ base/src/test/test_log.rs | 19 +++++++ base/src/test/test_log10.rs | 35 ++++++++++++ xlsx/tests/calc_tests/LOG_LOG10_LN.xlsx | Bin 0 -> 13094 bytes 11 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 base/src/test/test_ln.rs create mode 100644 base/src/test/test_log.rs create mode 100644 base/src/test/test_log10.rs create mode 100644 xlsx/tests/calc_tests/LOG_LOG10_LN.xlsx diff --git a/base/src/expressions/lexer/mod.rs b/base/src/expressions/lexer/mod.rs index 50152f1..8a2ae7f 100644 --- a/base/src/expressions/lexer/mod.rs +++ b/base/src/expressions/lexer/mod.rs @@ -314,6 +314,9 @@ impl Lexer { } else if name_upper == self.language.booleans.r#false { return TokenType::Boolean(false); } + if self.peek_char() == Some('(') { + return TokenType::Ident(name); + } if self.mode == LexerMode::A1 { let parsed_reference = utils::parse_reference_a1(&name_upper); if parsed_reference.is_some() diff --git a/base/src/expressions/lexer/test/test_common.rs b/base/src/expressions/lexer/test/test_common.rs index 4a46e03..0ee6fbd 100644 --- a/base/src/expressions/lexer/test/test_common.rs +++ b/base/src/expressions/lexer/test/test_common.rs @@ -1,5 +1,6 @@ #![allow(clippy::unwrap_used)] +use crate::expressions::utils::column_to_number; use crate::language::get_language; use crate::locale::get_locale; @@ -685,3 +686,29 @@ fn test_comparisons() { assert_eq!(lx.next_token(), Number(7.0)); assert_eq!(lx.next_token(), EOF); } + +#[test] +fn test_log10_is_cell_reference() { + let mut lx = new_lexer("LOG10", true); + assert_eq!( + lx.next_token(), + Reference { + sheet: None, + column: column_to_number("LOG").unwrap(), + row: 10, + absolute_column: false, + absolute_row: false, + } + ); + assert_eq!(lx.next_token(), EOF); +} + +#[test] +fn test_log10_is_function() { + let mut lx = new_lexer("LOG10(100)", true); + assert_eq!(lx.next_token(), Ident("LOG10".to_string())); + assert_eq!(lx.next_token(), LeftParenthesis); + assert_eq!(lx.next_token(), Number(100.0)); + assert_eq!(lx.next_token(), RightParenthesis); + assert_eq!(lx.next_token(), EOF); +} diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 850d662..80f1943 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -609,6 +609,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Scalar; arg_count], Function::Column => args_signature_row(arg_count), Function::Columns => args_signature_one_vector(arg_count), + Function::Ln => args_signature_scalars(arg_count, 1, 0), + Function::Log => args_signature_scalars(arg_count, 1, 1), + Function::Log10 => args_signature_scalars(arg_count, 1, 0), Function::Cos => args_signature_scalars(arg_count, 1, 0), Function::Cosh => args_signature_scalars(arg_count, 1, 0), Function::Max => vec![Signature::Vector; arg_count], @@ -820,6 +823,9 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Round => scalar_arguments(args), Function::Rounddown => scalar_arguments(args), Function::Roundup => scalar_arguments(args), + Function::Ln => scalar_arguments(args), + Function::Log => scalar_arguments(args), + Function::Log10 => scalar_arguments(args), Function::Sin => scalar_arguments(args), Function::Sinh => scalar_arguments(args), Function::Sqrt => scalar_arguments(args), diff --git a/base/src/expressions/utils/test.rs b/base/src/expressions/utils/test.rs index b6dc3da..d53bc4e 100644 --- a/base/src/expressions/utils/test.rs +++ b/base/src/expressions/utils/test.rs @@ -211,4 +211,6 @@ fn test_names() { assert!(!is_valid_identifier("test€")); assert!(!is_valid_identifier("truñe")); assert!(!is_valid_identifier("tr&ue")); + + assert!(!is_valid_identifier("LOG10")); } diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 82f4b8b..8931b58 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -378,6 +378,16 @@ impl Model { } } + single_number_fn!(fn_log10, |f| if f <= 0.0 { + Err(Error::NUM) + } else { + Ok(f64::log10(f)) + }); + single_number_fn!(fn_ln, |f| if f <= 0.0 { + Err(Error::NUM) + } else { + Ok(f64::ln(f)) + }); single_number_fn!(fn_sin, |f| Ok(f64::sin(f))); single_number_fn!(fn_cos, |f| Ok(f64::cos(f))); single_number_fn!(fn_tan, |f| Ok(f64::tan(f))); @@ -431,6 +441,47 @@ impl Model { CalcResult::Number(f64::atan2(y, x)) } + pub(crate) fn fn_log(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let n_args = args.len(); + if !(1..=2).contains(&n_args) { + return CalcResult::new_args_number_error(cell); + } + let x = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let y = if n_args == 1 { + 10.0 + } else { + match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + } + }; + if x <= 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Number must be positive".to_string(), + }; + } + if y == 1.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Logarithm base cannot be 1".to_string(), + }; + } + if y <= 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Logarithm base must be positive".to_string(), + }; + } + CalcResult::Number(f64::log(x, y)) + } + pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.len() != 2 { return CalcResult::new_args_number_error(cell); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 45da025..21c8f72 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -54,6 +54,9 @@ pub enum Function { Columns, Cos, Cosh, + Log, + Log10, + Ln, Max, Min, Pi, @@ -250,7 +253,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -277,6 +280,9 @@ impl Function { Function::Atanh, Function::Abs, Function::Pi, + Function::Ln, + Function::Log, + Function::Log10, Function::Sqrt, Function::Sqrtpi, Function::Atan2, @@ -534,6 +540,10 @@ impl Function { "POWER" => Some(Function::Power), "ATAN2" => Some(Function::Atan2), + "LN" => Some(Function::Ln), + "LOG" => Some(Function::Log), + "LOG10" => Some(Function::Log10), + "MAX" => Some(Function::Max), "MIN" => Some(Function::Min), "PRODUCT" => Some(Function::Product), @@ -734,6 +744,9 @@ impl fmt::Display for Function { Function::Switch => write!(f, "SWITCH"), Function::True => write!(f, "TRUE"), Function::Xor => write!(f, "XOR"), + Function::Log => write!(f, "LOG"), + Function::Log10 => write!(f, "LOG10"), + Function::Ln => write!(f, "LN"), Function::Sin => write!(f, "SIN"), Function::Cos => write!(f, "COS"), Function::Tan => write!(f, "TAN"), @@ -961,6 +974,9 @@ impl Model { Function::True => self.fn_true(args, cell), Function::Xor => self.fn_xor(args, cell), // Math and trigonometry + Function::Log => self.fn_log(args, cell), + Function::Log10 => self.fn_log10(args, cell), + Function::Ln => self.fn_ln(args, cell), Function::Sin => self.fn_sin(args, cell), Function::Cos => self.fn_cos(args, cell), Function::Tan => self.fn_tan(args, cell), diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index 8e1b4eb..a0a0d69 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -61,6 +61,9 @@ mod test_geomean; mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; +mod test_ln; +mod test_log; +mod test_log10; mod test_percentage; mod test_set_functions_error_handling; mod test_today; diff --git a/base/src/test/test_ln.rs b/base/src/test/test_ln.rs new file mode 100644 index 0000000..67c3b59 --- /dev/null +++ b/base/src/test/test_ln.rs @@ -0,0 +1,17 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn arguments() { + let mut model = new_empty_model(); + model._set("A1", "=LN(100)"); + model._set("A2", "=LN()"); + model._set("A3", "=LN(100, 10)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"4.605170186"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} diff --git a/base/src/test/test_log.rs b/base/src/test/test_log.rs new file mode 100644 index 0000000..27e240f --- /dev/null +++ b/base/src/test/test_log.rs @@ -0,0 +1,19 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn arguments() { + let mut model = new_empty_model(); + model._set("A1", "=LOG(100)"); + model._set("A2", "=LOG()"); + model._set("A3", "=LOG(10000, 10)"); + model._set("A4", "=LOG(100, 10, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"4"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); +} diff --git a/base/src/test/test_log10.rs b/base/src/test/test_log10.rs new file mode 100644 index 0000000..c642e31 --- /dev/null +++ b/base/src/test/test_log10.rs @@ -0,0 +1,35 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn arguments() { + let mut model = new_empty_model(); + model._set("A1", "=LOG10(100)"); + model._set("A2", "=LOG10()"); + model._set("A3", "=LOG10(100, 10)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn cell_and_function() { + let mut model = new_empty_model(); + model._set("A1", "=LOG10"); + + model.evaluate(); + + // This is the cell LOG10 + assert_eq!(model._get_text("A1"), *"0"); + + model._set("LOG10", "1000"); + model._set("A2", "=LOG10(LOG10)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"1000"); + assert_eq!(model._get_text("A2"), *"3"); +} diff --git a/xlsx/tests/calc_tests/LOG_LOG10_LN.xlsx b/xlsx/tests/calc_tests/LOG_LOG10_LN.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d88cc875c2a0e3458861ea527a5eb1346f0cc0a9 GIT binary patch literal 13094 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 IC&d8&4{