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 0000000..d88cc87 Binary files /dev/null and b/xlsx/tests/calc_tests/LOG_LOG10_LN.xlsx differ