diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index c99316b..eb24277 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -872,12 +872,10 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 2, 0), Function::Combina => args_signature_scalars(arg_count, 2, 0), Function::Sumsq => vec![Signature::Vector; arg_count], - Function::N => args_signature_scalars(arg_count, 1, 0), Function::Sheets => args_signature_scalars(arg_count, 0, 1), Function::Cell => args_signature_scalars(arg_count, 1, 1), Function::Info => args_signature_scalars(arg_count, 1, 1), - Function::Daverage => vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dcount => vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dget => vec![Signature::Vector, Signature::Scalar, Signature::Vector], @@ -890,7 +888,6 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dvarp => vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dstdevp => vec![Signature::Vector, Signature::Scalar, Signature::Vector], - Function::BetaDist => args_signature_scalars(arg_count, 4, 2), Function::BetaInv => args_signature_scalars(arg_count, 3, 2), Function::BinomDist => args_signature_scalars(arg_count, 4, 0), @@ -990,6 +987,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Vector; 2], + Function::Sumx2py2 => vec![Signature::Vector; 2], + Function::Sumxmy2 => vec![Signature::Vector; 2], } } @@ -1015,7 +1015,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Atan => scalar_arguments(args), Function::Atan2 => scalar_arguments(args), Function::Atanh => scalar_arguments(args), - Function::Choose => scalar_arguments(args), // static_analysis_choose(args, cell), + Function::Choose => scalar_arguments(args), Function::Column => not_implemented(args), Function::Columns => not_implemented(args), Function::Cos => scalar_arguments(args), @@ -1062,7 +1062,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Lookup => not_implemented(args), Function::Match => not_implemented(args), Function::Offset => static_analysis_offset(args), - // FIXME: Row could return an array Function::Row => StaticResult::Scalar, Function::Rows => not_implemented(args), Function::Vlookup => not_implemented(args), @@ -1254,7 +1253,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Sheets => scalar_arguments(args), Function::Cell => scalar_arguments(args), Function::Info => scalar_arguments(args), - Function::Dget => not_implemented(args), Function::Dmax => not_implemented(args), Function::Dmin => not_implemented(args), @@ -1267,7 +1265,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Dvar => not_implemented(args), Function::Dvarp => not_implemented(args), Function::Dstdevp => not_implemented(args), - Function::BetaDist => StaticResult::Scalar, Function::BetaInv => StaticResult::Scalar, Function::BinomDist => StaticResult::Scalar, @@ -1324,5 +1321,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::VarA => StaticResult::Scalar, Function::WeibullDist => StaticResult::Scalar, Function::ZTest => StaticResult::Scalar, + Function::Sumx2my2 => StaticResult::Scalar, + Function::Sumx2py2 => StaticResult::Scalar, + Function::Sumxmy2 => StaticResult::Scalar, } } diff --git a/base/src/functions/mathematical_sum.rs b/base/src/functions/mathematical_sum.rs new file mode 100644 index 0000000..915be4d --- /dev/null +++ b/base/src/functions/mathematical_sum.rs @@ -0,0 +1,225 @@ +use crate::expressions::types::CellReferenceIndex; + +use crate::{ + calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, +}; + +type TwoMatricesResult = (i32, i32, Vec>, Vec>); + +// Helper to check if two shapes are the same or compatible 1D shapes +fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool { + (rows1 == rows2 && cols1 == cols2) + || (rows1 == 1 && cols2 == 1 && cols1 == rows2) + || (rows2 == 1 && cols1 == 1 && cols2 == rows1) +} + +impl Model { + pub(crate) fn fn_sumx2my2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let result = match self.fn_get_two_matrices(args, cell) { + Ok(s) => s, + Err(s) => return s, + }; + + let (_, _, values_left, values_right) = result; + + let mut sum = 0.0; + for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) { + let x = x_opt.unwrap_or(0.0); + let y = y_opt.unwrap_or(0.0); + sum += x * x - y * y; + } + + CalcResult::Number(sum) + } + + pub(crate) fn fn_sumx2py2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let result = match self.fn_get_two_matrices(args, cell) { + Ok(s) => s, + Err(s) => return s, + }; + + let (_rows, _cols, values_left, values_right) = result; + + let mut sum = 0.0; + for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) { + let x = x_opt.unwrap_or(0.0); + let y = y_opt.unwrap_or(0.0); + sum += x * x + y * y; + } + + CalcResult::Number(sum) + } + + pub(crate) fn fn_sumxmy2(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let result = match self.fn_get_two_matrices(args, cell) { + Ok(s) => s, + Err(s) => return s, + }; + + let (_, _, values_left, values_right) = result; + + let mut sum = 0.0; + for (x_opt, y_opt) in values_left.into_iter().zip(values_right.into_iter()) { + let x = x_opt.unwrap_or(0.0); + let y = y_opt.unwrap_or(0.0); + let diff = x - y; + sum += diff * diff; + } + + CalcResult::Number(sum) + } + + pub(crate) fn fn_get_two_matrices( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> Result { + if args.len() != 2 { + return Err(CalcResult::new_args_number_error(cell)); + } + let x_range = self.evaluate_node_in_context(&args[0], cell); + let y_range = self.evaluate_node_in_context(&args[1], cell); + + let result = match (x_range, y_range) { + ( + CalcResult::Range { + left: l1, + right: r1, + }, + CalcResult::Range { + left: l2, + right: r2, + }, + ) => { + if l1.sheet != l2.sheet { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + )); + } + let rows1 = r1.row - l1.row + 1; + let cols1 = r1.column - l1.column + 1; + let rows2 = r2.row - l2.row + 1; + let cols2 = r2.column - l2.column + 1; + if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Ranges must be of the same shape".to_string(), + )); + } + let values_left = self.values_from_range(l1, r1)?; + let values_right = self.values_from_range(l2, r2)?; + (rows1, cols1, values_left, values_right) + } + ( + CalcResult::Array(left), + CalcResult::Range { + left: l2, + right: r2, + }, + ) => { + let rows2 = r2.row - l2.row + 1; + let cols2 = r2.column - l2.column + 1; + + let rows1 = left.len() as i32; + let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; + if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Array and range must be of the same shape".to_string(), + )); + } + let values_left = match self.values_from_array(left) { + Err(error) => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + format!("Error in first array: {:?}", error), + )); + } + Ok(v) => v, + }; + let values_right = self.values_from_range(l2, r2)?; + (rows2, cols2, values_left, values_right) + } + ( + CalcResult::Range { + left: l1, + right: r1, + }, + CalcResult::Array(right), + ) => { + let rows1 = r1.row - l1.row + 1; + let cols1 = r1.column - l1.column + 1; + + let rows2 = right.len() as i32; + let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; + if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Range and array must be of the same shape".to_string(), + )); + } + let values_left = self.values_from_range(l1, r1)?; + let values_right = match self.values_from_array(right) { + Err(error) => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + format!("Error in second array: {:?}", error), + )); + } + Ok(v) => v, + }; + (rows1, cols1, values_left, values_right) + } + (CalcResult::Array(left), CalcResult::Array(right)) => { + let rows1 = left.len() as i32; + let rows2 = right.len() as i32; + let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; + let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; + + if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Arrays must be of the same shape".to_string(), + )); + } + let values_left = match self.values_from_array(left) { + Err(error) => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + format!("Error in first array: {:?}", error), + )); + } + Ok(v) => v, + }; + let values_right = match self.values_from_array(right) { + Err(error) => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + format!("Error in second array: {:?}", error), + )); + } + Ok(v) => v, + }; + (rows1, cols1, values_left, values_right) + } + _ => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Both arguments must be ranges or arrays".to_string(), + )); + } + }; + Ok(result) + } +} diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 47f82b5..a522330 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -19,6 +19,7 @@ mod lookup_and_reference; mod macros; mod math_util; mod mathematical; +mod mathematical_sum; mod statistical; mod subtotal; mod text; @@ -76,6 +77,9 @@ pub enum Function { Sum, Sumif, Sumifs, + Sumx2my2, + Sumx2py2, + Sumxmy2, Tan, Tanh, Acot, @@ -420,7 +424,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -497,6 +501,9 @@ impl Function { Function::Sum, Function::Sumif, Function::Sumifs, + Function::Sumx2my2, + Function::Sumx2py2, + Function::Sumxmy2, Function::Choose, Function::Column, Function::Columns, @@ -1224,6 +1231,9 @@ impl Function { "VARA" => Some(Function::VarA), "WEIBULL.DIST" | "_XLFN.WEIBULL.DIST" => Some(Function::WeibullDist), "Z.TEST" | "_XLFN.Z.TEST" => Some(Function::ZTest), + "SUMX2MY2" => Some(Function::Sumx2my2), + "SUMX2PY2" => Some(Function::Sumx2py2), + "SUMXMY2" => Some(Function::Sumxmy2), _ => None, } @@ -1560,6 +1570,9 @@ impl fmt::Display for Function { Function::VarA => write!(f, "VARA"), Function::WeibullDist => write!(f, "WEIBULL.DIST"), Function::ZTest => write!(f, "Z.TEST"), + Function::Sumx2my2 => write!(f, "SUMX2MY2"), + Function::Sumx2py2 => write!(f, "SUMX2PY2"), + Function::Sumxmy2 => write!(f, "SUMXMY2"), } } } @@ -1913,6 +1926,9 @@ impl Model { Function::VarA => self.fn_vara(args, cell), Function::WeibullDist => self.fn_weibull_dist(args, cell), Function::ZTest => self.fn_z_test(args, cell), + Function::Sumx2my2 => self.fn_sumx2my2(args, cell), + Function::Sumx2py2 => self.fn_sumx2py2(args, cell), + Function::Sumxmy2 => self.fn_sumxmy2(args, cell), } } } diff --git a/base/src/functions/statistical/chisq.rs b/base/src/functions/statistical/chisq.rs index e997f00..fa9f16a 100644 --- a/base/src/functions/statistical/chisq.rs +++ b/base/src/functions/statistical/chisq.rs @@ -6,13 +6,6 @@ use crate::{ calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, }; -// Helper to check if two shapes are the same or compatible 1D shapes -pub(crate) fn is_same_shape_or_1d(rows1: i32, cols1: i32, rows2: i32, cols2: i32) -> bool { - (rows1 == rows2 && cols1 == cols2) - || (rows1 == 1 && cols2 == 1 && cols1 == rows2) - || (rows2 == 1 && cols1 == 1 && cols2 == rows1) -} - impl Model { // CHISQ.DIST(x, deg_freedom, cumulative) pub(crate) fn fn_chisq_dist(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { @@ -310,171 +303,10 @@ impl Model { // CHISQ.TEST(actual_range, expected_range) pub(crate) fn fn_chisq_test(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); - } - let actual_range = self.evaluate_node_in_context(&args[0], cell); - let expected_range = self.evaluate_node_in_context(&args[1], cell); - - let (width, height, values_left, values_right) = match (actual_range, expected_range) { - ( - CalcResult::Range { - left: l1, - right: r1, - }, - CalcResult::Range { - left: l2, - right: r2, - }, - ) => { - if l1.sheet != l2.sheet { - return CalcResult::new_error( - Error::VALUE, - cell, - "Ranges are in different sheets".to_string(), - ); - } - let rows1 = r1.row - l1.row + 1; - let cols1 = r1.column - l1.column + 1; - let rows2 = r2.row - l2.row + 1; - let cols2 = r2.column - l2.column + 1; - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Ranges must be of the same shape".to_string(), - ); - } - let values_left = match self.values_from_range(l1, r1) { - Err(error) => { - return error; - } - Ok(v) => v, - }; - let values_right = match self.values_from_range(l2, r2) { - Err(error) => { - return error; - } - Ok(v) => v, - }; - (rows1, cols1, values_left, values_right) - } - ( - CalcResult::Array(left), - CalcResult::Range { - left: l2, - right: r2, - }, - ) => { - let rows2 = r2.row - l2.row + 1; - let cols2 = r2.column - l2.column + 1; - - let rows1 = left.len() as i32; - let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Array and range must be of the same shape".to_string(), - ); - } - let values_left = match self.values_from_array(left) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in first array: {:?}", error), - ); - } - Ok(v) => v, - }; - let values_right = match self.values_from_range(l2, r2) { - Err(error) => { - return error; - } - Ok(v) => v, - }; - (rows2, cols2, values_left, values_right) - } - ( - CalcResult::Range { - left: l1, - right: r1, - }, - CalcResult::Array(right), - ) => { - let rows1 = r1.row - l1.row + 1; - let cols1 = r1.column - l1.column + 1; - - let rows2 = right.len() as i32; - let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Range and array must be of the same shape".to_string(), - ); - } - let values_left = match self.values_from_range(l1, r1) { - Err(error) => { - return error; - } - Ok(v) => v, - }; - let values_right = match self.values_from_array(right) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in second array: {:?}", error), - ); - } - Ok(v) => v, - }; - (rows1, cols1, values_left, values_right) - } - (CalcResult::Array(left), CalcResult::Array(right)) => { - let rows1 = left.len() as i32; - let rows2 = right.len() as i32; - let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; - let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; - - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Arrays must be of the same shape".to_string(), - ); - } - let values_left = match self.values_from_array(left) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in first array: {:?}", error), - ); - } - Ok(v) => v, - }; - let values_right = match self.values_from_array(right) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in second array: {:?}", error), - ); - } - Ok(v) => v, - }; - (rows1, cols1, values_left, values_right) - } - _ => { - return CalcResult::new_error( - Error::VALUE, - cell, - "Both arguments must be ranges or arrays".to_string(), - ); - } + let (width, height, values_left, values_right) = match self.fn_get_two_matrices(args, cell) + { + Ok(v) => v, + Err(r) => return r, }; let mut values = Vec::with_capacity(values_left.len()); diff --git a/base/src/functions/statistical/pearson.rs b/base/src/functions/statistical/pearson.rs index 5b3ebd3..a72da17 100644 --- a/base/src/functions/statistical/pearson.rs +++ b/base/src/functions/statistical/pearson.rs @@ -1,5 +1,4 @@ use crate::expressions::types::CellReferenceIndex; -use crate::functions::statistical::chisq::is_same_shape_or_1d; use crate::{ calc_result::CalcResult, expressions::parser::Node, expressions::token::Error, model::Model, }; @@ -7,176 +6,9 @@ use crate::{ impl Model { // PEARSON(array1, array2) pub(crate) fn fn_pearson(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { - if args.len() != 2 { - return CalcResult::new_args_number_error(cell); - } - - let left_arg = self.evaluate_node_in_context(&args[0], cell); - let right_arg = self.evaluate_node_in_context(&args[1], cell); - - let (values_left, values_right) = match (left_arg, right_arg) { - ( - CalcResult::Range { - left: l1, - right: r1, - }, - CalcResult::Range { - left: l2, - right: r2, - }, - ) => { - if l1.sheet != l2.sheet { - return CalcResult::new_error( - Error::VALUE, - cell, - "Ranges are in different sheets".to_string(), - ); - } - - let rows1 = r1.row - l1.row + 1; - let cols1 = r1.column - l1.column + 1; - let rows2 = r2.row - l2.row + 1; - let cols2 = r2.column - l2.column + 1; - - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Ranges must be of the same shape".to_string(), - ); - } - - let values_left = match self.values_from_range(l1, r1) { - Err(error) => return error, - Ok(v) => v, - }; - let values_right = match self.values_from_range(l2, r2) { - Err(error) => return error, - Ok(v) => v, - }; - - (values_left, values_right) - } - ( - CalcResult::Array(left), - CalcResult::Range { - left: l2, - right: r2, - }, - ) => { - let rows2 = r2.row - l2.row + 1; - let cols2 = r2.column - l2.column + 1; - - let rows1 = left.len() as i32; - let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; - - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Array and range must be of the same shape".to_string(), - ); - } - - let values_left = match self.values_from_array(left) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in first array: {:?}", error), - ); - } - Ok(v) => v, - }; - let values_right = match self.values_from_range(l2, r2) { - Err(error) => return error, - Ok(v) => v, - }; - - (values_left, values_right) - } - ( - CalcResult::Range { - left: l1, - right: r1, - }, - CalcResult::Array(right), - ) => { - let rows1 = r1.row - l1.row + 1; - let cols1 = r1.column - l1.column + 1; - - let rows2 = right.len() as i32; - let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; - - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Range and array must be of the same shape".to_string(), - ); - } - - let values_left = match self.values_from_range(l1, r1) { - Err(error) => return error, - Ok(v) => v, - }; - let values_right = match self.values_from_array(right) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in second array: {:?}", error), - ); - } - Ok(v) => v, - }; - - (values_left, values_right) - } - (CalcResult::Array(left), CalcResult::Array(right)) => { - let rows1 = left.len() as i32; - let rows2 = right.len() as i32; - let cols1 = if rows1 > 0 { left[0].len() as i32 } else { 0 }; - let cols2 = if rows2 > 0 { right[0].len() as i32 } else { 0 }; - - if !is_same_shape_or_1d(rows1, cols1, rows2, cols2) { - return CalcResult::new_error( - Error::VALUE, - cell, - "Arrays must be of the same shape".to_string(), - ); - } - - let values_left = match self.values_from_array(left) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in first array: {:?}", error), - ); - } - Ok(v) => v, - }; - let values_right = match self.values_from_array(right) { - Err(error) => { - return CalcResult::new_error( - Error::VALUE, - cell, - format!("Error in second array: {:?}", error), - ); - } - Ok(v) => v, - }; - - (values_left, values_right) - } - _ => { - return CalcResult::new_error( - Error::VALUE, - cell, - "Both arguments must be ranges or arrays".to_string(), - ); - } + let (_, _, values_left, values_right) = match self.fn_get_two_matrices(args, cell) { + Ok(result) => result, + Err(e) => return e, }; // Flatten into (x, y) pairs, skipping non-numeric entries (None) @@ -228,8 +60,7 @@ impl Model { } let denom = (denom_x * denom_y).sqrt(); - let r = num / denom; - CalcResult::Number(r) + CalcResult::Number(num / denom) } } diff --git a/xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx b/xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx new file mode 100644 index 0000000..988ff57 Binary files /dev/null and b/xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx differ