mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-11 16:34:47 -06:00
A dynamic array that couldn't spill was only rejected when results were written back, after formulas evaluated later in the same pass had read the array as if it had spilled. With 1s in A1:C3 except B2 = SEQUENCE(2,2,9) (blocked), =SUM(A1:C3) gave 47 after a full recalculation (opening a file, Calculate Now) instead of #SPILL!; a reader through OFFSET kept the wrong value even when entered one cell at a time. The spill area is now checked as soon as the array is computed, so the anchor is #SPILL! for everything after it.
257 lines
7.7 KiB
Rust
257 lines
7.7 KiB
Rust
use gridcraft_core::{CellError, CellRef, Value};
|
|
use gridcraft_model::{Cell, Formula, Workbook};
|
|
|
|
use crate::Calc;
|
|
|
|
fn c(s: &str) -> CellRef {
|
|
CellRef::parse(s).unwrap()
|
|
}
|
|
|
|
struct T {
|
|
wb: Workbook,
|
|
calc: Calc,
|
|
}
|
|
|
|
impl T {
|
|
fn new() -> T {
|
|
T { wb: Workbook::new(), calc: Calc::new() }
|
|
}
|
|
fn set(&mut self, at: &str, input: &str) {
|
|
let cell = if input.starts_with('=') {
|
|
Cell::formula(Formula::new(input))
|
|
} else {
|
|
Cell::value(gridcraft_core::parse::parse_input(input, self.wb.date_system).value)
|
|
};
|
|
self.wb.sheet_mut(0).unwrap().cells.set(c(at), cell);
|
|
self.calc.cells_changed(&mut self.wb, &[(0, c(at))]);
|
|
}
|
|
fn get(&self, at: &str) -> Value {
|
|
self.wb.sheet(0).unwrap().value(c(at))
|
|
}
|
|
fn num(&self, at: &str) -> f64 {
|
|
self.get(at).as_f64().unwrap_or_else(|| panic!("{at} = {:?}", self.get(at)))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn arithmetic_and_dependencies() {
|
|
let mut t = T::new();
|
|
t.set("A1", "2");
|
|
t.set("A2", "3");
|
|
t.set("A3", "=A1*A2+1");
|
|
assert_eq!(t.num("A3"), 7.0);
|
|
t.set("A1", "10");
|
|
assert_eq!(t.num("A3"), 31.0);
|
|
t.set("B1", "=SUM(A1:A3)");
|
|
assert_eq!(t.num("B1"), 44.0);
|
|
t.set("A2", "0");
|
|
assert_eq!(t.num("A3"), 1.0);
|
|
assert_eq!(t.num("B1"), 11.0);
|
|
}
|
|
|
|
#[test]
|
|
fn chains_out_of_order() {
|
|
let mut t = T::new();
|
|
t.set("A5", "=A4+1");
|
|
t.set("A4", "=A3+1");
|
|
t.set("A3", "=A2+1");
|
|
t.set("A2", "=A1+1");
|
|
t.set("A1", "1");
|
|
assert_eq!(t.num("A5"), 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn long_chain() {
|
|
let mut t = T::new();
|
|
t.set("A1", "1");
|
|
for r in 2..=3000 {
|
|
t.set(&format!("A{r}"), &format!("=A{}+1", r - 1));
|
|
}
|
|
assert_eq!(t.num("A3000"), 3000.0);
|
|
t.set("A1", "10");
|
|
assert_eq!(t.num("A3000"), 3009.0);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_and_cycles() {
|
|
let mut t = T::new();
|
|
t.set("A1", "=1/0");
|
|
assert_eq!(t.get("A1"), Value::Error(CellError::Div0));
|
|
t.set("B1", "=B2");
|
|
t.set("B2", "=B1");
|
|
assert!(t.get("B1").is_error());
|
|
t.set("C1", "=NOSUCHFN(1)");
|
|
assert_eq!(t.get("C1"), Value::Error(CellError::Name));
|
|
t.set("C2", "=1+");
|
|
assert_eq!(t.get("C2"), Value::Error(CellError::Name));
|
|
}
|
|
|
|
#[test]
|
|
fn operators() {
|
|
let mut t = T::new();
|
|
t.set("A1", "=\"a\"&1&TRUE");
|
|
assert_eq!(t.get("A1"), Value::from("a1TRUE"));
|
|
t.set("A2", "=-2^2");
|
|
assert_eq!(t.num("A2"), 4.0);
|
|
t.set("A3", "=50%");
|
|
assert_eq!(t.num("A3"), 0.5);
|
|
t.set("A4", "=\"abc\"=\"ABC\"");
|
|
assert_eq!(t.get("A4"), Value::Bool(true));
|
|
t.set("A5", "=\"5\"+1");
|
|
assert_eq!(t.num("A5"), 6.0);
|
|
t.set("A6", "=\"x\"+1");
|
|
assert_eq!(t.get("A6"), Value::Error(CellError::Value));
|
|
t.set("A7", "=B99");
|
|
assert_eq!(t.num("A7"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn blocked_spill_reads_as_spill_error() {
|
|
// Calculated in the same pass, a formula over a blocked spill read the array that couldn't
|
|
// spill.
|
|
let mut t = T::new();
|
|
let s = t.wb.sheet_mut(0).unwrap();
|
|
for a in ["A1", "B1", "C1", "A2", "C2", "A3", "B3", "C3"] {
|
|
s.cells.set(c(a), Cell::value(Value::Number(1.0)));
|
|
}
|
|
s.cells.set(c("B2"), Cell::formula(Formula::new("=SEQUENCE(2,2,9)")));
|
|
s.cells.set(c("E1"), Cell::formula(Formula::new("=SUM(A1:C3)")));
|
|
t.calc.recalc_all(&mut t.wb);
|
|
assert_eq!(t.get("B2"), Value::Error(CellError::Spill));
|
|
assert_eq!(t.get("E1"), Value::Error(CellError::Spill));
|
|
assert_eq!(t.num("C3"), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn blocked_spill_read_through_offset() {
|
|
// The reader's reference is only known while evaluating, so it isn't in the dependency graph.
|
|
let mut t = T::new();
|
|
t.set("E4", "5");
|
|
t.set("E8", "=SUM(OFFSET(E2,0,0,2,2))");
|
|
t.set("E3", "=SEQUENCE(2,2,3)");
|
|
t.calc.recalc_all(&mut t.wb);
|
|
assert_eq!(t.get("E3"), Value::Error(CellError::Spill));
|
|
assert_eq!(t.get("E8"), Value::Error(CellError::Spill));
|
|
}
|
|
|
|
#[test]
|
|
fn spill_into_the_old_area_of_a_blocked_neighbour() {
|
|
// C7's array becomes blocked by C8, whose array then takes over C7's old area.
|
|
let mut t = T::new();
|
|
t.set("C7", "=SEQUENCE(2,2)");
|
|
t.set("C8", "=SEQUENCE(2,2)");
|
|
assert_eq!(t.get("C7"), Value::Error(CellError::Spill));
|
|
assert_eq!([t.num("C8"), t.num("D8"), t.num("C9"), t.num("D9")], [1.0, 2.0, 3.0, 4.0]);
|
|
}
|
|
|
|
#[test]
|
|
fn ifs_and_lazy() {
|
|
let mut t = T::new();
|
|
t.set("A1", "5");
|
|
t.set("B1", "=IF(A1>3,\"big\",1/0)");
|
|
assert_eq!(t.get("B1"), Value::from("big"));
|
|
t.set("B2", "=IFERROR(1/0,\"oops\")");
|
|
assert_eq!(t.get("B2"), Value::from("oops"));
|
|
t.set("B3", "=CHOOSE(2,\"a\",\"b\",\"c\")");
|
|
assert_eq!(t.get("B3"), Value::from("b"));
|
|
t.set("B4", "=SWITCH(A1,1,\"one\",5,\"five\",\"other\")");
|
|
assert_eq!(t.get("B4"), Value::from("five"));
|
|
}
|
|
|
|
#[test]
|
|
fn references_functions() {
|
|
let mut t = T::new();
|
|
for (i, v) in [10, 20, 30, 40].iter().enumerate() {
|
|
t.set(&format!("A{}", i + 1), &v.to_string());
|
|
}
|
|
t.set("B1", "=ROW(A3)");
|
|
assert_eq!(t.num("B1"), 3.0);
|
|
t.set("B2", "=ROWS(A1:A4)*COLUMNS(A1:C1)");
|
|
assert_eq!(t.num("B2"), 12.0);
|
|
t.set("B3", "=SUM(OFFSET(A1,1,0,2,1))");
|
|
assert_eq!(t.num("B3"), 50.0);
|
|
t.set("B4", "=INDIRECT(\"A\"&4)");
|
|
assert_eq!(t.num("B4"), 40.0);
|
|
t.set("B5", "=SUM(A1:INDEX(A1:A4,3))");
|
|
assert_eq!(t.num("B5"), 60.0);
|
|
t.set("B6", "=SUM(A:A)");
|
|
assert_eq!(t.num("B6"), 100.0);
|
|
t.set("A2", "25");
|
|
assert_eq!(t.num("B3"), 55.0);
|
|
assert_eq!(t.num("B6"), 105.0);
|
|
}
|
|
|
|
#[test]
|
|
fn spills() {
|
|
let mut t = T::new();
|
|
t.set("A1", "=SEQUENCE(3)");
|
|
assert_eq!(t.num("A1"), 1.0);
|
|
assert_eq!(t.num("A3"), 3.0);
|
|
t.set("B1", "=SUM(A1:A3)");
|
|
assert_eq!(t.num("B1"), 6.0);
|
|
t.set("A2", "x");
|
|
assert_eq!(t.get("A1"), Value::Error(CellError::Spill));
|
|
t.set("A2", "");
|
|
assert_eq!(t.num("A3"), 3.0);
|
|
t.set("C1", "={1,2;3,4}*10");
|
|
assert_eq!(t.num("D2"), 40.0);
|
|
}
|
|
|
|
#[test]
|
|
fn let_lambda() {
|
|
let mut t = T::new();
|
|
t.set("A1", "=LET(x,2,y,3,x*y)");
|
|
assert_eq!(t.num("A1"), 6.0);
|
|
t.set("A2", "=LAMBDA(a,b,a+b)(2,3)");
|
|
assert_eq!(t.num("A2"), 5.0);
|
|
t.set("A3", "=REDUCE(0,{1,2,3},LAMBDA(acc,v,acc+v))");
|
|
assert_eq!(t.num("A3"), 6.0);
|
|
t.set("A4", "=SUM(MAP({1,2,3},LAMBDA(v,v*v)))");
|
|
assert_eq!(t.num("A4"), 14.0);
|
|
}
|
|
|
|
#[test]
|
|
fn names_and_sheets() {
|
|
let mut t = T::new();
|
|
t.wb.names.push(gridcraft_model::DefinedName { name: "Rate".into(), scope: None, formula: "0.5".into(), comment: String::new(), hidden: false });
|
|
t.wb.sheets.push(std::sync::Arc::new(gridcraft_model::Sheet::new("Data")));
|
|
t.wb.sheet_mut(1).unwrap().set_value(c("A1"), Value::Number(8.0));
|
|
t.calc.rebuild(&t.wb);
|
|
t.set("A1", "=Rate*Data!A1");
|
|
assert_eq!(t.num("A1"), 4.0);
|
|
t.wb.sheet_mut(1).unwrap().set_value(c("A1"), Value::Number(10.0));
|
|
t.calc.cells_changed(&mut t.wb, &[(1, c("A1"))]);
|
|
assert_eq!(t.num("A1"), 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn text_function() {
|
|
let mut t = T::new();
|
|
t.set("A1", "=TEXT(1234.5,\"#,##0.00\")");
|
|
assert_eq!(t.get("A1"), Value::from("1,234.50"));
|
|
}
|
|
|
|
#[test]
|
|
fn subtotal_skips_hidden() {
|
|
let mut t = T::new();
|
|
t.set("A1", "1");
|
|
t.set("A2", "2");
|
|
t.set("A3", "3");
|
|
t.wb.sheet_mut(0).unwrap().rows.insert(1, gridcraft_model::LineInfo { hidden: true, ..Default::default() });
|
|
t.set("B1", "=SUBTOTAL(109,A1:A3)");
|
|
t.set("B2", "=SUBTOTAL(9,A1:A3)");
|
|
assert_eq!(t.num("B1"), 4.0);
|
|
assert_eq!(t.num("B2"), 6.0);
|
|
}
|
|
|
|
#[test]
|
|
fn recalc_all_after_load() {
|
|
let mut t = T::new();
|
|
let s = t.wb.sheet_mut(0).unwrap();
|
|
s.cells.set(c("A1"), Cell::value(Value::Number(4.0)));
|
|
s.cells.set(c("A2"), Cell::formula(Formula::new("=A1*2")));
|
|
s.cells.set(c("A3"), Cell::formula(Formula::new("=A2+A1")));
|
|
t.calc.recalc_all(&mut t.wb);
|
|
assert_eq!(t.num("A3"), 12.0);
|
|
}
|