use std::collections::{BTreeSet, HashMap};
use crate::refers;
use crate::rows::quoted;
use crate::sheet::{cell_ref, Workbook};
use super::eval::Library;
use super::formula::Read;
use super::program;
use super::shape::Shapes;
use super::vector;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Kind {
Stride,
Slot,
Array,
Lookup,
Cells,
Once,
Start,
Each,
}
impl Kind {
pub(crate) fn word(self) -> &'static str {
match self {
Kind::Stride => "stride",
Kind::Slot => "slot",
Kind::Array => "array",
Kind::Lookup => "lookup",
Kind::Cells => "cells",
Kind::Once => "once",
Kind::Start => "start",
Kind::Each => "each",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Why {
Name,
Span,
Broken,
Unread,
}
impl Why {
fn word(self) -> &'static str {
match self {
Why::Name => "name",
Why::Span => "span",
Why::Broken => "broken",
Why::Unread => "unread",
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Facts {
pub(crate) shape: usize,
pub(crate) sheet: String,
pub(crate) first: String,
pub(crate) r1c1: String,
pub(crate) cells: usize,
pub(crate) sheets: usize,
pub(crate) in_order: usize,
pub(crate) levels: Option<(usize, usize, usize, usize)>,
pub(crate) runs: usize,
pub(crate) lanes: usize,
pub(crate) narrowest: usize,
pub(crate) widest: usize,
pub(crate) reads: BTreeSet<Kind>,
pub(crate) calls: Vec<String>,
pub(crate) missing: Vec<String>,
pub(crate) same: Option<usize>,
pub(crate) every_frame: Option<Why>,
pub(crate) unlowered: Option<program::Why>,
}
impl Facts {
pub(crate) fn alone(&self) -> usize {
self.in_order - self.lanes
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct Census {
pub(crate) shapes: Vec<Facts>,
pub(crate) readers: Vec<(String, usize, Option<usize>)>,
pub(crate) formulas: usize,
pub(crate) levels: usize,
pub(crate) cycles: usize,
}
pub(crate) fn of(
plan: &Shapes,
model: &Workbook,
library: &Library,
arrays: &dyn Fn(usize) -> bool,
) -> Census {
let all = plan.every_shape();
let n = all.len();
let reach = plan.reach();
let mut f: Vec<Facts> = (0..n)
.map(|shape| Facts {
shape,
..Facts::default()
})
.collect();
let mut on: Vec<Vec<usize>> = vec![Vec::new(); n];
for (id, p) in plan.places() {
f[p.shape].cells += 1;
if !on[p.shape].contains(&id.0) {
on[p.shape].push(id.0);
}
}
let order = plan.order();
let ends = plan.level_ends();
let (mut k, mut p, mut levels) = (0usize, 0usize, 0usize);
let mut counted: Option<usize> = None;
while p < order.len() {
while k < ends.len() && ends[k] as usize <= p {
k += 1;
}
let end = ends.get(k).map_or(order.len(), |&e| e as usize);
let s = reach.shape_at(p);
let mut q = p + 1;
while q < end && reach.shape_at(q) == s {
q += 1;
}
let wide = q - p;
let x = &mut f[s];
x.in_order += wide;
x.levels = Some(match x.levels {
None => (k, k, 1, wide),
Some((first, _, count, most)) => (first, k, count + 1, most.max(wide)),
});
if counted != Some(k) {
counted = Some(k);
levels += 1;
}
p = q;
}
for (start, end, s) in plan.run_spans() {
let x = &mut f[s];
let wide = end - start;
x.narrowest = if x.runs == 0 {
wide
} else {
x.narrowest.min(wide)
};
x.runs += 1;
x.lanes += wide;
x.widest = x.widest.max(wide);
}
let mut unfollowed = vec![false; n];
for &s in reach.unfollowed() {
unfollowed[s] = true;
}
let mut first_of: HashMap<String, usize> = HashMap::new();
let mut shapes = Vec::new();
for (s, mut x) in f.into_iter().enumerate() {
if x.cells == 0 {
continue;
}
let shape = &all[s];
let (si, row, col) = shape.anchor;
x.sheet = model
.sheets
.get(si)
.map_or_else(String::new, |sh| sh.name.clone());
x.first = cell_ref(row, col);
x.r1c1 = format!("={}", refers::r1c1(&shape.text, row, col));
x.sheets = on[s].len();
x.same = match first_of.get(&x.r1c1) {
Some(&t) => Some(t),
None => {
first_of.insert(x.r1c1.clone(), s);
None
}
};
if let Ok(e) = &shape.expr {
let paths = vector::paths(library, model, e, arrays, &on[s], x.runs > 0);
for (taken, kind) in [
(paths.stride, Kind::Stride),
(paths.slot, Kind::Slot),
(paths.array, Kind::Array),
(paths.lookup, Kind::Lookup),
(paths.cells, Kind::Cells),
(paths.once, Kind::Once),
(!reach.parts(s).is_empty(), Kind::Start),
(paths.each, Kind::Each),
] {
if taken {
x.reads.insert(kind);
}
}
x.calls = paths.calls;
}
let mut missing: Vec<String> = shape
.missing()
.iter()
.map(|m| m.to_ascii_uppercase())
.collect();
missing.sort_unstable();
missing.dedup();
x.missing = missing;
if unfollowed[s] {
x.every_frame = Some(why(shape));
} else if x.alone() > 0 && x.missing.is_empty() {
x.unlowered = shape.unlowered();
}
shapes.push(x);
}
let readers = model
.sheets
.iter()
.enumerate()
.filter_map(|(si, sheet)| {
let n = reach.readers_on(si);
(n > 0).then(|| (sheet.name.clone(), n, reach.most_on(si)))
})
.collect();
Census {
shapes,
readers,
formulas: plan.formulas(),
levels,
cycles: plan.cycles().iter().map(Vec::len).sum(),
}
}
fn why(shape: &super::shape::Shape) -> Why {
let Ok(e) = &shape.expr else {
return Why::Unread;
};
if shape.risky {
let span = refers::scan(&shape.text)
.iter()
.any(|r| r.kind == refers::Kind::ThreeD);
return if span { Why::Span } else { Why::Broken };
}
let mut name = false;
e.each_read(&mut |r| {
if let Read::Name { .. } = r {
name = true;
}
});
if name {
Why::Name
} else {
Why::Span
}
}
pub(crate) fn spell(c: &Census) -> String {
let texts = c.shapes.iter().filter(|x| x.same.is_none()).count();
let runs: usize = c.shapes.iter().map(|x| x.runs).sum();
let lanes: usize = c.shapes.iter().map(|x| x.lanes).sum();
let alone: usize = c.shapes.iter().map(Facts::alone).sum();
let every: usize = c
.shapes
.iter()
.filter(|x| x.every_frame.is_some())
.map(|x| x.cells)
.sum();
let mut out = format!(
"(plan (shapes {}) (texts {texts}) (formulas {}) (levels {}) (runs {runs}) \
(lanes {lanes}) (alone {alone}) (every-frame {every}) (cycles {}))\n",
c.shapes.len(),
c.formulas,
c.levels,
c.cycles
);
for (sheet, n, most) in &c.readers {
match most {
Some(k) => out.push_str(&format!("(readers {} {n} (most {k}))\n", quoted(sheet))),
None => out.push_str(&format!("(readers {} {n})\n", quoted(sheet))),
}
}
for x in &c.shapes {
out.push_str(&format!(
"(shape {} {} {} {} (cells {})",
x.shape,
quoted(&x.sheet),
quoted(&x.first),
quoted(&x.r1c1),
x.cells
));
match x.levels {
Some((first, last, count, most)) => {
out.push_str(&format!(" (levels {first} {last} {count} {most})"))
}
None => out.push_str(" (levels)"),
}
out.push_str(&format!(
" (runs {} {} {} {}) (alone {}) (reads",
x.runs,
x.lanes,
x.narrowest,
x.widest,
x.alone()
));
for kind in &x.reads {
out.push(' ');
out.push_str(kind.word());
}
out.push(')');
for (head, names) in [("calls", &x.calls), ("missing", &x.missing)] {
if names.is_empty() {
continue;
}
out.push_str(&format!(" ({head}"));
for name in names {
out.push(' ');
out.push_str("ed(name));
}
out.push(')');
}
if x.sheets > 1 {
out.push_str(&format!(" (sheets {})", x.sheets));
}
if let Some(k) = x.same {
out.push_str(&format!(" (same {k})"));
}
if let Some(why) = x.every_frame {
out.push_str(&format!(" (every-frame {})", why.word()));
}
if let Some(why) = x.unlowered {
out.push_str(&format!(" (unlowered {})", why.word()));
}
out.push_str(")\n");
}
out
}
pub(crate) fn rows(
plan: &Shapes,
model: &Workbook,
library: &Library,
arrays: &dyn Fn(usize) -> bool,
) -> String {
spell(&of(plan, model, library, arrays))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calc::graph::Names;
use crate::calc::library;
use crate::calc::{Arg, Ctx, Extent, Reason, Value};
use crate::machine::{read_rows, Machine};
use crate::reader::read_forms;
use crate::sheet::Sheet;
fn machine(rows: &str, lib: Library) -> Machine {
Machine::load(rows, lib).unwrap_or_else(|e| panic!("line {}: {}", e.line, e.refusal.text))
}
fn zero(_: &Ctx<'_>, _: &[Arg<'_>]) -> Result<Value, Reason> {
Ok(Value::Number(0.0))
}
fn with_round() -> Library {
let mut lib = library::language();
lib.register("ROUND", zero);
lib
}
fn door(rows: &str, lib: &Library) -> String {
let forms = read_forms(rows).expect("reads");
let draft = read_rows(&forms).unwrap_or_else(|e| panic!("{}", e.refusal.text));
let wb = &draft.workbook;
let extents: Vec<Extent> = wb.sheets.iter().map(Sheet::extent).collect();
let plan = Shapes::of(wb, wb.sheets.len(), lib, &Names::of(wb), &extents);
plan.census(wb, lib)
}
#[test]
fn a_column_s_census_is_its_rows_whole() {
let m = machine(
concat!(
"(sheet \"Output\" visible)\n",
"(cell \"Output\" \"E1\" 3)\n",
"(formula \"Output\" \"A1:A20\" \"=ROW()\")\n",
"(formula \"Output\" \"B1:B20\" \"=A1*$E$1+1\")\n",
"(formula \"Output\" \"C1:C20\" \"=IF(B1>30000,B1-30000,B1)\")\n",
),
library::language(),
);
assert_eq!(
m.census(),
concat!(
"(plan (shapes 3) (texts 3) (formulas 60) (levels 3) (runs 3) (lanes 60) (alone 0) (every-frame 0) (cycles 0))\n",
"(readers \"Output\" 3)\n",
"(shape 0 \"Output\" \"A1\" \"=ROW()\" (cells 20) (levels 0 0 1 20) (runs 1 20 20 20) (alone 0) (reads))\n",
"(shape 1 \"Output\" \"B1\" \"=RC[-1]*R1C5+1\" (cells 20) (levels 1 1 1 20) (runs 1 20 20 20) (alone 0) (reads stride once))\n",
"(shape 2 \"Output\" \"C1\" \"=IF(RC[-1]>30000,RC[-1]-30000,RC[-1])\" (cells 20) (levels 2 2 1 20) (runs 1 20 20 20) (alone 0) (reads stride))\n",
)
);
}
#[test]
fn a_running_balance_s_census_is_one_cell_a_level() {
let m = machine(
concat!(
"(sheet \"Output\" visible)\n",
"(cell \"Output\" \"A1\" 1)\n",
"(formula \"Output\" \"A2:A20\" \"=MOD(ROW()*7,13)\")\n",
"(formula \"Output\" \"B1\" \"=A1\")\n",
"(formula \"Output\" \"B2:B20\" \"=B1+A2\")\n",
),
library::language(),
);
assert_eq!(
m.census(),
concat!(
"(plan (shapes 3) (texts 3) (formulas 39) (levels 20) (runs 1) (lanes 19) (alone 20) (every-frame 0) (cycles 0))\n",
"(readers \"Output\" 3)\n",
"(shape 0 \"Output\" \"B1\" \"=RC[-1]\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads lookup))\n",
"(shape 1 \"Output\" \"A2\" \"=MOD(ROW()*7,13)\" (cells 19) (levels 0 0 1 19) (runs 1 19 19 19) (alone 0) (reads))\n",
"(shape 2 \"Output\" \"B2\" \"=R[-1]C+RC[-1]\" (cells 19) (levels 1 19 19 1) (runs 0 0 0 0) (alone 19) (reads lookup))\n",
)
);
}
const MONTHS: &str = concat!(
"(sheet \"Output\" visible)\n",
"(cell \"Output\" \"P1\" 100)\n",
"(formula \"Output\" \"B2:M4\" \"=MOD(ROW()*31+COLUMN()*17,97)+$P$1\")\n",
"(formula \"Output\" \"N2:N4\" \"=SUM(B2:M2)\")\n",
"(formula \"Output\" \"O2:O4\" \"=ROUND(N2/12,0)\")\n",
"(formula \"Output\" \"B5:O5\" \"=SUM(B2:B4)\")\n",
);
const MONTHS_PLAN: &str = concat!(
"(plan (shapes 4) (texts 4) (formulas 56) (levels 4) (runs 2) (lanes 48) (alone 8) (every-frame 0) (cycles 0))\n",
"(readers \"Output\" 4)\n",
"(shape 0 \"Output\" \"B2\" \"=MOD(ROW()*31+COLUMN()*17,97)+R1C16\" (cells 36) (levels 0 0 1 36) (runs 1 36 36 36) (alone 0) (reads once))\n",
);
#[test]
fn a_range_of_the_frame_s_own_sheet_is_walked_by_index() {
let m = machine(MONTHS, with_round());
assert_eq!(
m.census(),
format!(
"{MONTHS_PLAN}{}{}{}",
"(shape 1 \"Output\" \"N2\" \"=SUM(RC[-12]:RC[-1])\" (cells 3) (levels 1 1 1 3) (runs 0 0 0 0) (alone 3) (reads array))\n",
"(shape 2 \"Output\" \"O2\" \"=ROUND(RC[-1]/12,0)\" (cells 3) (levels 2 2 1 3) (runs 0 0 0 0) (alone 3) (reads slot) (calls \"ROUND\"))\n",
"(shape 3 \"Output\" \"B5\" \"=SUM(R[-3]C:R[-1]C)\" (cells 14) (levels 1 3 3 12) (runs 1 12 12 12) (alone 2) (reads stride))\n",
)
);
assert_eq!(door(MONTHS, &with_round()), m.census());
}
#[test]
fn a_range_of_a_sheet_too_sparse_for_an_array_is_walked_through_its_cells() {
let rows = format!("{MONTHS}(cell \"Output\" \"XFA1\" 1)\n");
let m = machine(&rows, with_round());
assert_eq!(
m.census(),
format!(
"{MONTHS_PLAN}{}{}{}",
"(shape 1 \"Output\" \"N2\" \"=SUM(RC[-12]:RC[-1])\" (cells 3) (levels 1 1 1 3) (runs 0 0 0 0) (alone 3) (reads cells) (calls \"SUM\"))\n",
"(shape 2 \"Output\" \"O2\" \"=ROUND(RC[-1]/12,0)\" (cells 3) (levels 2 2 1 3) (runs 0 0 0 0) (alone 3) (reads lookup) (calls \"ROUND\"))\n",
"(shape 3 \"Output\" \"B5\" \"=SUM(R[-3]C:R[-1]C)\" (cells 14) (levels 1 3 3 12) (runs 1 12 12 12) (alone 2) (reads cells) (calls \"SUM\"))\n",
)
);
assert_eq!(door(&rows, &with_round()), m.census());
}
const LIFE: &str = concat!(
"(sheet \"Clock\" visible)\n",
"(cell \"Clock\" \"B1\" 1)\n",
"(cell \"Clock\" \"B3\" 1)\n",
"(sheet \"Screen\" visible)\n",
"(formula \"Screen\" \"B2:F4\" \"=IF(Clock!$B$1=1,MOD(ROW()+COLUMN()+Clock!$B$3,2),",
"IF(OR(SUM(Screen.last!A1:C3)-Screen.last!B2=3,AND(Screen.last!B2=1,",
"SUM(Screen.last!A1:C3)-Screen.last!B2=2)),1,0))\")\n",
);
fn life_row(reads: &str, tail: &str) -> String {
format!(
"(shape 0 \"Screen\" \"B2\" \"=IF(Clock!R1C2=1,MOD(ROW()+COLUMN()+Clock!R3C2,2),\
IF(OR(SUM(Screen.last!R[-1]C[-1]:R[1]C[1])-Screen.last!RC=3,AND(Screen.last!RC=1,\
SUM(Screen.last!R[-1]C[-1]:R[1]C[1])-Screen.last!RC=2)),1,0))\" (cells 15) \
(levels 0 0 1 15) (runs 1 15 15 15) (alone 0) (reads {reads}){tail})\n"
)
}
#[test]
fn life_s_census_reads_its_twin_and_its_range_by_slices() {
let mut m = machine(LIFE, library::language());
let census = format!(
"{}{}{}{}",
"(plan (shapes 1) (texts 1) (formulas 15) (levels 1) (runs 1) (lanes 15) (alone 0) (every-frame 0) (cycles 0))\n",
"(readers \"Clock\" 2)\n",
"(readers \"Screen.last\" 1)\n",
life_row("stride once start", "")
);
assert_eq!(m.census(), census);
for _ in 0..3 {
assert!(m.step(0).done);
}
assert_eq!(m.census(), census, "after three frames");
}
#[test]
fn a_twin_too_sparse_for_an_array_is_read_through_its_cells() {
let mut m = machine(LIFE, library::language());
assert!(m.step(0).done);
m.write("(cell \"Screen\" \"XFA1\" 1)").expect("writes");
assert!(m.step(0).done);
let census = m.census();
assert!(
census.ends_with(&life_row("lookup cells once start", " (calls \"SUM\")")),
"{census}"
);
}
#[test]
fn the_census_counts_the_shapes_a_cell_holds() {
let mut m = machine(
"(sheet \"S\" visible)\n(formula \"S\" \"A1:A8\" \"=ROW()+1\")\n",
library::language(),
);
m.write("(formula \"S\" \"A1:A8\" \"=ROW()*2\")")
.expect("writes");
assert_eq!(m.formulas(), (8, 1));
assert_eq!(
m.census(),
concat!(
"(plan (shapes 1) (texts 1) (formulas 8) (levels 1) (runs 1) (lanes 8) (alone 0) (every-frame 0) (cycles 0))\n",
"(shape 0 \"S\" \"A1\" \"=ROW()*2\" (cells 8) (levels 0 0 1 8) (runs 1 8 8 8) (alone 0) (reads))\n",
)
);
}
#[test]
fn a_door_reads_another_sheet_as_the_machine_does() {
let rows = concat!(
"(sheet \"S\" visible)\n",
"(sheet \"T\" visible)\n",
"(cell \"T\" \"A1:A5\" 1)\n",
"(formula \"S\" \"A1:A4\" \"=T!A1+SUM(T!A1:A2)\")\n",
);
let plan = concat!(
"(plan (shapes 1) (texts 1) (formulas 4) (levels 1) (runs 1) (lanes 4) (alone 0) (every-frame 0) (cycles 0))\n",
"(readers \"T\" 1)\n",
);
let row = "(shape 0 \"S\" \"A1\" \"=T!RC+SUM(T!RC:R[1]C)\" (cells 4) (levels 0 0 1 4) (runs 1 4 4 4) (alone 0)";
for (rows, reads) in [
(rows.to_string(), " (reads stride))\n"),
(
format!("{rows}(cell \"T\" \"XFA1\" 1)\n"),
" (reads lookup cells) (calls \"SUM\"))\n",
),
] {
let census = door(&rows, &library::language());
assert_eq!(census, format!("{plan}{row}{reads}"));
assert_eq!(machine(&rows, library::language()).census(), census);
}
}
#[test]
fn a_level_only_a_cycle_stands_at_holds_no_cell_of_the_order() {
let census = door(
concat!(
"(sheet \"S\" visible)\n",
"(formula \"S\" \"X1\" \"=1\")\n",
"(formula \"S\" \"B1\" \"=X1+C1\")\n",
"(formula \"S\" \"C1\" \"=B1\")\n",
"(formula \"S\" \"D1\" \"=B1\")\n",
),
&library::language(),
);
assert_eq!(
census,
concat!(
"(plan (shapes 4) (texts 4) (formulas 4) (levels 2) (runs 0) (lanes 0) (alone 2) (every-frame 0) (cycles 2))\n",
"(readers \"S\" 1)\n",
"(shape 0 \"S\" \"B1\" \"=RC[22]+RC[1]\" (cells 1) (levels) (runs 0 0 0 0) (alone 0) (reads lookup))\n",
"(shape 1 \"S\" \"C1\" \"=RC[-1]\" (cells 1) (levels) (runs 0 0 0 0) (alone 0) (reads lookup))\n",
"(shape 2 \"S\" \"D1\" \"=RC[-2]\" (cells 1) (levels 2 2 1 1) (runs 0 0 0 0) (alone 1) (reads lookup))\n",
"(shape 3 \"S\" \"X1\" \"=1\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads))\n",
)
);
}
#[test]
fn a_door_s_plan_holds_what_a_load_refuses() {
let rows = concat!(
"(sheet \"Model\" visible)\n",
"(sheet \"Jan\" visible)\n",
"(sheet \"Feb\" visible)\n",
"(cell \"Model\" \"A1:A4\" 2)\n",
"(cell \"Jan\" \"A1\" 5)\n",
"(cell \"Feb\" \"A1\" 7)\n",
"(name \"Rate\" \"Model!$A$1\")\n",
"(formula \"Model\" \"B1\" \"=C1*3\")\n",
"(formula \"Model\" \"C1\" \"=B1*3\")\n",
"(formula \"Model\" \"D1:D2\" \"=A1*2\")\n",
"(formula \"Model\" \"D3:D4\" \"=A3*2\")\n",
"(formula \"Model\" \"E1\" \"=Rate*2\")\n",
"(formula \"Model\" \"F1\" \"=VLOOKUP(1,A1:A4,1)\")\n",
"(formula \"Model\" \"G1\" \"=A1 A2\")\n",
"(formula \"Model\" \"H1\" \"=SUM(Jan:Feb!A1)\")\n",
"(formula \"Jan\" \"B1\" \"=A1+1\")\n",
"(formula \"Feb\" \"B1\" \"=A1+1\")\n",
);
let census = door(rows, &library::language());
let lines: Vec<&str> = census.lines().collect();
assert_eq!(
lines[0],
"(plan (shapes 9) (texts 8) (formulas 12) (levels 1) (runs 0) (lanes 0) (alone 10) (every-frame 3) (cycles 2))",
"{census}"
);
assert_eq!(
&lines[1..4],
[
"(readers \"Model\" 3)",
"(readers \"Jan\" 1)",
"(readers \"Feb\" 1)"
],
"{census}"
);
for row in [
"(shape 0 \"Model\" \"B1\" \"=RC[1]*3\" (cells 1) (levels) (runs 0 0 0 0) (alone 0) (reads slot))",
"(shape 1 \"Model\" \"C1\" \"=RC[-1]*3\" (cells 1) (levels) (runs 0 0 0 0) (alone 0) (reads slot))",
"(shape 2 \"Model\" \"D1\" \"=RC[-3]*2\" (cells 2) (levels 0 0 1 2) (runs 0 0 0 0) (alone 2) (reads slot))",
"(shape 3 \"Model\" \"E1\" \"=Rate*2\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads each) (every-frame name))",
"(shape 4 \"Model\" \"F1\" \"=VLOOKUP(1,RC[-5]:R[3]C[-5],1)\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads) (missing \"VLOOKUP\"))",
"(shape 5 \"Model\" \"G1\" \"=RC[-6] R[1]C[-6]\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads) (every-frame unread))",
"(shape 6 \"Model\" \"H1\" \"=SUM(Jan:Feb!RC[-7])\" (cells 1) (levels 0 0 1 1) (runs 0 0 0 0) (alone 1) (reads lookup) (calls \"SUM\") (every-frame span))",
"(shape 7 \"Model\" \"D3\" \"=RC[-3]*2\" (cells 2) (levels 0 0 1 2) (runs 0 0 0 0) (alone 2) (reads slot) (same 2))",
"(shape 8 \"Jan\" \"B1\" \"=RC[-1]+1\" (cells 2) (levels 0 0 1 2) (runs 0 0 0 0) (alone 2) (reads lookup) (sheets 2))",
] {
assert!(lines.contains(&row), "no row {row} in\n{census}");
}
assert_eq!(lines.len(), 1 + 3 + 9, "{census}");
}
}