mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-09 03:49:54 -06:00
address review: a database function whose computed criterion ran out of work is #CALC!
This commit is contained in:
@@ -151,6 +151,10 @@ pub struct Evaluator<'h> {
|
||||
/// Work this cell still has left (see [`EVAL_WORK`]), shared by every branch of the formula
|
||||
/// and by the formulas it computes on the way (names, computed criteria).
|
||||
work: u64,
|
||||
/// A spend found too little work left. A spend that leaves exactly nothing is fine, so this
|
||||
/// can't be told from `work` alone; a database function checks it after its computed
|
||||
/// criteria ran, whose failures it can't otherwise see.
|
||||
exhausted: bool,
|
||||
/// Computed criteria being evaluated, outermost first: one that is reached again from inside
|
||||
/// itself counts as not met instead of repeating its work at every level.
|
||||
active_criteria: Vec<(usize, CellRef)>,
|
||||
@@ -206,6 +210,7 @@ impl Ctx for FnCtx<'_, '_> {
|
||||
stack_top: self.ev.stack_top,
|
||||
fuel: self.ev.fuel,
|
||||
work: self.ev.work,
|
||||
exhausted: false,
|
||||
active_criteria: std::mem::take(&mut self.ev.active_criteria),
|
||||
};
|
||||
ev.active_criteria.push((area.sheet, at));
|
||||
@@ -213,6 +218,7 @@ impl Ctx for FnCtx<'_, '_> {
|
||||
// The child's spend counts against this cell's budget.
|
||||
self.ev.fuel = ev.fuel;
|
||||
self.ev.work = ev.work;
|
||||
self.ev.exhausted |= ev.exhausted;
|
||||
let mut active = std::mem::take(&mut ev.active_criteria);
|
||||
active.pop();
|
||||
self.ev.active_criteria = active;
|
||||
@@ -245,6 +251,7 @@ impl<'h> Evaluator<'h> {
|
||||
stack_top: 0,
|
||||
fuel: LAMBDA_FUEL,
|
||||
work: EVAL_WORK,
|
||||
exhausted: false,
|
||||
active_criteria: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -258,6 +265,7 @@ impl<'h> Evaluator<'h> {
|
||||
}
|
||||
None => {
|
||||
self.work = 0;
|
||||
self.exhausted = true;
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -763,6 +771,58 @@ mod fuel_tests {
|
||||
matches!(v, Value::Error(CellError::Calc))
|
||||
}
|
||||
|
||||
/// Serves the workbook's stored values, for database functions to read.
|
||||
struct Stored {
|
||||
wb: Workbook,
|
||||
}
|
||||
impl Host for Stored {
|
||||
fn workbook(&self) -> &Workbook {
|
||||
&self.wb
|
||||
}
|
||||
fn cell_value(&mut self, sheet: usize, c: CellRef) -> Value {
|
||||
self.wb.sheet(sheet).and_then(|s| s.cell(c)).map_or(Value::Empty, |cell| cell.value.clone())
|
||||
}
|
||||
fn now_serial(&self) -> f64 {
|
||||
0.0
|
||||
}
|
||||
fn random(&mut self) -> f64 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A computed criterion that runs out of work counts as not met for its record, which the
|
||||
/// database function can't see: it must answer #CALC!, not a count that leaves records out.
|
||||
#[test]
|
||||
fn a_computed_criterion_out_of_work_makes_the_database_function_calc() {
|
||||
use gridcraft_model::{Cell, Formula};
|
||||
let mut wb = Workbook::new();
|
||||
let name = |name: String, formula: String| gridcraft_model::DefinedName { name, scope: None, formula, comment: String::new(), hidden: false };
|
||||
wb.names.push(name("Step0".into(), "1".into()));
|
||||
for i in 1..=60 {
|
||||
wb.names.push(name(format!("Step{i}"), format!("Step{}+Step{}", i - 1, i - 1)));
|
||||
}
|
||||
let at = |s: &str| CellRef::parse(s).unwrap();
|
||||
let sheet = wb.sheet_mut(0).unwrap();
|
||||
for (r, (n, age)) in [("name", Value::text("age")), ("a", Value::number(1.0)), ("b", Value::number(2.0))].into_iter().enumerate() {
|
||||
sheet.cells.set(at(&format!("A{}", r + 1)), Cell::value(Value::text(n)));
|
||||
sheet.cells.set(at(&format!("B{}", r + 1)), Cell::value(age));
|
||||
}
|
||||
sheet.cells.set(at("D1"), Cell::value(Value::text("Formula")));
|
||||
let host_wb = |criterion: &str| {
|
||||
let mut wb = wb.clone();
|
||||
wb.sheet_mut(0).unwrap().cells.set(at("D2"), Cell::formula(Formula::new(criterion)));
|
||||
wb
|
||||
};
|
||||
let run = |criterion: &str| {
|
||||
let mut host = Stored { wb: host_wb(criterion) };
|
||||
let mut ev = Evaluator::new(&mut host, 0, CellRef::new(9, 9));
|
||||
ev.work = 100_000;
|
||||
ev.value(&gridcraft_formula::parser::parse("=DCOUNT(A1:B3,\"age\",D1:D2)").unwrap())
|
||||
};
|
||||
assert_eq!(run("=B2>0"), Value::number(2.0), "a cheap criterion counts every record");
|
||||
assert!(is_calc(&run("=Step60>0")), "{:?}", run("=Step60>0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_that_double_at_each_step_run_out_of_work() {
|
||||
// Step1 = Step0+Step0, Step2 = Step1+Step1, ...: Step60 would evaluate Step0 2^60 times.
|
||||
|
||||
@@ -359,8 +359,15 @@ impl<'h> Evaluator<'h> {
|
||||
if !self.spend(values) {
|
||||
return err(CellError::Calc);
|
||||
}
|
||||
let database = criteria.is_some();
|
||||
let mut ctx = FnCtx { ev: self, criteria, lambda };
|
||||
gridcraft_functions::call(spec, &evaluated, &mut ctx).pipe(Ev::V)
|
||||
let v = gridcraft_functions::call(spec, &evaluated, &mut ctx);
|
||||
// A computed criterion that ran out of work counted as not met: the result leaves
|
||||
// records out.
|
||||
if database && self.exhausted {
|
||||
return err(CellError::Calc);
|
||||
}
|
||||
Ev::V(v)
|
||||
}
|
||||
|
||||
/// Performs aggregation operations without copying its ranges: blocks are folded over the stored
|
||||
|
||||
Reference in New Issue
Block a user