diff --git a/crates/calc/src/eval.rs b/crates/calc/src/eval.rs
index 2f1bb08..7ce2483 100644
--- a/crates/calc/src/eval.rs
+++ b/crates/calc/src/eval.rs
@@ -88,6 +88,8 @@ pub struct Evaluator<'h> {
struct FnCtx<'a, 'h> {
ev: &'a mut Evaluator<'h>,
+ /// The criteria argument of a database function, when it is a reference.
+ criteria: Option,
}
impl Ctx for FnCtx<'_, '_> {
@@ -100,6 +102,28 @@ impl Ctx for FnCtx<'_, '_> {
fn random(&mut self) -> f64 {
self.ev.host.random()
}
+ /// Excel's "formula as criteria": the formula is written for the first record, so its relative
+ /// references move down to `record`'s row, and the database's labels name its fields.
+ /// Known gap: the shifted references are not dependencies of the calling cell. One that lands
+ /// outside both the database and the criteria cell's own precedents (say `=G2
Option {
+ let area = self.criteria?;
+ let at = area.range.start.offset(row as i64, col as i64).filter(|c| area.range.contains(*c))?;
+ let expr = self.ev.host.workbook().sheet(area.sheet)?.cell(at)?.formula.as_ref()?.expr_arc()?;
+ let shifted = gridcraft_formula::adjust::shift_relative((*expr).clone(), record as i64 - 1, 0);
+ let env = (0..db.cols)
+ .filter_map(|c| match db.get(0, c) {
+ Some(Value::Text(t)) if !t.trim().is_empty() => {
+ Some((t.trim().to_string(), Ev::V(db.get(record, c).cloned().unwrap_or(Value::Empty))))
+ }
+ _ => None,
+ })
+ .collect();
+ let mut ev = Evaluator { host: &mut *self.ev.host, sheet: area.sheet, at, env, depth: self.ev.depth };
+ Some(matches!(ev.value(&shifted).scalar().to_bool(), Ok(true)))
+ }
}
impl<'h> Evaluator<'h> {
@@ -592,7 +616,7 @@ impl<'h> Evaluator<'h> {
let value = self.value(a);
evaluated.push(Arg { value, from_ref: false });
}
- let mut ctx = FnCtx { ev: self };
+ let mut ctx = FnCtx { ev: self, criteria: None };
return gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V);
}
let i = match index.to_number() {
@@ -789,8 +813,17 @@ impl<'h> Evaluator<'h> {
let mut evaluated = Vec::with_capacity(args.len());
// SUMIF/AVERAGEIF: the size of the criteria range (argument 1), which the sum range takes.
let mut criteria_shape = None;
+ // Database functions: the criteria range (argument 2), whose formulas are computed criteria.
+ let mut criteria = None;
for (i, a) in args.iter().enumerate() {
let mut ev = self.eval(a);
+ if spec.category == gridcraft_functions::Category::Database
+ && i == 2
+ && let Ev::R(areas) = &ev
+ && let [area] = areas.as_slice()
+ {
+ criteria = Some(*area);
+ }
if resizes_sum_range(name, args.len()) {
if i == 0 {
criteria_shape = ev_shape(&ev);
@@ -818,7 +851,7 @@ impl<'h> Evaluator<'h> {
};
evaluated.push(Arg { value, from_ref });
}
- let mut ctx = FnCtx { ev: self };
+ let mut ctx = FnCtx { ev: self, criteria };
gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
}
@@ -872,13 +905,13 @@ impl<'h> Evaluator<'h> {
};
let args = [Arg { value, from_ref }];
if kind != Agg::Count {
- match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self }) {
+ match gridcraft_functions::call(sum, &args, &mut FnCtx { ev: self, criteria: None }) {
Value::Number(n) => acc.sum += n,
other => return Err(Ev::V(other)),
}
}
if kind != Agg::Sum {
- match gridcraft_functions::call(count, &args, &mut FnCtx { ev: self }) {
+ match gridcraft_functions::call(count, &args, &mut FnCtx { ev: self, criteria: None }) {
Value::Number(n) => acc.count = acc.count.saturating_add(n as u64),
other => return Err(Ev::V(other)),
}
@@ -1281,7 +1314,7 @@ impl<'h> Evaluator<'h> {
}
}
}
- let mut ctx = FnCtx { ev: self };
+ let mut ctx = FnCtx { ev: self, criteria: None };
return match gridcraft_functions::call(spec, &xargs, &mut ctx) {
Value::Number(p) if p >= 1.0 => {
let k = p as u32 - 1;
diff --git a/crates/calc/src/tests.rs b/crates/calc/src/tests.rs
index 2914734..ee4c4bc 100644
--- a/crates/calc/src/tests.rs
+++ b/crates/calc/src/tests.rs
@@ -1121,6 +1121,56 @@ fn trim_references() {
assert_eq!(t.get("K20"), Value::Error(CellError::Calc));
}
+/// Expected results are Excel's, from Apache POI's `DGet.xls` (rows 46-49): a criteria cell holding
+/// a formula under a label that names no field is evaluated for every record.
+#[test]
+fn database_functions_with_computed_criteria() {
+ let mut t = T::new();
+ let db = [
+ ["name", "age", "size", "headgear", "glasses"],
+ ["Huey", "6", "45", "cap~", "FALSE"],
+ ["Dewey", "7", "fourty-six", "cap?", "FALSE"],
+ ["Louie", "8", "47", "cap*", "FALSE"],
+ ["Donald", "32", "70", "sailor's hat", "FALSE"],
+ ["Scrooge", "62", "65", "top hat", "TRUE"],
+ ];
+ for (r, row) in db.iter().enumerate() {
+ for (c, v) in row.iter().enumerate() {
+ t.set(&format!("{}{}", ["F", "G", "H", "I", "J"][c], r + 2), v);
+ }
+ }
+ t.set("G45", "7");
+ let cases = [
+ // Relative references are to the first record's row and move to each record's row;
+ // `$G$45` stays put.
+ ("G", "=G2<$G$45", "Dewey"),
+ // Column labels stand for the record's fields.
+ ("H", "=glasses=TRUE", "Scrooge"),
+ ("I", "=age<7", "Huey"),
+ ("J", "=size/7=10", "Donald"),
+ // An absolute reference to a cell that is not the first record's.
+ ("K", "=H3=$L$1", "Donald"),
+ ];
+ t.set("L1", "70");
+ for (i, (col, formula, _)) in cases.iter().enumerate() {
+ t.set(&format!("{col}46"), "Formula");
+ t.set(&format!("{col}47"), formula);
+ t.set(&format!("B{}", 46 + i), &format!("=DGET($F$2:$J$7,\"name\",{col}46:{col}47)"));
+ }
+ for (i, (_, formula, want)) in cases.iter().enumerate() {
+ assert_eq!(t.get(&format!("B{}", 46 + i)), Value::text(*want), "{formula}");
+ }
+ // Every database function takes them, ANDed with the other criteria of their row.
+ // Text sorts after numbers, so Dewey's "fourty-six" is > 40.
+ t.set("L47", "=size>40");
+ t.set("M46", "age");
+ t.set("M47", ">6");
+ t.set("B51", "=DCOUNT($F$2:$J$7,\"age\",I46:I47)");
+ t.set("B52", "=DSUM($F$2:$J$7,\"age\",G46:G47)");
+ t.set("B53", "=DCOUNTA($F$2:$J$7,\"name\",L46:M47)");
+ assert_eq!(t.num("B51"), 1.0);
+ assert_eq!(t.num("B52"), 7.0);
+ assert_eq!(t.num("B53"), 4.0);
#[test]
fn offset_extreme_arguments_return_ref() {
let wb = Workbook::new();
diff --git a/crates/functions/src/database.rs b/crates/functions/src/database.rs
index 38e3729..67132e9 100644
--- a/crates/functions/src/database.rs
+++ b/crates/functions/src/database.rs
@@ -4,7 +4,9 @@
//! The database and the criteria are arrays whose first row holds column labels. Each criteria
//! row is a set of conditions that must all hold (AND); a record matches when any criteria row
//! holds (OR). An empty criteria cell matches anything. Plain text conditions match values that
-//! begin with the text, as in Excel's advanced filter.
+//! begin with the text, as in Excel's advanced filter, so an empty string matches any text. A
+//! formula under a label that names no field is a computed criterion, which the host evaluates
+//! for each record ([`Ctx::computed_criterion`]).
use std::borrow::Cow;
@@ -49,7 +51,6 @@ fn field_index(db: &Array, a: &[Arg]) -> R