Merge pull request #751 from marcoarnulfo:fix/dget-empty-criteria-results
This commit is contained in:
commit
8b75c7c29f
|
|
@ -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<Area>,
|
||||
}
|
||||
|
||||
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<H2` with column H
|
||||
/// outside the database) is read here but not tracked, so editing that cell doesn't recalculate
|
||||
/// the D-function until something else does.
|
||||
fn computed_criterion(&mut self, row: usize, col: usize, db: &Array, record: usize) -> Option<bool> {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<Option<usize>> {
|
|||
fn cell_criterion(v: &Value) -> Option<Criterion> {
|
||||
match v {
|
||||
Value::Empty => None,
|
||||
Value::Text(t) if t.is_empty() => None,
|
||||
Value::Text(t) => {
|
||||
let s: &str = t;
|
||||
let has_op = s.starts_with(['=', '<', '>']);
|
||||
|
|
@ -65,9 +66,10 @@ fn cell_criterion(v: &Value) -> Option<Criterion> {
|
|||
}
|
||||
|
||||
/// Rows (1-based within the database, excluding the header) of matching records.
|
||||
fn matching_records(db: &Array, crit: &Array) -> Vec<usize> {
|
||||
// Per criteria row: list of (database column or None when the label is unknown, criterion).
|
||||
let mut rows: Vec<Vec<(Option<usize>, Criterion)>> = Vec::new();
|
||||
fn matching_records(db: &Array, crit: &Array, ctx: &mut dyn Ctx) -> Vec<usize> {
|
||||
// Per criteria row: its index and a list of (criteria column, database column or None when
|
||||
// the label is unknown, criterion).
|
||||
let mut rows: Vec<(usize, Vec<(usize, Option<usize>, Criterion)>)> = Vec::new();
|
||||
for r in 1..crit.rows {
|
||||
let mut conds = Vec::new();
|
||||
for c in 0..crit.cols {
|
||||
|
|
@ -78,18 +80,20 @@ fn matching_records(db: &Array, crit: &Array) -> Vec<usize> {
|
|||
Some(Value::Number(n)) => find_column(db, &gridcraft_core::number_to_text(*n)),
|
||||
_ => None,
|
||||
};
|
||||
conds.push((col, cr));
|
||||
conds.push((c, col, cr));
|
||||
}
|
||||
rows.push(conds);
|
||||
rows.push((r, conds));
|
||||
}
|
||||
let all_match = rows.is_empty();
|
||||
(1..db.rows)
|
||||
.filter(|&r| {
|
||||
.filter(|&rec| {
|
||||
all_match
|
||||
|| rows.iter().any(|conds| {
|
||||
conds.iter().all(|(col, cr)| match col {
|
||||
Some(c) => cr.matches(db.get(r, *c).unwrap_or(&Value::Empty)),
|
||||
None => false,
|
||||
|| rows.iter().any(|(r, conds)| {
|
||||
conds.iter().all(|(c, col, cr)| match col {
|
||||
Some(f) => cr.matches(db.get(rec, *f).unwrap_or(&Value::Empty)),
|
||||
// Under a label that names no field, only a formula (a computed
|
||||
// criterion) can hold.
|
||||
None => ctx.computed_criterion(*r, *c, db, rec).unwrap_or(false),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -102,7 +106,7 @@ struct Selection<'a> {
|
|||
records: Vec<usize>,
|
||||
}
|
||||
|
||||
fn select(a: &[Arg]) -> R<Selection<'_>> {
|
||||
fn select<'a>(a: &'a [Arg], ctx: &mut dyn Ctx) -> R<Selection<'a>> {
|
||||
let dbv = &a.first().ok_or(CellError::Value)?.value;
|
||||
let critv = &a.get(2).ok_or(CellError::Value)?.value;
|
||||
if let Value::Error(e) = dbv {
|
||||
|
|
@ -114,31 +118,31 @@ fn select(a: &[Arg]) -> R<Selection<'_>> {
|
|||
let db = as_array(dbv);
|
||||
let crit = as_array(critv);
|
||||
let field = field_index(&db, a)?;
|
||||
let records = matching_records(&db, &crit);
|
||||
let records = matching_records(&db, &crit, ctx);
|
||||
Ok(Selection { db, field, records })
|
||||
}
|
||||
|
||||
/// Numbers of the field among matching records (field required).
|
||||
fn field_numbers(a: &[Arg]) -> R<Vec<f64>> {
|
||||
let s = select(a)?;
|
||||
fn field_numbers(a: &[Arg], c: &mut dyn Ctx) -> R<Vec<f64>> {
|
||||
let s = select(a, c)?;
|
||||
let f = s.field.ok_or(CellError::Value)?;
|
||||
Ok(s.records.iter().filter_map(|&r| s.db.get(r, f).and_then(Value::as_f64)).collect())
|
||||
}
|
||||
|
||||
fn dsum(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(field_numbers(a)?.iter().sum())
|
||||
fn dsum(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(field_numbers(a, c)?.iter().sum())
|
||||
}
|
||||
|
||||
fn daverage(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a)?;
|
||||
fn daverage(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a, c)?;
|
||||
if v.is_empty() {
|
||||
return Err(CellError::Div0);
|
||||
}
|
||||
num_val(v.iter().sum::<f64>() / v.len() as f64)
|
||||
}
|
||||
|
||||
fn dcount(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a)?;
|
||||
fn dcount(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a, c)?;
|
||||
let n = match s.field {
|
||||
Some(f) => s.records.iter().filter(|&&r| matches!(s.db.get(r, f), Some(Value::Number(_)))).count(),
|
||||
None => s.records.len(),
|
||||
|
|
@ -146,8 +150,8 @@ fn dcount(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||
num_val(n as f64)
|
||||
}
|
||||
|
||||
fn dcounta(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a)?;
|
||||
fn dcounta(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a, c)?;
|
||||
let n = match s.field {
|
||||
Some(f) => s.records.iter().filter(|&&r| !matches!(s.db.get(r, f), None | Some(Value::Empty))).count(),
|
||||
None => s.records.len(),
|
||||
|
|
@ -155,28 +159,31 @@ fn dcounta(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||
num_val(n as f64)
|
||||
}
|
||||
|
||||
fn dget(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a)?;
|
||||
fn dget(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let s = select(a, c)?;
|
||||
let f = s.field.ok_or(CellError::Value)?;
|
||||
match s.records.as_slice() {
|
||||
[] => Err(CellError::Value),
|
||||
[r] => Ok(s.db.get(*r, f).cloned().unwrap_or(Value::Empty)),
|
||||
// Records whose field is blank (or "") don't count, as in Excel.
|
||||
let mut found =
|
||||
s.records.iter().filter_map(|&r| s.db.get(r, f)).filter(|v| !matches!(v, Value::Empty) && !matches!(v, Value::Text(t) if t.is_empty()));
|
||||
match (found.next(), found.next()) {
|
||||
(None, _) => Err(CellError::Value),
|
||||
(Some(v), None) => Ok(v.clone()),
|
||||
_ => Err(CellError::Num),
|
||||
}
|
||||
}
|
||||
|
||||
fn dmax(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a)?;
|
||||
fn dmax(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a, c)?;
|
||||
num_val(v.iter().copied().reduce(f64::max).unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn dmin(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a)?;
|
||||
fn dmin(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a, c)?;
|
||||
num_val(v.iter().copied().reduce(f64::min).unwrap_or(0.0))
|
||||
}
|
||||
|
||||
fn dproduct(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a)?;
|
||||
fn dproduct(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
let v = field_numbers(a, c)?;
|
||||
if v.is_empty() {
|
||||
return num_val(0.0);
|
||||
}
|
||||
|
|
@ -194,20 +201,20 @@ fn variance(v: &[f64], sample: bool) -> R<f64> {
|
|||
Ok(ss / if sample { n - 1.0 } else { n })
|
||||
}
|
||||
|
||||
fn dvar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a)?, true)?)
|
||||
fn dvar(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a, c)?, true)?)
|
||||
}
|
||||
|
||||
fn dvarp(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a)?, false)?)
|
||||
fn dvarp(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a, c)?, false)?)
|
||||
}
|
||||
|
||||
fn dstdev(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a)?, true)?.sqrt())
|
||||
fn dstdev(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a, c)?, true)?.sqrt())
|
||||
}
|
||||
|
||||
fn dstdevp(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a)?, false)?.sqrt())
|
||||
fn dstdevp(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(variance(&field_numbers(a, c)?, false)?.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn specs() -> Vec<FnSpec> {
|
||||
|
|
@ -371,4 +378,69 @@ mod tests {
|
|||
is_err(ev("DSUM", vec![e(CellError::Ref), t("Yield"), rf(c("=Cherry"))]), CellError::Ref);
|
||||
close(ev("DCOUNTA", vec![rf(db()), empty(), rf(c("=Pear"))]), 2.0);
|
||||
}
|
||||
|
||||
/// Expected results are Excel's, from Apache POI's `DGet.xls` (rows 74-86).
|
||||
#[test]
|
||||
fn empty_string_criterion() {
|
||||
let db = arr(vec![
|
||||
vec![tv("Index"), tv("Value")],
|
||||
vec![tv("a"), tv("=")],
|
||||
vec![tv("b"), Value::Empty],
|
||||
vec![tv("c"), tv("")],
|
||||
vec![tv("d"), tv(" ")],
|
||||
vec![tv("f"), Value::Error(CellError::Value)],
|
||||
vec![tv("h"), tv("abc")],
|
||||
vec![tv("i"), nv(0.0)],
|
||||
]);
|
||||
let get = |index: &str| {
|
||||
let crit = arr(vec![vec![tv("Index"), tv("Value")], vec![tv(index), tv("")]]);
|
||||
ev("DGET", vec![rf(db.clone()), t("Index"), rf(crit)])
|
||||
};
|
||||
// Like any plain text, "" is a "begins with" pattern: it matches every text value.
|
||||
is_text(get("c"), "c");
|
||||
is_text(get("h"), "h");
|
||||
is_err(get("b"), CellError::Value);
|
||||
is_err(get("i"), CellError::Value);
|
||||
is_err(get("f"), CellError::Value);
|
||||
let all = arr(vec![vec![tv("Value")], vec![tv("")]]);
|
||||
close(ev("DCOUNTA", vec![rf(db.clone()), t("Index"), rf(all)]), 4.0);
|
||||
|
||||
// Under a label that names no field, "" is a condition that never holds.
|
||||
let db = arr(vec![vec![tv("One"), tv("Two"), tv("Three")], vec![tv("a"), tv("c"), tv("x")], vec![tv("b"), tv("d"), tv("y")]]);
|
||||
let get = |label: Value, value: Value| {
|
||||
let crit = arr(vec![vec![tv("One"), label, tv("Three")], vec![tv("a"), value, tv("x")]]);
|
||||
ev("DGET", vec![rf(db.clone()), t("One"), rf(crit)])
|
||||
};
|
||||
is_text(get(Value::Empty, Value::Empty), "a");
|
||||
is_text(get(tv(""), Value::Empty), "a");
|
||||
is_err(get(Value::Empty, tv("")), CellError::Value);
|
||||
is_err(get(tv(""), tv("")), CellError::Value);
|
||||
}
|
||||
|
||||
/// Expected results are Excel's, from Apache POI's `DGet.xls` (rows 108-119).
|
||||
#[test]
|
||||
fn dget_skips_empty_fields() {
|
||||
let db = arr(vec![
|
||||
vec![tv("One"), tv("Two")],
|
||||
vec![tv("a"), Value::Empty],
|
||||
vec![tv("b"), tv(" ")],
|
||||
vec![tv("c"), tv("")],
|
||||
vec![tv("d"), Value::Error(CellError::NA)],
|
||||
vec![tv("j"), Value::Empty],
|
||||
vec![tv("j"), tv("Text")],
|
||||
vec![tv("j"), Value::Empty],
|
||||
vec![tv("k"), Value::Empty],
|
||||
vec![tv("k"), tv("")],
|
||||
]);
|
||||
let get = |one: &str| {
|
||||
let crit = arr(vec![vec![tv("One")], vec![tv(one)]]);
|
||||
ev("DGET", vec![rf(db.clone()), t("Two"), rf(crit)])
|
||||
};
|
||||
is_err(get("a"), CellError::Value);
|
||||
is_text(get("b"), " ");
|
||||
is_err(get("c"), CellError::Value);
|
||||
is_err(get("d"), CellError::NA);
|
||||
is_text(get("j"), "Text");
|
||||
is_err(get("k"), CellError::Value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gridcraft_core::{CellError, DateSystem, Value};
|
||||
use gridcraft_core::{Array, CellError, DateSystem, Value};
|
||||
|
||||
/// Builds a [`FnSpec`] whose implementation returns `Result<Value, CellError>`.
|
||||
///
|
||||
|
|
@ -105,6 +105,12 @@ pub trait Ctx {
|
|||
fn now_serial(&self) -> f64;
|
||||
/// Uniform in [0, 1).
|
||||
fn random(&mut self) -> f64;
|
||||
/// For the database functions: whether the formula in the criteria cell at (`row`, `col`) holds
|
||||
/// for `record` (a row of `db` below its labels). `None` when that cell holds no formula or the
|
||||
/// criteria didn't come from a reference.
|
||||
fn computed_criterion(&mut self, _row: usize, _col: usize, _db: &Array, _record: usize) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub type FnImpl = fn(&[Arg], &mut dyn Ctx) -> Value;
|
||||
|
|
|
|||
Loading…
Reference in New Issue