diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 9dd2fb9..1e9aeb7 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -883,6 +883,12 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dmin => vec![Signature::Vector, Signature::Scalar, Signature::Vector], Function::Dsum => vec![Signature::Vector, Signature::Scalar, Signature::Vector], + Function::Dcounta => vec![Signature::Vector, Signature::Scalar, Signature::Vector], + Function::Dproduct => vec![Signature::Vector, Signature::Scalar, Signature::Vector], + Function::Dstdev => vec![Signature::Vector, Signature::Scalar, Signature::Vector], + Function::Dvar => 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], } } @@ -1153,5 +1159,11 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Dcount => not_implemented(args), Function::Daverage => not_implemented(args), Function::Dsum => not_implemented(args), + Function::Dcounta => not_implemented(args), + Function::Dproduct => not_implemented(args), + Function::Dstdev => not_implemented(args), + Function::Dvar => not_implemented(args), + Function::Dvarp => not_implemented(args), + Function::Dstdevp => not_implemented(args), } } diff --git a/base/src/functions/database.rs b/base/src/functions/database.rs index 8930eac..99596d0 100644 --- a/base/src/functions/database.rs +++ b/base/src/functions/database.rs @@ -249,6 +249,264 @@ impl Model { CalcResult::Number(sum) } + // =DCOUNTA(database, field, criteria) + // Counts non-empty entries (any type) in the field for rows that match criteria + pub(crate) fn fn_dcounta(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + + let (db_left, db_right) = match self.get_reference(&args[0], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return e, + }; + + let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) { + Ok(c) => c, + Err(e) => return e, + }; + + let criteria = match self.get_reference(&args[2], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return e, + }; + + if db_right.row <= db_left.row { + // no data rows + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No data rows in database".to_string(), + }; + } + + let mut count = 0; + for row in (db_left.row + 1)..=db_right.row { + if self.db_row_matches_criteria(db_left, db_right, row, criteria) { + let v = self.evaluate_cell(CellReferenceIndex { + sheet: db_left.sheet, + row, + column: field_col, + }); + if !matches!(v, CalcResult::EmptyCell | CalcResult::EmptyArg) { + count += 1; + } + } + } + + CalcResult::Number(count as f64) + } + + // =DPRODUCT(database, field, criteria) + pub(crate) fn fn_dproduct(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + + let (db_left, db_right) = match self.get_reference(&args[0], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return e, + }; + + let field_col = match self.resolve_db_field_column(db_left, db_right, &args[1], cell) { + Ok(c) => c, + Err(e) => return e, + }; + + let criteria = match self.get_reference(&args[2], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return e, + }; + + if db_right.row <= db_left.row { + // no data rows + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No data rows in database".to_string(), + }; + } + + let mut product = 1.0f64; + let mut has_numeric = false; + + let mut row = db_left.row + 1; // skip header + while row <= db_right.row { + if self.db_row_matches_criteria(db_left, db_right, row, criteria) { + let v = self.evaluate_cell(CellReferenceIndex { + sheet: db_left.sheet, + row, + column: field_col, + }); + if let CalcResult::Number(n) = v { + if n.is_finite() { + product *= n; + has_numeric = true; + } + } + } + row += 1; + } + + // Excel returns 0 when no rows / no numeric values match for DPRODUCT + if has_numeric { + CalcResult::Number(product) + } else { + CalcResult::Number(0.0) + } + } + + // Small internal helper for DSTDEV / DVAR + // Collects sum, sum of squares, and count of numeric values in the field + // for rows that match the criteria. + fn db_numeric_stats( + &mut self, + args: &[Node], + cell: CellReferenceIndex, + ) -> Result<(f64, f64, usize), CalcResult> { + if args.len() != 3 { + return Err(CalcResult::new_args_number_error(cell)); + } + + let (db_left, db_right) = match self.get_reference(&args[0], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return Err(e), + }; + + let field_col = self.resolve_db_field_column(db_left, db_right, &args[1], cell)?; + + let criteria = match self.get_reference(&args[2], cell) { + Ok(r) => (r.left, r.right), + Err(e) => return Err(e), + }; + + if db_right.row <= db_left.row { + // no data rows + return Err(CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "No data rows in database".to_string(), + }); + } + + let mut sum = 0.0f64; + let mut sumsq = 0.0f64; + let mut count = 0usize; + + let mut row = db_left.row + 1; // skip header + while row <= db_right.row { + if self.db_row_matches_criteria(db_left, db_right, row, criteria) { + let v = self.evaluate_cell(CellReferenceIndex { + sheet: db_left.sheet, + row, + column: field_col, + }); + if let CalcResult::Number(n) = v { + if n.is_finite() { + sum += n; + sumsq += n * n; + count += 1; + } + } + } + row += 1; + } + + Ok((sum, sumsq, count)) + } + + // =DSTDEV(database, field, criteria) + // Sample standard deviation of matching numeric values + pub(crate) fn fn_dstdev(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) { + Ok(stats) => stats, + Err(e) => return e, + }; + + // Excel behaviour: #DIV/0! if 0 or 1 numeric values match + if count < 2 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Not enough numeric values matched criteria".to_string(), + }; + } + + let n = count as f64; + let var = (sumsq - (sum * sum) / n) / (n - 1.0); + let var = if var < 0.0 { 0.0 } else { var }; + CalcResult::Number(var.sqrt()) + } + + // =DVAR(database, field, criteria) + // Sample variance of matching numeric values + pub(crate) fn fn_dvar(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) { + Ok(stats) => stats, + Err(e) => return e, + }; + + // Excel behaviour: #DIV/0! if 0 or 1 numeric values match + if count < 2 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Not enough numeric values matched criteria".to_string(), + }; + } + + let n = count as f64; + let var = (sumsq - (sum * sum) / n) / (n - 1.0); + let var = if var < 0.0 { 0.0 } else { var }; + CalcResult::Number(var) + } + + // =DSTDEVP(database, field, criteria) + // Population standard deviation of matching numeric values + pub(crate) fn fn_dstdevp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) { + Ok(stats) => stats, + Err(e) => return e, + }; + + // Excel behaviour: #DIV/0! if no numeric values match + if count == 0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "No numeric values matched criteria".to_string(), + }; + } + + let n = count as f64; + let var = (sumsq - (sum * sum) / n) / n; + let var = if var < 0.0 { 0.0 } else { var }; + CalcResult::Number(var.sqrt()) + } + + // =DVARP(database, field, criteria) + // Population variance of matching numeric values + pub(crate) fn fn_dvarp(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + let (sum, sumsq, count) = match self.db_numeric_stats(args, cell) { + Ok(stats) => stats, + Err(e) => return e, + }; + + // Excel behaviour: #DIV/0! if no numeric values match + if count == 0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "No numeric values matched criteria".to_string(), + }; + } + + let n = count as f64; + let var = (sumsq - (sum * sum) / n) / n; + let var = if var < 0.0 { 0.0 } else { var }; + CalcResult::Number(var) + } + /// Resolve the "field" (2nd arg) to an absolute column index (i32) within the sheet. /// Field can be a number (1-based index) or a header name (case-insensitive). /// Returns the absolute column index, not a 1-based offset within the database range. @@ -544,15 +802,7 @@ impl Model { _ => false, } } else { - let rhs = CalcResult::Number(t); - let c = compare_values(db_val, &rhs); - match op { - ">" => c > 0, - ">=" => c >= 0, - "<" => c < 0, - "<=" => c <= 0, - _ => false, - } + false } } else { // string comparison (case-insensitive) using compare_values semantics diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index b3b321b..7b84410 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -319,10 +319,16 @@ pub enum Function { Dmax, Dmin, Dsum, + Dcounta, + Dproduct, + Dstdev, + Dvar, + Dvarp, + Dstdevp, } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -586,6 +592,12 @@ impl Function { Function::Dmax, Function::Dmin, Function::Dsum, + Function::Dcounta, + Function::Dproduct, + Function::Dstdev, + Function::Dvar, + Function::Dvarp, + Function::Dstdevp, ] .into_iter() } @@ -938,6 +950,12 @@ impl Function { "DMAX" => Some(Function::Dmax), "DMIN" => Some(Function::Dmin), "DSUM" => Some(Function::Dsum), + "DCOUNTA" => Some(Function::Dcounta), + "DPRODUCT" => Some(Function::Dproduct), + "DSTDEV" => Some(Function::Dstdev), + "DVAR" => Some(Function::Dvar), + "DVARP" => Some(Function::Dvarp), + "DSTDEVP" => Some(Function::Dstdevp), _ => None, } @@ -1210,6 +1228,12 @@ impl fmt::Display for Function { Function::Dmax => write!(f, "DMAX"), Function::Dmin => write!(f, "DMIN"), Function::Dsum => write!(f, "DSUM"), + Function::Dcounta => write!(f, "DCOUNTA"), + Function::Dproduct => write!(f, "DPRODUCT"), + Function::Dstdev => write!(f, "DSTDEV"), + Function::Dvar => write!(f, "DVAR"), + Function::Dvarp => write!(f, "DVARP"), + Function::Dstdevp => write!(f, "DSTDEVP"), } } } @@ -1500,6 +1524,12 @@ impl Model { Function::Dmax => self.fn_dmax(args, cell), Function::Dmin => self.fn_dmin(args, cell), Function::Dsum => self.fn_dsum(args, cell), + Function::Dcounta => self.fn_dcounta(args, cell), + Function::Dproduct => self.fn_dproduct(args, cell), + Function::Dstdev => self.fn_dstdev(args, cell), + Function::Dvar => self.fn_dvar(args, cell), + Function::Dvarp => self.fn_dvarp(args, cell), + Function::Dstdevp => self.fn_dstdevp(args, cell), } } } diff --git a/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx b/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx new file mode 100644 index 0000000..20d7133 Binary files /dev/null and b/xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx differ