diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..17b4ef2df 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -610,6 +610,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_row(arg_count), Function::Columns => args_signature_one_vector(arg_count), Function::Ln => args_signature_scalars(arg_count, 1, 0), + Function::Gcd => vec![Signature::Vector; arg_count], + Function::Lcm => vec![Signature::Vector; arg_count], 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), @@ -824,6 +826,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Rounddown => scalar_arguments(args), Function::Roundup => scalar_arguments(args), Function::Ln => scalar_arguments(args), + Function::Gcd => StaticResult::Scalar, + Function::Lcm => StaticResult::Scalar, Function::Log => scalar_arguments(args), Function::Log10 => scalar_arguments(args), Function::Sin => scalar_arguments(args), diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 8931b58d2..5fba03f0c 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -523,6 +523,228 @@ impl Model { CalcResult::Number(result) } + pub(crate) fn fn_gcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + fn gcd(mut a: u128, mut b: u128) -> u128 { + while b != 0 { + let r = a % b; + a = b; + b = r; + } + a + } + + let mut result: Option = None; + let mut update = |value: f64| -> Result<(), CalcResult> { + if value < 0.0 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "numbers must be positive".to_string(), + )); + } + if !value.is_finite() { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "value must be finite".to_string(), + )); + } + let truncated = value.trunc(); + if truncated > u128::MAX as f64 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "value too large".to_string(), + )); + } + let v = truncated as u128; + result = Some(match result { + None => v, + Some(r) => gcd(r, v), + }); + Ok(()) + }; + + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + for row in left.row..=right.row { + for column in left.column..=right.column { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + } + } + CalcResult::Array(arr) => { + for row in arr { + for value in row { + match value { + ArrayNode::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + ArrayNode::Error(err) => { + return CalcResult::Error { + error: err, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => {} + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + + CalcResult::Number(result.unwrap_or(0) as f64) + } + + pub(crate) fn fn_lcm(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + fn gcd(mut a: u128, mut b: u128) -> u128 { + while b != 0 { + let r = a % b; + a = b; + b = r; + } + a + } + + let mut result: Option = None; + let mut update = |value: f64| -> Result<(), CalcResult> { + if value < 0.0 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "numbers must be positive".to_string(), + )); + } + if !value.is_finite() { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "value must be finite".to_string(), + )); + } + let truncated = value.trunc(); + if truncated > u128::MAX as f64 { + return Err(CalcResult::new_error( + Error::NUM, + cell, + "value too large".to_string(), + )); + } + let v = truncated as u128; + result = Some(match result { + None => v, + Some(r) => { + if r == 0 || v == 0 { + 0 + } else { + r / gcd(r, v) * v + } + } + }); + Ok(()) + }; + + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + for row in left.row..=right.row { + for column in left.column..=right.column { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + } + } + CalcResult::Array(arr) => { + for row in arr { + for value in row { + match value { + ArrayNode::Number(v) => { + if let Err(e) = update(v) { + return e; + } + } + ArrayNode::Error(err) => { + return CalcResult::Error { + error: err, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => {} + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => {} + } + } + + CalcResult::Number(result.unwrap_or(0) as f64) + } + pub(crate) fn fn_rand(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if !args.is_empty() { return CalcResult::new_args_number_error(cell); diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..364827717 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -57,6 +57,8 @@ pub enum Function { Log, Log10, Ln, + Gcd, + Lcm, Max, Min, Pi, @@ -253,7 +255,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -281,6 +283,8 @@ impl Function { Function::Abs, Function::Pi, Function::Ln, + Function::Gcd, + Function::Lcm, Function::Log, Function::Log10, Function::Sqrt, @@ -541,6 +545,8 @@ impl Function { "ATAN2" => Some(Function::Atan2), "LN" => Some(Function::Ln), + "GCD" => Some(Function::Gcd), + "LCM" => Some(Function::Lcm), "LOG" => Some(Function::Log), "LOG10" => Some(Function::Log10), @@ -747,6 +753,8 @@ impl fmt::Display for Function { Function::Log => write!(f, "LOG"), Function::Log10 => write!(f, "LOG10"), Function::Ln => write!(f, "LN"), + Function::Gcd => write!(f, "GCD"), + Function::Lcm => write!(f, "LCM"), Function::Sin => write!(f, "SIN"), Function::Cos => write!(f, "COS"), Function::Tan => write!(f, "TAN"), @@ -977,6 +985,8 @@ impl Model { Function::Log => self.fn_log(args, cell), Function::Log10 => self.fn_log10(args, cell), Function::Ln => self.fn_ln(args, cell), + Function::Gcd => self.fn_gcd(args, cell), + Function::Lcm => self.fn_lcm(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 a0a0d69d6..17d9a2aa1 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -57,10 +57,12 @@ mod test_extend; mod test_fn_fv; mod test_fn_type; mod test_frozen_rows_and_columns; +mod test_gcd; mod test_geomean; mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; +mod test_lcm; mod test_ln; mod test_log; mod test_log10; diff --git a/base/src/test/test_gcd.rs b/base/src/test/test_gcd.rs new file mode 100644 index 000000000..af1367d89 --- /dev/null +++ b/base/src/test/test_gcd.rs @@ -0,0 +1,75 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_gcd_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=GCD()"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn test_fn_gcd_basic() { + let mut model = new_empty_model(); + model._set("A1", "=GCD(12)"); + model._set("A2", "=GCD(60,36)"); + model._set("A3", "=GCD(15,25,35)"); + model._set("A4", "=GCD(12.7,8.3)"); // Decimal truncation + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"12"); + assert_eq!(model._get_text("A2"), *"12"); + assert_eq!(model._get_text("A3"), *"5"); + assert_eq!(model._get_text("A4"), *"4"); +} + +#[test] +fn test_fn_gcd_zeros_and_edge_cases() { + let mut model = new_empty_model(); + model._set("A1", "=GCD(0)"); + model._set("A2", "=GCD(0,12)"); + model._set("A3", "=GCD(12,0)"); + model._set("A4", "=GCD(1,2,3,4,5)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"12"); + assert_eq!(model._get_text("A3"), *"12"); + assert_eq!(model._get_text("A4"), *"1"); +} + +#[test] +fn test_fn_gcd_error_cases() { + let mut model = new_empty_model(); + model._set("A1", "=GCD(-5)"); + model._set("A2", "=GCD(12,-8)"); + model._set("B1", "=1/0"); // Infinity + model._set("A3", "=GCD(B1)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + assert_eq!(model._get_text("A2"), *"#NUM!"); + assert_eq!(model._get_text("A3"), *"#DIV/0!"); +} + +#[test] +fn test_fn_gcd_ranges() { + let mut model = new_empty_model(); + // Range with numbers + model._set("B1", "12"); + model._set("B2", "18"); + model._set("B3", "24"); + model._set("A1", "=GCD(B1:B3)"); + + // Range with mixed data (text ignored) + model._set("C1", "12"); + model._set("C2", "text"); + model._set("C3", "6"); + model._set("A2", "=GCD(C1:C3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"6"); + assert_eq!(model._get_text("A2"), *"6"); +} diff --git a/base/src/test/test_lcm.rs b/base/src/test/test_lcm.rs new file mode 100644 index 000000000..ff1bc12fc --- /dev/null +++ b/base/src/test/test_lcm.rs @@ -0,0 +1,82 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_lcm_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=LCM()"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn test_fn_lcm_basic() { + let mut model = new_empty_model(); + model._set("A1", "=LCM(12)"); + model._set("A2", "=LCM(25,40)"); + model._set("A3", "=LCM(4,6,8)"); + model._set("A4", "=LCM(4.7,6.3)"); // Decimal truncation + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"12"); + assert_eq!(model._get_text("A2"), *"200"); + assert_eq!(model._get_text("A3"), *"24"); + assert_eq!(model._get_text("A4"), *"12"); +} + +#[test] +fn test_fn_lcm_zeros_and_edge_cases() { + let mut model = new_empty_model(); + model._set("A1", "=LCM(0)"); + model._set("A2", "=LCM(0,12)"); + model._set("A3", "=LCM(12,0)"); + model._set("A4", "=LCM(1,2,3,4,5)"); + model.evaluate(); + + // LCM with any zero = 0 + assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("A3"), *"0"); + assert_eq!(model._get_text("A4"), *"60"); +} + +#[test] +fn test_fn_lcm_error_cases() { + let mut model = new_empty_model(); + model._set("A1", "=LCM(-5)"); + model._set("A2", "=LCM(12,-8)"); + model._set("B1", "=1/0"); // Infinity + model._set("A3", "=LCM(B1)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + assert_eq!(model._get_text("A2"), *"#NUM!"); + assert_eq!(model._get_text("A3"), *"#DIV/0!"); +} + +#[test] +fn test_fn_lcm_ranges() { + let mut model = new_empty_model(); + // Range with numbers + model._set("B1", "4"); + model._set("B2", "6"); + model._set("B3", "8"); + model._set("A1", "=LCM(B1:B3)"); + + // Range with mixed data (text ignored) + model._set("C1", "4"); + model._set("C2", "text"); + model._set("C3", "6"); + model._set("A2", "=LCM(C1:C3)"); + + // Zero in range + model._set("D1", "4"); + model._set("D2", "0"); + model._set("A3", "=LCM(D1:D2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"24"); + assert_eq!(model._get_text("A2"), *"12"); + assert_eq!(model._get_text("A3"), *"0"); +}