diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index a2af299..23022a6 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -851,17 +851,19 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 1, 0), Function::Even => args_signature_scalars(arg_count, 1, 0), Function::Odd => args_signature_scalars(arg_count, 1, 0), - Function::Ceiling => args_signature_scalars(arg_count, 2, 0), // (number, significance) - Function::CeilingMath => args_signature_scalars(arg_count, 1, 2), // (number, [significance], [mode]) - Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1), // (number, [significance]) - Function::Floor => args_signature_scalars(arg_count, 2, 0), // (number, significance) - Function::FloorMath => args_signature_scalars(arg_count, 1, 2), // (number, [significance], [mode]) - Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1), // (number, [significance]) - Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1), // (number, [significance]) - Function::Mod => args_signature_scalars(arg_count, 2, 0), // (number, divisor) - Function::Quotient => args_signature_scalars(arg_count, 2, 0), // (number, denominator) - Function::Mround => args_signature_scalars(arg_count, 2, 0), // (number, multiple) - Function::Trunc => args_signature_scalars(arg_count, 1, 1), // (num, [num_digits]) + Function::Ceiling => args_signature_scalars(arg_count, 2, 0), + Function::CeilingMath => args_signature_scalars(arg_count, 1, 2), + Function::CeilingPrecise => args_signature_scalars(arg_count, 1, 1), + Function::Floor => args_signature_scalars(arg_count, 2, 0), + Function::FloorMath => args_signature_scalars(arg_count, 1, 2), + Function::FloorPrecise => args_signature_scalars(arg_count, 1, 1), + Function::IsoCeiling => args_signature_scalars(arg_count, 1, 1), + Function::Mod => args_signature_scalars(arg_count, 2, 0), + Function::Quotient => args_signature_scalars(arg_count, 2, 0), + Function::Mround => args_signature_scalars(arg_count, 2, 0), + Function::Trunc => args_signature_scalars(arg_count, 1, 1), + Function::Gcd => vec![Signature::Vector; arg_count], + Function::Lcm => vec![Signature::Vector; arg_count], } } @@ -1112,5 +1114,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Quotient => scalar_arguments(args), Function::Mround => scalar_arguments(args), Function::Trunc => scalar_arguments(args), + Function::Gcd => not_implemented(args), + Function::Lcm => not_implemented(args), } } diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index af1b066..91ad94e 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -14,6 +14,32 @@ pub fn random() -> f64 { rand::random() } +// Euclidean gcd for i64 (non-negative inputs expected) +fn gcd_i64(mut a: i64, mut b: i64) -> i64 { + while b != 0 { + let r = a % b; + a = b; + b = r; + } + a +} + +// lcm(a, b) = a / gcd(a, b) * b +// we do it in i128 to reduce overflow risk, then back to i64/f64 +fn lcm_i64(a: i64, b: i64) -> Option { + if a == 0 || b == 0 { + return Some(0); + } + let g = gcd_i64(a, b); + let a_div_g = (a / g) as i128; + let prod = a_div_g * (b as i128); + if prod > i64::MAX as i128 { + None + } else { + Some(prod as i64) + } +} + #[cfg(target_arch = "wasm32")] pub fn random() -> f64 { use js_sys::Math; @@ -107,6 +133,297 @@ 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); + } + + let mut acc: Option = None; + let mut saw_number = false; + let mut has_range = false; + + // Returns Some(CalcResult) if an error occurred + let mut handle_number = |value: f64| -> Option { + if !value.is_finite() { + return Some(CalcResult::new_error( + Error::VALUE, + cell, + "Non-finite number in GCD".to_string(), + )); + } + let n = value.trunc() as i64; + if n < 0 { + return Some(CalcResult::new_error( + Error::NUM, + cell, + "GCD only accepts non-negative integers".to_string(), + )); + } + saw_number = true; + acc = Some(match acc { + Some(cur) => gcd_i64(cur, n), + None => n, + }); + None + }; + + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + has_range = true; + let row1 = left.row; + let mut row2 = right.row; + let column1 = left.column; + let mut column2 = right.column; + + if row1 == 1 && row2 == LAST_ROW { + row2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_row, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + if column1 == 1 && column2 == LAST_COLUMN { + column2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_column, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + + for row in row1..=row2 { + for column in column1..=column2 { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // ignore strings / booleans + } + } + } + } + } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + ArrayNode::Error(error) => { + return CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => { + // ignore strings / booleans + } + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // ignore strings / booleans + } + } + } + + if !saw_number && !has_range { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No valid numbers found".to_string(), + }; + } + + CalcResult::Number(acc.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); + } + + let mut acc: Option = None; + let mut saw_number = false; + let mut has_range = false; + + // Returns Some(CalcResult) if an error occurred + let mut handle_number = |value: f64| -> Option { + if !value.is_finite() { + return Some(CalcResult::new_error( + Error::VALUE, + cell, + "Non-finite number in LCM".to_string(), + )); + } + let n = value.trunc() as i64; + if n < 0 { + return Some(CalcResult::new_error( + Error::NUM, + cell, + "LCM only accepts non-negative integers".to_string(), + )); + } + saw_number = true; + acc = Some(match acc { + Some(cur) => match lcm_i64(cur, n) { + Some(v) => v, + None => { + return Some(CalcResult::new_error( + Error::NUM, + cell, + "LCM result too large".to_string(), + )); + } + }, + None => n, + }); + None + }; + + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + has_range = true; + let row1 = left.row; + let mut row2 = right.row; + let column1 = left.column; + let mut column2 = right.column; + + if row1 == 1 && row2 == LAST_ROW { + row2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_row, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + if column1 == 1 && column2 == LAST_COLUMN { + column2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_column, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + + for row in row1..=row2 { + for column in column1..=column2 { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // ignore strings / booleans + } + } + } + } + } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + if let Some(res) = handle_number(value) { + return res; + } + } + ArrayNode::Error(error) => { + return CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => { + // ignore strings / booleans + } + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // ignore strings / booleans + } + } + } + + if !saw_number && !has_range { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No valid numbers found".to_string(), + }; + } + + CalcResult::Number(acc.unwrap_or(0) as f64) + } + pub(crate) fn fn_sum(&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 f65dadc..3f13716 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -108,6 +108,9 @@ pub enum Function { Mround, Trunc, + Gcd, + Lcm, + // Information ErrorType, Formulatext, @@ -301,7 +304,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -361,6 +364,8 @@ impl Function { Function::Quotient, Function::Mround, Function::Trunc, + Function::Gcd, + Function::Lcm, Function::Max, Function::Min, Function::Product, @@ -667,6 +672,9 @@ impl Function { "MROUND" => Some(Function::Mround), "TRUNC" => Some(Function::Trunc), + "GCD" => Some(Function::Gcd), + "LCM" => Some(Function::Lcm), + "PI" => Some(Function::Pi), "ABS" => Some(Function::Abs), "SQRT" => Some(Function::Sqrt), @@ -1128,6 +1136,8 @@ impl fmt::Display for Function { Function::Quotient => write!(f, "QUOTIENT"), Function::Mround => write!(f, "MROUND"), Function::Trunc => write!(f, "TRUNC"), + Function::Gcd => write!(f, "GCD"), + Function::Lcm => write!(f, "LCM"), } } } @@ -1399,6 +1409,8 @@ impl Model { Function::Quotient => self.fn_quotient(args, cell), Function::Mround => self.fn_mround(args, cell), Function::Trunc => self.fn_trunc(args, cell), + Function::Gcd => self.fn_gcd(args, cell), + Function::Lcm => self.fn_lcm(args, cell), } } } diff --git a/xlsx/tests/calc_tests/GCD_LCM.xlsx b/xlsx/tests/calc_tests/GCD_LCM.xlsx new file mode 100644 index 0000000..da0236b Binary files /dev/null and b/xlsx/tests/calc_tests/GCD_LCM.xlsx differ