UPDATE: Adds the rest of the DATABASE functions
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
774b447c84
commit
41c8d88b80
@@ -883,6 +883,12 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
|
|||||||
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
Function::Dmax => vec![Signature::Vector, Signature::Scalar, Signature::Vector],
|
||||||
Function::Dmin => 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::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::Dcount => not_implemented(args),
|
||||||
Function::Daverage => not_implemented(args),
|
Function::Daverage => not_implemented(args),
|
||||||
Function::Dsum => 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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,264 @@ impl Model {
|
|||||||
CalcResult::Number(sum)
|
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.
|
/// 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).
|
/// 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.
|
/// Returns the absolute column index, not a 1-based offset within the database range.
|
||||||
@@ -544,15 +802,7 @@ impl Model {
|
|||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let rhs = CalcResult::Number(t);
|
false
|
||||||
let c = compare_values(db_val, &rhs);
|
|
||||||
match op {
|
|
||||||
">" => c > 0,
|
|
||||||
">=" => c >= 0,
|
|
||||||
"<" => c < 0,
|
|
||||||
"<=" => c <= 0,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// string comparison (case-insensitive) using compare_values semantics
|
// string comparison (case-insensitive) using compare_values semantics
|
||||||
|
|||||||
@@ -319,10 +319,16 @@ pub enum Function {
|
|||||||
Dmax,
|
Dmax,
|
||||||
Dmin,
|
Dmin,
|
||||||
Dsum,
|
Dsum,
|
||||||
|
Dcounta,
|
||||||
|
Dproduct,
|
||||||
|
Dstdev,
|
||||||
|
Dvar,
|
||||||
|
Dvarp,
|
||||||
|
Dstdevp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn into_iter() -> IntoIter<Function, 262> {
|
pub fn into_iter() -> IntoIter<Function, 268> {
|
||||||
[
|
[
|
||||||
Function::And,
|
Function::And,
|
||||||
Function::False,
|
Function::False,
|
||||||
@@ -586,6 +592,12 @@ impl Function {
|
|||||||
Function::Dmax,
|
Function::Dmax,
|
||||||
Function::Dmin,
|
Function::Dmin,
|
||||||
Function::Dsum,
|
Function::Dsum,
|
||||||
|
Function::Dcounta,
|
||||||
|
Function::Dproduct,
|
||||||
|
Function::Dstdev,
|
||||||
|
Function::Dvar,
|
||||||
|
Function::Dvarp,
|
||||||
|
Function::Dstdevp,
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
}
|
}
|
||||||
@@ -938,6 +950,12 @@ impl Function {
|
|||||||
"DMAX" => Some(Function::Dmax),
|
"DMAX" => Some(Function::Dmax),
|
||||||
"DMIN" => Some(Function::Dmin),
|
"DMIN" => Some(Function::Dmin),
|
||||||
"DSUM" => Some(Function::Dsum),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -1210,6 +1228,12 @@ impl fmt::Display for Function {
|
|||||||
Function::Dmax => write!(f, "DMAX"),
|
Function::Dmax => write!(f, "DMAX"),
|
||||||
Function::Dmin => write!(f, "DMIN"),
|
Function::Dmin => write!(f, "DMIN"),
|
||||||
Function::Dsum => write!(f, "DSUM"),
|
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::Dmax => self.fn_dmax(args, cell),
|
||||||
Function::Dmin => self.fn_dmin(args, cell),
|
Function::Dmin => self.fn_dmin(args, cell),
|
||||||
Function::Dsum => self.fn_dsum(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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DMIN_DMAX_DAVERAGE_DSUM_DCOUNT_DGET 1.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user