mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-29 13:16:50 -06:00
343 lines
11 KiB
Rust
343 lines
11 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);
|
|
}
|
|
|
|
#[test]
|
|
fn sumif_sum_range_takes_the_criteria_size() {
|
|
// Excel adds up the range that starts at sum_range's top-left cell with the criteria range's
|
|
// height and width, whatever size sum_range has.
|
|
let mut t = T::new();
|
|
for (r, (k, v)) in [("a", 1), ("b", 2), ("a", 3)].iter().enumerate() {
|
|
t.set(&format!("A{}", r + 1), k);
|
|
t.set(&format!("B{}", r + 1), &v.to_string());
|
|
}
|
|
t.set("D1", "=SUMIF(A1:A3,\"a\",B1)");
|
|
assert_eq!(t.num("D1"), 4.0);
|
|
t.set("D2", "=AVERAGEIF(A1:A3,\"a\",B1)");
|
|
assert_eq!(t.num("D2"), 2.0);
|
|
t.set("D3", "=SUMIF(A1:A3,\"a\",B1:B2)");
|
|
assert_eq!(t.num("D3"), 4.0);
|
|
t.set("D4", "=SUMIF(A1:A3,\"a\",B1:F1)");
|
|
assert_eq!(t.num("D4"), 4.0);
|
|
t.set("D5", "=SUMIF(A1:A3,\"a\",B1:B99)");
|
|
assert_eq!(t.num("D5"), 4.0);
|
|
t.set("D6", "=SUMIF(A:A,\"a\",B1)");
|
|
assert_eq!(t.num("D6"), 4.0);
|
|
t.set("D7", "=SUMIF(A1:A3,\"a\",$B$2)");
|
|
assert_eq!(t.num("D7"), 2.0); // B2:B4: rows 1 and 3 match → B2 + B4 (empty)
|
|
t.set("D8", "=SUMIF(A1:A3,\"a\")");
|
|
assert_eq!(t.num("D8"), 0.0);
|
|
// A horizontal criteria range resizes across.
|
|
t.set("H1", "1");
|
|
t.set("I1", "0");
|
|
t.set("J1", "1");
|
|
t.set("H2", "10");
|
|
t.set("I2", "20");
|
|
t.set("J2", "30");
|
|
t.set("D9", "=SUMIF(H1:J1,1,H2)");
|
|
assert_eq!(t.num("D9"), 40.0);
|
|
// The resized cells are precedents: editing one recalculates.
|
|
t.set("B3", "5");
|
|
assert_eq!(t.num("D1"), 6.0);
|
|
assert_eq!(t.num("D2"), 3.0);
|
|
assert_eq!(t.num("D3"), 6.0);
|
|
assert_eq!(t.num("D4"), 6.0);
|
|
assert_eq!(t.num("D6"), 6.0);
|
|
// SUMIFS and AVERAGEIFS don't resize: different sizes are #VALUE!.
|
|
t.set("D11", "=SUMIFS(B1,A1:A3,\"a\")");
|
|
assert_eq!(t.get("D11"), Value::Error(CellError::Value));
|
|
t.set("D12", "=AVERAGEIFS(B1:B2,A1:A3,\"a\")");
|
|
assert_eq!(t.get("D12"), Value::Error(CellError::Value));
|
|
t.set("D13", "=SUMIFS(B1:B3,A1:A3,\"a\")");
|
|
assert_eq!(t.num("D13"), 6.0);
|
|
}
|
|
|
|
#[test]
|
|
fn sumif_resized_sum_range_stops_at_the_sheet_edge() {
|
|
let mut t = T::new();
|
|
t.set("A1", "a");
|
|
t.set("A2", "a");
|
|
t.set("B1048575", "5");
|
|
t.set("B1048576", "7");
|
|
t.set("C1", "=SUMIF(A1:A2,\"a\",B1048576)");
|
|
assert_eq!(t.num("C1"), 7.0);
|
|
t.set("C2", "=SUMIF(A1:A2,\"a\",B1048575)");
|
|
assert_eq!(t.num("C2"), 12.0);
|
|
t.set("C3", "=SUMIF(A1:B1,\"a\",XFD1)");
|
|
assert_eq!(t.num("C3"), 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn sumif_resized_sum_range_on_another_sheet_and_through_a_name() {
|
|
let mut t = T::new();
|
|
t.wb.sheets.push(std::sync::Arc::new(gridcraft_model::Sheet::new("Data")));
|
|
t.wb.names.push(gridcraft_model::DefinedName {
|
|
name: "Amounts".into(),
|
|
scope: None,
|
|
formula: "Data!$B$1".into(),
|
|
comment: String::new(),
|
|
hidden: false,
|
|
});
|
|
for (r, k) in ["a", "b", "a"].iter().enumerate() {
|
|
t.set(&format!("A{}", r + 1), k);
|
|
t.wb.sheet_mut(1).unwrap().set_value(CellRef::new(r as u32, 1), Value::Number((r + 1) as f64));
|
|
}
|
|
t.calc.rebuild(&t.wb);
|
|
t.set("C1", "=SUMIF(A1:A3,\"a\",Data!B1)");
|
|
assert_eq!(t.num("C1"), 4.0);
|
|
t.set("C2", "=AVERAGEIF(A1:A3,\"a\",Amounts)");
|
|
assert_eq!(t.num("C2"), 2.0);
|
|
t.wb.sheet_mut(1).unwrap().set_value(c("B3"), Value::Number(9.0));
|
|
t.calc.cells_changed(&mut t.wb, &[(1, c("B3"))]);
|
|
assert_eq!(t.num("C1"), 10.0);
|
|
assert_eq!(t.num("C2"), 5.0);
|
|
}
|