Files
gridcraft/crates/calc/src/tests.rs
T
nihilist-narwhal 9c29ef3fff Formulas: the spill range operator A1#
formula::parse("Sheet1!$D$1#") failed with "unknown error literal".
A # right after a cell reference is now the spill range operator: it
evaluates to the anchor's current spill range, or #REF! when the cell
doesn't spill (or its spill is blocked). Its users depend on the
anchor, so they recalculate when the spill grows or shrinks. Names can
use it.

Files have no # operator: like Excel, the writer stores A1# as
_xlfn.ANCHORARRAY(A1), and the parser reads that back as A1#.
2026-10-09 23:29:35 -05:00

252 lines
7.3 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 spill_range_operator() {
let mut t = T::new();
t.set("A1", "3");
t.set("B1", "=SEQUENCE(A1)");
t.set("C1", "=SUM(B1#)");
t.set("C2", "=ROWS(B1#)");
assert_eq!((t.num("C1"), t.num("C2")), (6.0, 3.0));
// Growing and shrinking the spill recalculates its users.
t.set("A1", "5");
assert_eq!((t.num("C1"), t.num("C2")), (15.0, 5.0));
t.set("A1", "2");
assert_eq!((t.num("C1"), t.num("C2")), (3.0, 2.0));
// A blocked spill, or a cell that doesn't spill, gives #REF!.
t.set("B2", "x");
assert_eq!(t.get("C1"), Value::Error(CellError::Ref));
t.set("B2", "");
assert_eq!(t.num("C1"), 3.0);
t.set("D1", "=SUM(A1#)");
assert_eq!(t.get("D1"), Value::Error(CellError::Ref));
// Through a defined name.
t.wb.names.push(gridcraft_model::DefinedName {
name: "Seq".into(),
scope: None,
formula: "Sheet1!$B$1#".into(),
comment: String::new(),
hidden: false,
});
t.set("E1", "=SUM(Seq)*10");
assert_eq!(t.num("E1"), 30.0);
t.set("A1", "4");
assert_eq!(t.num("E1"), 100.0);
}
#[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 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);
}