From f772ba521d79afbdbe0003a121371d8a459e705d Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Mon, 14 Jul 2025 01:32:13 -0700 Subject: [PATCH 1/6] Add GCD and LCM functions --- .../src/expressions/parser/static_analysis.rs | 4 + base/src/functions/mathematical.rs | 178 ++++++++++++++++++ base/src/functions/mod.rs | 12 +- base/src/test/mod.rs | 2 + base/src/test/test_gcd.rs | 18 ++ base/src/test/test_lcm.rs | 18 ++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 base/src/test/test_gcd.rs create mode 100644 base/src/test/test_lcm.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..eac991fd9 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 => scalar_arguments(args), + Function::Lcm => scalar_arguments(args), 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..6fb8b9bd2 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -523,6 +523,184 @@ 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(), + )); + } + let v = value.trunc() 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(), + )); + } + let v = value.trunc() 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..bffe7ea7f 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -61,6 +61,8 @@ mod test_geomean; mod test_get_cell_content; mod test_implicit_intersection; mod test_issue_155; +mod test_gcd; +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..def40719d --- /dev/null +++ b/base/src/test/test_gcd.rs @@ -0,0 +1,18 @@ +#![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(60,36)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"12"); +} diff --git a/base/src/test/test_lcm.rs b/base/src/test/test_lcm.rs new file mode 100644 index 000000000..1a9a0802d --- /dev/null +++ b/base/src/test/test_lcm.rs @@ -0,0 +1,18 @@ +#![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(25,40)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"200"); +} From 3fe2bc0077b4a0259f94755db190aa984e063552 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Mon, 14 Jul 2025 17:39:57 -0700 Subject: [PATCH 2/6] fmt --- base/src/functions/mathematical.rs | 20 +++++++++++++++++--- base/src/test/mod.rs | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 6fb8b9bd2..b6b494db8 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -571,7 +571,11 @@ impl Model { } for row in left.row..=right.row { for column in left.column..=right.column { - match self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row, column }) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { CalcResult::Number(v) => { if let Err(e) = update(v) { return e; @@ -638,7 +642,13 @@ impl Model { let v = value.trunc() as u128; result = Some(match result { None => v, - Some(r) => if r == 0 || v == 0 { 0 } else { r / gcd(r, v) * v }, + Some(r) => { + if r == 0 || v == 0 { + 0 + } else { + r / gcd(r, v) * v + } + } }); Ok(()) }; @@ -660,7 +670,11 @@ impl Model { } for row in left.row..=right.row { for column in left.column..=right.column { - match self.evaluate_cell(CellReferenceIndex { sheet: left.sheet, row, column }) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { CalcResult::Number(v) => { if let Err(e) = update(v) { return e; diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index bffe7ea7f..17d9a2aa1 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -57,11 +57,11 @@ 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_gcd; mod test_lcm; mod test_ln; mod test_log; From 882897141291159bfd209517ff190b8e706d4287 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 12:12:36 -0700 Subject: [PATCH 3/6] fix unvalidated cast to uint128 --- base/src/functions/mathematical.rs | 34 ++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index b6b494db8..5fba03f0c 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -546,7 +546,22 @@ impl Model { "numbers must be positive".to_string(), )); } - let v = value.trunc() as u128; + 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), @@ -639,7 +654,22 @@ impl Model { "numbers must be positive".to_string(), )); } - let v = value.trunc() as u128; + 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) => { From 936c1a7f28b96b4712da71bdd0a7d163423167b2 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 12:15:27 -0700 Subject: [PATCH 4/6] fix return type --- base/src/expressions/parser/static_analysis.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index eac991fd9..17b4ef2df 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -826,8 +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 => scalar_arguments(args), - Function::Lcm => 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), From 133b8c58e64c8a2478e25ec711593bdc822d6766 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 12:22:45 -0700 Subject: [PATCH 5/6] add more test coverage --- base/src/test/test_gcd.rs | 87 ++++++++++++++++++++++++++++++- base/src/test/test_lcm.rs | 104 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 5 deletions(-) diff --git a/base/src/test/test_gcd.rs b/base/src/test/test_gcd.rs index def40719d..dd2df7956 100644 --- a/base/src/test/test_gcd.rs +++ b/base/src/test/test_gcd.rs @@ -10,9 +10,92 @@ fn test_fn_gcd_arguments() { } #[test] -fn test_fn_gcd_basic() { +fn test_fn_gcd_basic_functionality() { let mut model = new_empty_model(); - model._set("A1", "=GCD(60,36)"); + + // Single argument + model._set("A1", "=GCD(12)"); + model._set("A2", "=GCD(0)"); + + // Multiple arguments + model._set("A3", "=GCD(60,36)"); + model._set("A4", "=GCD(15,25,35)"); + model._set("A5", "=GCD(48,18,24)"); + + // With zeros + model._set("A6", "=GCD(0,12)"); + model._set("A7", "=GCD(12,0)"); + + // Decimal inputs (should truncate) + model._set("A8", "=GCD(12.7,8.3)"); + + // Edge cases + model._set("A9", "=GCD(1,1)"); + model._set("A10", "=GCD(1,2,3,4,5)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"12"); + assert_eq!(model._get_text("A2"), *"0"); + assert_eq!(model._get_text("A3"), *"12"); + assert_eq!(model._get_text("A4"), *"5"); + assert_eq!(model._get_text("A5"), *"6"); + assert_eq!(model._get_text("A6"), *"12"); + assert_eq!(model._get_text("A7"), *"12"); + assert_eq!(model._get_text("A8"), *"4"); + assert_eq!(model._get_text("A9"), *"1"); + assert_eq!(model._get_text("A10"), *"1"); +} + +#[test] +fn test_fn_gcd_error_cases() { + let mut model = new_empty_model(); + + // Negative numbers + model._set("A1", "=GCD(-5)"); + model._set("A2", "=GCD(12,-8)"); + + // Non-finite values + model._set("B1", "=1/0"); // Infinity + model._set("B2", "=0/0"); // NaN + model._set("A3", "=GCD(B1)"); + model._set("A4", "=GCD(B2)"); + model._set("A5", "=GCD(12,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!"); + assert_eq!(model._get_text("A4"), *"#DIV/0!"); + assert_eq!(model._get_text("A5"), *"#DIV/0!"); +} + +#[test] +fn test_fn_gcd_ranges_and_mixed() { + let mut model = new_empty_model(); + + // Range inputs + model._set("B1", "12"); + model._set("B2", "18"); + model._set("B3", "24"); + model._set("A1", "=GCD(B1:B3)"); + + // Mixed inputs (numbers, text, empty cells) + model._set("C1", "12"); + model._set("C2", "text"); // Should be ignored + model._set("C3", "18"); + // C4 is empty, should be ignored + model._set("C5", "6"); + model._set("A2", "=GCD(C1:C5)"); + + // No valid numbers case + model._set("D1", "text"); + model._set("A3", "=GCD(D1,D2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"6"); + assert_eq!(model._get_text("A2"), *"6"); + assert_eq!(model._get_text("A3"), *"0"); } diff --git a/base/src/test/test_lcm.rs b/base/src/test/test_lcm.rs index 1a9a0802d..277372916 100644 --- a/base/src/test/test_lcm.rs +++ b/base/src/test/test_lcm.rs @@ -10,9 +10,107 @@ fn test_fn_lcm_arguments() { } #[test] -fn test_fn_lcm_basic() { +fn test_fn_lcm_basic_functionality() { let mut model = new_empty_model(); - model._set("A1", "=LCM(25,40)"); + + // Single argument + model._set("A1", "=LCM(12)"); + model._set("A2", "=LCM(1)"); + + // Multiple arguments + model._set("A3", "=LCM(25,40)"); + model._set("A4", "=LCM(4,6,8)"); + model._set("A5", "=LCM(12,15,20)"); + + // With zeros (LCM with any zero = 0) + model._set("A6", "=LCM(0)"); + model._set("A7", "=LCM(0,12)"); + model._set("A8", "=LCM(12,0)"); + model._set("A9", "=LCM(10,0,20)"); + + // Decimal inputs (should truncate) + model._set("A10", "=LCM(4.7,6.3)"); + + // Edge cases + model._set("A11", "=LCM(1,1)"); + model._set("A12", "=LCM(1,2,3,4,5)"); + + // Large numbers (simpler ones) + model._set("A13", "=LCM(100,150)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"12"); + assert_eq!(model._get_text("A2"), *"1"); + assert_eq!(model._get_text("A3"), *"200"); + assert_eq!(model._get_text("A4"), *"24"); + assert_eq!(model._get_text("A5"), *"60"); + assert_eq!(model._get_text("A6"), *"0"); + assert_eq!(model._get_text("A7"), *"0"); + assert_eq!(model._get_text("A8"), *"0"); + assert_eq!(model._get_text("A9"), *"0"); + assert_eq!(model._get_text("A10"), *"12"); + assert_eq!(model._get_text("A11"), *"1"); + assert_eq!(model._get_text("A12"), *"60"); + assert_eq!(model._get_text("A13"), *"300"); +} + +#[test] +fn test_fn_lcm_error_cases() { + let mut model = new_empty_model(); + + // Negative numbers + model._set("A1", "=LCM(-5)"); + model._set("A2", "=LCM(12,-8)"); + + // Non-finite values + model._set("B1", "=1/0"); // Infinity + model._set("B2", "=0/0"); // NaN + model._set("A3", "=LCM(B1)"); + model._set("A4", "=LCM(B2)"); + model._set("A5", "=LCM(12,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!"); + assert_eq!(model._get_text("A4"), *"#DIV/0!"); + assert_eq!(model._get_text("A5"), *"#DIV/0!"); +} + +#[test] +fn test_fn_lcm_ranges_and_mixed() { + let mut model = new_empty_model(); + + // Range inputs + model._set("B1", "4"); + model._set("B2", "6"); + model._set("B3", "8"); + model._set("A1", "=LCM(B1:B3)"); + + // Mixed inputs (numbers, text, empty cells) + model._set("C1", "4"); + model._set("C2", "text"); // Should be ignored + model._set("C3", "6"); + // C4 is empty, should be ignored + model._set("C5", "8"); + model._set("A2", "=LCM(C1:C5)"); + + // Zero in range (should return 0) + model._set("D1", "4"); + model._set("D2", "0"); + model._set("D3", "6"); + model._set("A3", "=LCM(D1:D3)"); + + // No valid numbers case + model._set("E1", "text"); + model._set("A4", "=LCM(E1,E2)"); + model.evaluate(); - assert_eq!(model._get_text("A1"), *"200"); + + assert_eq!(model._get_text("A1"), *"24"); + assert_eq!(model._get_text("A2"), *"24"); + assert_eq!(model._get_text("A3"), *"0"); + assert_eq!(model._get_text("A4"), *"0"); } From 20966e71a6f903b4a49f58548583804a0d4ff9e8 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 12:25:21 -0700 Subject: [PATCH 6/6] refactor tests --- base/src/test/test_gcd.rs | 78 +++++++++++--------------------- base/src/test/test_lcm.rs | 94 +++++++++++++-------------------------- 2 files changed, 56 insertions(+), 116 deletions(-) diff --git a/base/src/test/test_gcd.rs b/base/src/test/test_gcd.rs index dd2df7956..af1367d89 100644 --- a/base/src/test/test_gcd.rs +++ b/base/src/test/test_gcd.rs @@ -10,92 +10,66 @@ fn test_fn_gcd_arguments() { } #[test] -fn test_fn_gcd_basic_functionality() { +fn test_fn_gcd_basic() { let mut model = new_empty_model(); - - // Single argument model._set("A1", "=GCD(12)"); - model._set("A2", "=GCD(0)"); - - // Multiple arguments - model._set("A3", "=GCD(60,36)"); - model._set("A4", "=GCD(15,25,35)"); - model._set("A5", "=GCD(48,18,24)"); - - // With zeros - model._set("A6", "=GCD(0,12)"); - model._set("A7", "=GCD(12,0)"); - - // Decimal inputs (should truncate) - model._set("A8", "=GCD(12.7,8.3)"); + 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(); - // Edge cases - model._set("A9", "=GCD(1,1)"); - model._set("A10", "=GCD(1,2,3,4,5)"); + 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"), *"12"); - assert_eq!(model._get_text("A2"), *"0"); + 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"), *"5"); - assert_eq!(model._get_text("A5"), *"6"); - assert_eq!(model._get_text("A6"), *"12"); - assert_eq!(model._get_text("A7"), *"12"); - assert_eq!(model._get_text("A8"), *"4"); - assert_eq!(model._get_text("A9"), *"1"); - assert_eq!(model._get_text("A10"), *"1"); + assert_eq!(model._get_text("A4"), *"1"); } #[test] fn test_fn_gcd_error_cases() { let mut model = new_empty_model(); - - // Negative numbers model._set("A1", "=GCD(-5)"); model._set("A2", "=GCD(12,-8)"); - - // Non-finite values model._set("B1", "=1/0"); // Infinity - model._set("B2", "=0/0"); // NaN model._set("A3", "=GCD(B1)"); - model._set("A4", "=GCD(B2)"); - model._set("A5", "=GCD(12,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!"); - assert_eq!(model._get_text("A4"), *"#DIV/0!"); - assert_eq!(model._get_text("A5"), *"#DIV/0!"); } #[test] -fn test_fn_gcd_ranges_and_mixed() { +fn test_fn_gcd_ranges() { let mut model = new_empty_model(); - - // Range inputs + // Range with numbers model._set("B1", "12"); model._set("B2", "18"); model._set("B3", "24"); model._set("A1", "=GCD(B1:B3)"); - // Mixed inputs (numbers, text, empty cells) + // Range with mixed data (text ignored) model._set("C1", "12"); - model._set("C2", "text"); // Should be ignored - model._set("C3", "18"); - // C4 is empty, should be ignored - model._set("C5", "6"); - model._set("A2", "=GCD(C1:C5)"); - - // No valid numbers case - model._set("D1", "text"); - model._set("A3", "=GCD(D1,D2)"); + 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"); - assert_eq!(model._get_text("A3"), *"0"); } diff --git a/base/src/test/test_lcm.rs b/base/src/test/test_lcm.rs index 277372916..ff1bc12fc 100644 --- a/base/src/test/test_lcm.rs +++ b/base/src/test/test_lcm.rs @@ -10,107 +10,73 @@ fn test_fn_lcm_arguments() { } #[test] -fn test_fn_lcm_basic_functionality() { +fn test_fn_lcm_basic() { let mut model = new_empty_model(); - - // Single argument model._set("A1", "=LCM(12)"); - model._set("A2", "=LCM(1)"); - - // Multiple arguments - model._set("A3", "=LCM(25,40)"); - model._set("A4", "=LCM(4,6,8)"); - model._set("A5", "=LCM(12,15,20)"); - - // With zeros (LCM with any zero = 0) - model._set("A6", "=LCM(0)"); - model._set("A7", "=LCM(0,12)"); - model._set("A8", "=LCM(12,0)"); - model._set("A9", "=LCM(10,0,20)"); - - // Decimal inputs (should truncate) - model._set("A10", "=LCM(4.7,6.3)"); - - // Edge cases - model._set("A11", "=LCM(1,1)"); - model._set("A12", "=LCM(1,2,3,4,5)"); + 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(); - // Large numbers (simpler ones) - model._set("A13", "=LCM(100,150)"); + 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(); - assert_eq!(model._get_text("A1"), *"12"); - assert_eq!(model._get_text("A2"), *"1"); - assert_eq!(model._get_text("A3"), *"200"); - assert_eq!(model._get_text("A4"), *"24"); - assert_eq!(model._get_text("A5"), *"60"); - assert_eq!(model._get_text("A6"), *"0"); - assert_eq!(model._get_text("A7"), *"0"); - assert_eq!(model._get_text("A8"), *"0"); - assert_eq!(model._get_text("A9"), *"0"); - assert_eq!(model._get_text("A10"), *"12"); - assert_eq!(model._get_text("A11"), *"1"); - assert_eq!(model._get_text("A12"), *"60"); - assert_eq!(model._get_text("A13"), *"300"); + // 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(); - - // Negative numbers model._set("A1", "=LCM(-5)"); model._set("A2", "=LCM(12,-8)"); - - // Non-finite values model._set("B1", "=1/0"); // Infinity - model._set("B2", "=0/0"); // NaN model._set("A3", "=LCM(B1)"); - model._set("A4", "=LCM(B2)"); - model._set("A5", "=LCM(12,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!"); - assert_eq!(model._get_text("A4"), *"#DIV/0!"); - assert_eq!(model._get_text("A5"), *"#DIV/0!"); } #[test] -fn test_fn_lcm_ranges_and_mixed() { +fn test_fn_lcm_ranges() { let mut model = new_empty_model(); - - // Range inputs + // Range with numbers model._set("B1", "4"); model._set("B2", "6"); model._set("B3", "8"); model._set("A1", "=LCM(B1:B3)"); - // Mixed inputs (numbers, text, empty cells) + // Range with mixed data (text ignored) model._set("C1", "4"); - model._set("C2", "text"); // Should be ignored + model._set("C2", "text"); model._set("C3", "6"); - // C4 is empty, should be ignored - model._set("C5", "8"); - model._set("A2", "=LCM(C1:C5)"); + model._set("A2", "=LCM(C1:C3)"); - // Zero in range (should return 0) + // Zero in range model._set("D1", "4"); model._set("D2", "0"); - model._set("D3", "6"); - model._set("A3", "=LCM(D1:D3)"); - - // No valid numbers case - model._set("E1", "text"); - model._set("A4", "=LCM(E1,E2)"); + model._set("A3", "=LCM(D1:D2)"); model.evaluate(); assert_eq!(model._get_text("A1"), *"24"); - assert_eq!(model._get_text("A2"), *"24"); + assert_eq!(model._get_text("A2"), *"12"); assert_eq!(model._get_text("A3"), *"0"); - assert_eq!(model._get_text("A4"), *"0"); }