diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index e7402f4..e4f64e2 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -868,6 +868,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 2, 0), Function::Roman => args_signature_scalars(arg_count, 1, 1), Function::Arabic => args_signature_scalars(arg_count, 1, 0), + Function::Combin => args_signature_scalars(arg_count, 2, 0), + Function::Combina => args_signature_scalars(arg_count, 2, 0), + Function::Sumsq => vec![Signature::Vector; arg_count], } } @@ -1124,5 +1127,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Decimal => scalar_arguments(args), Function::Roman => scalar_arguments(args), Function::Arabic => scalar_arguments(args), + Function::Combin => scalar_arguments(args), + Function::Combina => scalar_arguments(args), + Function::Sumsq => StaticResult::Scalar, } } diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 1333a11..22a16e5 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -633,6 +633,101 @@ impl Model { CalcResult::Number(result) } + pub(crate) fn fn_sumsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut result = 0.0; + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => result += value * value, + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + // TODO: We should do this for all functions that run through ranges + // Running cargo test for the ironcalc takes around .8 seconds with this speedup + // and ~ 3.5 seconds without it. Note that once properly in place sheet.dimension should be almost a noop + 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 + 1 { + for column in column1..(column2 + 1) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(value) => { + result += value * value; + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + } + } + } + } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + result += value * value; + } + ArrayNode::Error(error) => { + return CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => { + // We ignore booleans and strings + } + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + }; + } + CalcResult::Number(result) + } pub(crate) fn fn_product(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.is_empty() { return CalcResult::new_args_number_error(cell); @@ -1465,4 +1560,67 @@ impl Model { }, } } + + pub(crate) fn fn_combin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let n = match self.get_number(&args[0], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + let k = match self.get_number(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if n < 0.0 || k < 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Arguments must be non-negative integers".to_string(), + }; + } + if k > n { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "k cannot be greater than n".to_string(), + }; + } + let k = k as usize; + let mut result = 1.0; + for i in 0..k { + let t = i as f64; + result *= (n - t) / (t + 1.0); + } + CalcResult::Number(result) + } + + pub(crate) fn fn_combina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let n = match self.get_number(&args[0], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + let k = match self.get_number(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if n < 0.0 || k < 0.0 || (n == 0.0 && k > 0.0) { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Arguments must be non-negative integers".to_string(), + }; + } + let k = k as usize; + let mut result = 1.0; + for i in 0..k { + let t = i as f64; + result *= (n + t) / (t + 1.0); + } + CalcResult::Number(result) + } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 3b4bc50..5e994e0 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -111,6 +111,9 @@ pub enum Function { Decimal, Roman, Arabic, + Combin, + Combina, + Sumsq, // Information ErrorType, @@ -305,7 +308,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -556,6 +559,9 @@ impl Function { Function::Subtotal, Function::Roman, Function::Arabic, + Function::Combin, + Function::Combina, + Function::Sumsq, ] .into_iter() } @@ -607,6 +613,7 @@ impl Function { Function::Base => "_xlfn.BASE".to_string(), Function::Decimal => "_xlfn.DECIMAL".to_string(), Function::Arabic => "_xlfn.ARABIC".to_string(), + Function::Combina => "_xlfn.COMBINA".to_string(), _ => self.to_string(), } @@ -696,6 +703,9 @@ impl Function { "SUM" => Some(Function::Sum), "SUMIF" => Some(Function::Sumif), "SUMIFS" => Some(Function::Sumifs), + "COMBIN" => Some(Function::Combin), + "COMBINA" | "_XLFN.COMBINA" => Some(Function::Combina), + "SUMSQ" => Some(Function::Sumsq), // Lookup and Reference "CHOOSE" => Some(Function::Choose), @@ -1141,6 +1151,9 @@ impl fmt::Display for Function { Function::Decimal => write!(f, "DECIMAL"), Function::Roman => write!(f, "ROMAN"), Function::Arabic => write!(f, "ARABIC"), + Function::Combin => write!(f, "COMBIN"), + Function::Combina => write!(f, "COMBINA"), + Function::Sumsq => write!(f, "SUMSQ"), } } } @@ -1418,6 +1431,9 @@ impl Model { Function::Decimal => self.fn_decimal(args, cell), Function::Roman => self.fn_roman(args, cell), Function::Arabic => self.fn_arabic(args, cell), + Function::Combin => self.fn_combin(args, cell), + Function::Combina => self.fn_combina(args, cell), + Function::Sumsq => self.fn_sumsq(args, cell), } } } diff --git a/xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx b/xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx new file mode 100644 index 0000000..b02cc12 Binary files /dev/null and b/xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx differ diff --git a/xlsx/tests/calc_tests/SUMSQ.xlsx b/xlsx/tests/calc_tests/SUMSQ.xlsx new file mode 100644 index 0000000..36d077f Binary files /dev/null and b/xlsx/tests/calc_tests/SUMSQ.xlsx differ