From 4649a0c78cdc4ea75238387cf3d4c828b4a61de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Wed, 26 Nov 2025 19:16:59 +0100 Subject: [PATCH] UPDATE: Adds SUMX2MY2, SUMX2PY2 and SUMXMY2 mathematical functions --- .../src/expressions/parser/static_analysis.rs | 14 +- base/src/functions/mathematical_sum.rs | 225 ++++++++++++++++++ base/src/functions/mod.rs | 18 +- base/src/functions/statistical/chisq.rs | 176 +------------- base/src/functions/statistical/pearson.rs | 177 +------------- .../calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx | Bin 0 -> 9034 bytes 6 files changed, 257 insertions(+), 353 deletions(-) create mode 100644 base/src/functions/mathematical_sum.rs create mode 100644 xlsx/tests/calc_tests/SUMX2MY2_SUMX2PY2_SUMXMY2.xlsx 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 0000000000000000000000000000000000000000..988ff57b894d7ebf5eec596f795ed38592bbaf28 GIT binary patch literal 9034 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 literal 0 HcmV?d00001