use truecalc_workbook::{
Address, CellInput, EngineFlavor, RecalcContext, Value, Workbook, Worksheet,
BLOCKED_SPILL_ERROR,
};
fn a1(s: &str) -> Address {
Address::from_a1(s).expect("valid A1")
}
fn ctx() -> RecalcContext {
RecalcContext::new(1_780_000_000_000, "Etc/GMT", 0).expect("Etc/GMT is valid")
}
fn wb() -> Workbook {
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(Worksheet::new("Sheet1")).unwrap();
wb
}
fn num(n: f64) -> Value {
Value::Number(n)
}
#[test]
fn array_result_is_stored_at_the_anchor_and_spilled_cells_are_not_authored() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1,2;3,4}".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(
wb.get("Sheet1", a1("A1")).unwrap().value(),
&Value::Array(vec![vec![num(1.0), num(2.0)], vec![num(3.0), num(4.0)]])
);
assert!(wb.get("Sheet1", a1("B1")).is_none());
assert!(wb.get("Sheet1", a1("A2")).is_none());
assert!(wb.get("Sheet1", a1("B2")).is_none());
assert_eq!(wb.sheet("Sheet1").unwrap().len(), 1);
}
#[test]
fn resolved_reconstructs_spilled_cells_and_reports_the_anchor() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1,2;3,4}".into()))
.unwrap();
wb.recalc(&ctx());
let r = wb.resolved("Sheet1", a1("A1")).unwrap();
assert_eq!(r.anchor, None);
for (cell, expected, i, j) in [
("B1", 2.0, 0usize, 1usize),
("A2", 3.0, 1, 0),
("B2", 4.0, 1, 1),
] {
let r = wb
.resolved("Sheet1", a1(cell))
.expect("spilled cell resolves");
assert_eq!(r.value, num(expected), "value at {cell}");
assert_eq!(r.anchor, Some(a1("A1")), "anchor at {cell}");
let _ = (i, j);
}
assert_eq!(wb.spill_anchor("Sheet1", a1("B2")), Some(a1("A1")));
assert_eq!(wb.spill_anchor("Sheet1", a1("A1")), None);
assert!(wb.resolved("Sheet1", a1("Z9")).is_none());
}
#[test]
fn a_formula_reading_a_spilled_cell_reads_the_spilled_value() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1,2;3,4}".into()))
.unwrap();
wb.set("Sheet1", a1("D1"), CellInput::Formula("=B2+10".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(
wb.get("Sheet1", a1("D1")).unwrap().value(),
&num(14.0),
"reader of a spilled cell must read the spilled value"
);
}
#[test]
fn reader_before_anchor_in_topo_order_still_sees_the_spill() {
let mut wb = wb();
wb.set("Sheet1", a1("A3"), CellInput::Formula("={10,20}".into()))
.unwrap(); wb.set("Sheet1", a1("D1"), CellInput::Formula("=B3*2".into()))
.unwrap(); wb.recalc(&ctx());
assert_eq!(wb.get("Sheet1", a1("D1")).unwrap().value(), &num(40.0));
}
#[test]
fn blocked_spill_by_authored_literal_yields_the_blocked_error_and_spills_nothing() {
let mut wb = wb();
wb.set("Sheet1", a1("D2"), CellInput::Literal(num(99.0)))
.unwrap();
wb.set("Sheet1", a1("C1"), CellInput::Formula("={1,2;3,4}".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(
wb.get("Sheet1", a1("C1")).unwrap().value(),
&Value::Error(BLOCKED_SPILL_ERROR.to_owned())
);
assert_eq!(wb.get("Sheet1", a1("D2")).unwrap().value(), &num(99.0));
assert!(wb.resolved("Sheet1", a1("C2")).is_none());
assert!(wb.resolved("Sheet1", a1("D1")).is_none());
assert_eq!(wb.spill_anchor("Sheet1", a1("D2")), None);
}
#[test]
fn blocked_spill_by_another_formula_in_the_path() {
let mut wb = wb();
wb.set("Sheet1", a1("A2"), CellInput::Formula("=5+5".into()))
.unwrap();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1;2}".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(
wb.get("Sheet1", a1("A1")).unwrap().value(),
&Value::Error(BLOCKED_SPILL_ERROR.to_owned())
);
assert_eq!(wb.get("Sheet1", a1("A2")).unwrap().value(), &num(10.0));
}
#[test]
fn two_anchors_competing_for_a_cell_block_deterministically() {
let mut wb1 = wb();
wb1.set("Sheet1", a1("A1"), CellInput::Formula("={1,2}".into()))
.unwrap();
wb1.set("Sheet1", a1("A2"), CellInput::Formula("={5;6}".into()))
.unwrap();
wb1.recalc(&ctx());
assert!(matches!(
wb1.get("Sheet1", a1("A1")).unwrap().value(),
Value::Array(_)
));
assert!(matches!(
wb1.get("Sheet1", a1("A2")).unwrap().value(),
Value::Array(_)
));
let mut wb2 = wb();
wb2.set("Sheet1", a1("A1"), CellInput::Formula("={1,2,3}".into()))
.unwrap(); wb2.set("Sheet1", a1("B1"), CellInput::Formula("={9;9}".into()))
.unwrap(); wb2.recalc(&ctx());
assert_eq!(
wb2.get("Sheet1", a1("A1")).unwrap().value(),
&Value::Error(BLOCKED_SPILL_ERROR.to_owned()),
"an anchor whose spill path crosses an authored cell blocks"
);
assert!(
matches!(
wb2.get("Sheet1", a1("B1")).unwrap().value(),
Value::Array(_)
),
"the unobstructed anchor still spills"
);
}
#[test]
fn one_by_one_array_collapses_to_a_scalar_and_does_not_spill() {
let mut wb = wb();
wb.set(
"Sheet1",
a1("A1"),
CellInput::Formula("=TRANSPOSE({7})".into()),
)
.unwrap();
wb.recalc(&ctx());
assert_eq!(wb.get("Sheet1", a1("A1")).unwrap().value(), &num(7.0));
assert!(wb.get("Sheet1", a1("A2")).is_none());
}
#[test]
fn out_of_bounds_spill_is_blocked() {
let mut wb = wb();
let last_col_a1 = format!("{}1", col_to_letters(18278));
wb.set(
"Sheet1",
a1(&last_col_a1),
CellInput::Formula("={1,2,3}".into()),
)
.unwrap();
wb.recalc(&ctx());
assert_eq!(
wb.get("Sheet1", a1(&last_col_a1)).unwrap().value(),
&Value::Error(BLOCKED_SPILL_ERROR.to_owned())
);
}
#[test]
fn shrinking_an_array_clears_stale_spilled_cells() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1,2;3,4}".into()))
.unwrap();
wb.recalc(&ctx());
assert!(wb.resolved("Sheet1", a1("B2")).is_some());
wb.set("Sheet1", a1("A1"), CellInput::Formula("={5,6}".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(wb.resolved("Sheet1", a1("B1")).unwrap().value, num(6.0));
assert!(
wb.resolved("Sheet1", a1("A2")).is_none(),
"stale spilled cell must clear when the array shrinks"
);
assert!(wb.resolved("Sheet1", a1("B2")).is_none());
}
#[test]
fn replacing_a_spill_anchor_with_a_scalar_clears_the_whole_spill() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={1,2}".into()))
.unwrap();
wb.recalc(&ctx());
assert!(wb.resolved("Sheet1", a1("B1")).is_some());
wb.set("Sheet1", a1("A1"), CellInput::Formula("=42".into()))
.unwrap();
wb.recalc(&ctx());
assert_eq!(wb.get("Sheet1", a1("A1")).unwrap().value(), &num(42.0));
assert!(wb.resolved("Sheet1", a1("B1")).is_none());
}
fn col_to_letters(mut col: u32) -> String {
let mut out = Vec::new();
while col > 0 {
let rem = (col - 1) % 26;
out.push((b'A' + rem as u8) as char);
col = (col - 1) / 26;
}
out.iter().rev().collect()
}
#[test]
fn incremental_recalc_reads_a_spill_whose_anchor_is_not_dirty() {
let mut wb = wb();
wb.set("Sheet1", a1("A1"), CellInput::Formula("={10,20,30}".into()))
.unwrap();
wb.recalc(&ctx());
assert!(matches!(
wb.get("Sheet1", a1("A1")).unwrap().value(),
Value::Array(_)
));
wb.set("Sheet1", a1("D1"), CellInput::Formula("=C1+1".into()))
.unwrap();
let changes = wb.recalc_incremental(&ctx(), &[("Sheet1".to_string(), a1("D1"))]);
assert_eq!(wb.get("Sheet1", a1("D1")).unwrap().value(), &num(31.0));
assert!(changes.iter().any(|c| c.addr == a1("D1")));
}
#[test]
fn incremental_equals_full_for_a_spill_and_its_reader() {
let mut inc = wb();
inc.set("Sheet1", a1("A1"), CellInput::Formula("={1,2}".into()))
.unwrap();
inc.set("Sheet1", a1("D1"), CellInput::Formula("=B1*5".into()))
.unwrap();
inc.recalc(&ctx());
inc.set("Sheet1", a1("A1"), CellInput::Formula("={3,4}".into()))
.unwrap();
inc.recalc_incremental(&ctx(), &[("Sheet1".to_string(), a1("A1"))]);
let mut full = wb();
full.set("Sheet1", a1("A1"), CellInput::Formula("={3,4}".into()))
.unwrap();
full.set("Sheet1", a1("D1"), CellInput::Formula("=B1*5".into()))
.unwrap();
full.recalc(&ctx());
assert_eq!(inc.to_json().unwrap(), full.to_json().unwrap());
assert_eq!(inc.get("Sheet1", a1("D1")).unwrap().value(), &num(20.0));
}