pub(crate) mod census;
pub mod dense;
pub mod eval;
pub mod formula;
pub mod graph;
pub mod library;
pub(crate) mod program;
pub(crate) mod reach;
pub mod shape;
pub mod value;
pub(crate) mod vector;
use std::cell::Cell as Slot;
use std::collections::HashMap;
use crate::messages::{raise, Refusal};
use crate::refers::quote_sheet;
use crate::sheet::{cell_ref, formula_text, shared_masters, Content, Workbook};
pub use dense::Dense;
pub use eval::{Arg, Computed, Ctx, Env, Form, Library, Reason, Strict};
pub use formula::{calls, parse, Expr};
pub use graph::{Area, Areas, CellId, Extent, Names, Plan};
pub use shape::{Place, Shape, Shapes};
pub use value::{agrees, ErrorKind, Value};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Progress {
pub evaluated: usize,
pub of: usize,
pub done: bool,
}
pub struct Calc<'m> {
model: &'m Workbook,
library: &'m Library,
names: Names,
extents: Vec<Extent>,
texts: HashMap<CellId, String>,
plan: Plan,
values: HashMap<CellId, Computed>,
next: usize,
here: Slot<CellId>,
}
impl<'m> Calc<'m> {
pub fn new(model: &'m Workbook, library: &'m Library) -> Calc<'m> {
let names = Names::of(model);
let extents: Vec<Extent> = model.sheets.iter().map(|s| s.extent()).collect();
let mut texts = HashMap::new();
for (si, sheet) in model.sheets.iter().enumerate() {
let masters = shared_masters(sheet);
for (&(row, col), cell) in &sheet.cells {
if let Some(text) = formula_text(&cell.content, row, col, &masters) {
texts.insert((si, row, col), text);
}
}
}
let plan = graph::plan(model, &names, &extents, &texts);
let mut values = HashMap::with_capacity(texts.len());
for cycle in &plan.cycles {
for id in cycle {
values.insert(*id, Computed::NotComputed(Reason::Cycle));
}
}
Calc {
model,
library,
names,
extents,
texts,
plan,
values,
next: 0,
here: Slot::new((0, 1, 1)),
}
}
pub fn run(model: &'m Workbook, library: &'m Library) -> Calc<'m> {
let mut calc = Calc::new(model, library);
calc.step(0);
calc
}
pub fn model(&self) -> &'m Workbook {
self.model
}
pub fn library(&self) -> &'m Library {
self.library
}
pub fn plan(&self) -> &Plan {
&self.plan
}
pub fn cycles(&self) -> &[Vec<CellId>] {
&self.plan.cycles
}
pub fn formulas(&self) -> usize {
self.texts.len()
}
pub fn formula(&self, id: CellId) -> Option<&str> {
self.texts.get(&id).map(String::as_str)
}
pub fn formula_cells(&self) -> Vec<CellId> {
let mut ids: Vec<CellId> = self.texts.keys().copied().collect();
ids.sort_unstable();
ids
}
pub fn computed(&self, id: CellId) -> Option<&Computed> {
self.values.get(&id)
}
pub fn step(&mut self, budget: usize) -> Progress {
let order_len = self.plan.order.len();
let end = if budget == 0 {
order_len
} else {
self.next.saturating_add(budget).min(order_len)
};
for i in self.next..end {
let id = self.plan.order[i];
let computed = self.evaluate(id);
self.values.insert(id, computed);
}
self.next = end;
self.progress()
}
pub fn progress(&self) -> Progress {
let in_cycles = self.plan.cycles.iter().map(Vec::len).sum::<usize>();
Progress {
evaluated: self.next + in_cycles,
of: self.plan.formulas(),
done: self.next == self.plan.order.len(),
}
}
pub fn restart(&mut self) {
self.values
.retain(|_, v| matches!(v, Computed::NotComputed(Reason::Cycle)));
self.next = 0;
}
fn evaluate(&self, id: CellId) -> Computed {
let Some(text) = self.texts.get(&id) else {
return Computed::NotComputed(Reason::Construct(String::new()));
};
self.here.set(id);
let parsed = match formula::parse(text) {
Ok(e) => e,
Err(construct) => return Computed::NotComputed(Reason::Construct(construct)),
};
let ctx = Ctx::new(self, self.library);
match ctx.eval(&parsed).and_then(|arg| ctx.one(&arg)) {
Ok(v) => Computed::Value(v.settled()),
Err(reason) => Computed::NotComputed(reason),
}
}
pub fn missing_functions(&self) -> Vec<(CellId, String)> {
let mut seen: HashMap<String, CellId> = HashMap::new();
for id in self.formula_cells() {
for name in formula::calls(&self.texts[&id]) {
if !self.library.has(&name) {
seen.entry(name).or_insert(id);
}
}
}
let mut out: Vec<(CellId, String)> = seen.into_iter().map(|(n, id)| (id, n)).collect();
out.sort();
out
}
pub fn spell_cell(&self, id: CellId) -> String {
let sheet = self
.model
.sheets
.get(id.0)
.map(|s| s.name.as_str())
.unwrap_or("");
format!("{}!{}", quote_sheet(sheet), cell_ref(id.1, id.2))
}
pub fn first_refusal(&self) -> Option<Refusal> {
if let Some(cycle) = self.plan.cycles.first() {
let cells: Vec<String> = cycle.iter().map(|id| self.spell_cell(*id)).collect();
return Some(raise("calc-cycle", &[("cells", &cells.join(", "))]));
}
if let Some((id, name)) = self.missing_functions().into_iter().next() {
return Some(raise(
"calc-function-not-computed",
&[("cell", &self.spell_cell(id)), ("function", &name)],
));
}
let (id, construct) = self.formula_cells().into_iter().find_map(|id| {
formula::parse(&self.texts[&id])
.err()
.map(|construct| (id, construct))
})?;
Some(formula::unread_refusal(&self.spell_cell(id), &construct))
}
}
pub trait Values: std::fmt::Debug {
fn computed(&self, id: CellId) -> Option<&Computed>;
fn lags(&self, _sheet: usize) -> bool {
false
}
fn content(&self, _id: CellId) -> Option<Content> {
None
}
fn kept_extent(&self, _sheet: usize) -> Option<Extent> {
None
}
}
impl Values for Calc<'_> {
fn computed(&self, id: CellId) -> Option<&Computed> {
Calc::computed(self, id)
}
fn kept_extent(&self, sheet: usize) -> Option<Extent> {
self.extents.get(sheet).copied()
}
}
impl std::fmt::Debug for Calc<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Calc")
.field("formulas", &self.texts.len())
.field("cycles", &self.plan.cycles.len())
.field("progress", &self.progress())
.finish()
}
}
impl Env for Calc<'_> {
fn model(&self) -> &Workbook {
self.model
}
fn here(&self) -> CellId {
self.here.get()
}
fn read(&self, id: CellId) -> Result<Value, Reason> {
if self.texts.contains_key(&id) {
return match self.values.get(&id) {
Some(Computed::Value(v)) => Ok(v.clone()),
Some(Computed::NotComputed(r)) => Err(r.clone()),
None => Err(Reason::Cycle),
};
}
let Some(cell) = self
.model
.sheets
.get(id.0)
.and_then(|s| s.cells.get(&(id.1, id.2)))
else {
return Ok(Value::Empty);
};
Ok(match &cell.content {
Content::Text(t) => Value::Text(t.clone()),
Content::Number(n) => Value::Number(*n),
Content::Bool(b) => Value::Bool(*b),
Content::Error(e) => Value::Error(ErrorKind::parse(e).unwrap_or(ErrorKind::Value)),
_ => Value::Empty,
})
}
fn extent(&self, sheet: usize) -> Extent {
self.extents.get(sheet).copied().flatten()
}
fn name(&self, name: &str, scope: Option<usize>, home: usize) -> Option<&str> {
self.names.resolve(name, scope, home)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sheet::{parse_a1_range, Cell, Sheet};
fn model(rows: &[(&str, &str, &str)]) -> Workbook {
let mut wb = Workbook::new();
for (sheet, addr, text) in rows {
let i = wb.ensure_sheet(sheet);
let range = parse_a1_range(addr).unwrap();
let content = if let Some(f) = text.strip_prefix('=') {
Content::Formula(f.to_string())
} else if let Ok(n) = text.parse::<f64>() {
Content::Number(n)
} else if *text == "TRUE" || *text == "FALSE" {
Content::Bool(*text == "TRUE")
} else if let Some(e) = ErrorKind::parse(text) {
Content::Error(e.text().to_string())
} else {
Content::Text(text.to_string())
};
wb.sheets[i].set(range.top, range.left, Cell { content, style: 0 });
}
wb
}
fn at(calc: &Calc<'_>, sheet: &str, addr: &str) -> Computed {
let i = calc.model().find_sheet(sheet).unwrap();
let r = parse_a1_range(addr).unwrap();
calc.computed((i, r.top, r.left)).cloned().unwrap()
}
fn number(calc: &Calc<'_>, sheet: &str, addr: &str) -> f64 {
match at(calc, sheet, addr) {
Computed::Value(Value::Number(n)) => n,
other => panic!("{sheet}!{addr}: {other:?}"),
}
}
#[test]
fn arithmetic_runs_in_dependency_order_whatever_the_cell_order() {
let wb = model(&[
("S", "A3", "=A2*2"),
("S", "A2", "=A1+1"),
("S", "A1", "5"),
("S", "B1", "=A3-A2"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert_eq!(number(&calc, "S", "A2"), 6.0);
assert_eq!(number(&calc, "S", "A3"), 12.0);
assert_eq!(number(&calc, "S", "B1"), 6.0);
assert_eq!(calc.formulas(), 3);
assert!(calc.cycles().is_empty());
assert_eq!(
calc.progress(),
Progress {
evaluated: 3,
of: 3,
done: true
}
);
}
#[test]
fn a_budget_never_changes_a_value() {
let wb = model(&[
("S", "A1", "1"),
("S", "A2", "=A1+1"),
("S", "A3", "=A2+1"),
("S", "A4", "=A3+1"),
("S", "A5", "=SUM(A1:A4)"),
]);
let lib = library::language();
let whole = Calc::run(&wb, &lib);
let mut chunked = Calc::new(&wb, &lib);
let p1 = chunked.step(2);
assert_eq!(
p1,
Progress {
evaluated: 2,
of: 4,
done: false
}
);
assert!(
chunked.computed((0, 4, 1)).is_none(),
"A4 waits for the next step"
);
let p2 = chunked.step(1);
assert!(!p2.done);
let p3 = chunked.step(5);
assert!(p3.done && p3.evaluated == 4);
for id in whole.formula_cells() {
assert_eq!(whole.computed(id), chunked.computed(id), "{id:?}");
}
assert_eq!(number(&whole, "S", "A5"), 10.0);
chunked.restart();
assert!(chunked.computed((0, 5, 1)).is_none());
chunked.step(0);
assert_eq!(number(&chunked, "S", "A5"), 10.0);
}
#[test]
fn a_cycle_is_named_and_what_reads_it_is_not_computed() {
let wb = model(&[
("S", "A1", "=B1+1"),
("S", "B1", "=A1+1"),
("S", "C1", "=A1*2"),
("S", "D1", "=D1"),
("S", "E1", "=1+1"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert_eq!(
calc.cycles(),
&[vec![(0, 1, 1), (0, 1, 2)], vec![(0, 1, 4)]]
);
assert_eq!(at(&calc, "S", "A1"), Computed::NotComputed(Reason::Cycle));
assert_eq!(at(&calc, "S", "C1"), Computed::NotComputed(Reason::Cycle));
assert_eq!(at(&calc, "S", "D1"), Computed::NotComputed(Reason::Cycle));
assert_eq!(number(&calc, "S", "E1"), 2.0);
let refusal = calc.first_refusal().unwrap();
assert_eq!(refusal.id, "calc-cycle");
assert!(refusal.text.contains("S!A1, S!B1"), "{}", refusal.text);
assert_eq!(
calc.progress(),
Progress {
evaluated: 5,
of: 5,
done: true
}
);
}
#[test]
fn a_cell_that_reads_itself_through_another_sheet_s_twin_is_no_cycle() {
let wb = model(&[
("Screen", "B2", "=Screen.last!B2+1"),
("Screen.last", "B2", "41"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert!(calc.cycles().is_empty());
assert_eq!(number(&calc, "Screen", "B2"), 42.0);
}
#[test]
fn ranges_names_and_spans_are_read_as_excel_reads_them() {
let mut wb = model(&[
("Model", "B1", "1200"),
("Model", "B2", "800"),
("Model", "B3", "=B1-B2"),
("Model", "C1", "x"),
("Model", "C2", "TRUE"),
("Model", "D1", "=SUM(B:B)"),
("Model", "D2", "=SUM(B1:C2)"),
("Model", "D3", "=SUM(B1,\"5\",TRUE,C2)"),
("Model", "D4", "=B3*Rate"),
("Model", "D5", "=Local+1"),
("Model", "D6", "=SUM(Model:Scratch!B1)"),
("Model", "D7", "=Nothing"),
("Model", "D8", "=Missing!A1"),
("Model", "D9", "=MAX(B1:B3,C1)"),
("Model", "D10", "=MIN(C1:C2)"),
("Scratch", "B1", "7"),
("Scratch", "B2", "=Model!D1*2"),
]);
wb.defined_names
.push(("Rate".to_string(), "0.2".to_string()));
wb.defined_names
.push(("Model!Local".to_string(), "Model!$B$2".to_string()));
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert_eq!(number(&calc, "Model", "B3"), 400.0);
assert_eq!(
number(&calc, "Model", "D1"),
2400.0,
"the formula in B3 counts"
);
assert_eq!(
number(&calc, "Model", "D2"),
2000.0,
"a text and a truth value in a range are skipped"
);
assert_eq!(
number(&calc, "Model", "D3"),
1206.0,
"given directly they are coerced; C2 is read through a reference and skipped"
);
assert_eq!(number(&calc, "Model", "D4"), 80.0);
assert_eq!(number(&calc, "Model", "D5"), 801.0);
assert_eq!(
number(&calc, "Model", "D6"),
1207.0,
"a span sums both sheets"
);
assert_eq!(
at(&calc, "Model", "D7"),
Computed::Value(Value::Error(ErrorKind::Name))
);
assert_eq!(
at(&calc, "Model", "D8"),
Computed::Value(Value::Error(ErrorKind::Ref))
);
assert_eq!(number(&calc, "Model", "D9"), 1200.0);
assert_eq!(
number(&calc, "Model", "D10"),
0.0,
"no number in the range is 0"
);
assert_eq!(
number(&calc, "Scratch", "B2"),
4800.0,
"a sheet reads another's formula after it is computed"
);
}
#[test]
fn an_aggregate_folds_in_the_walk_s_order_and_keeps_the_first_answer() {
let wb = model(&[
("S", "A1", "1E16"),
("S", "B1", "1"),
("S", "A2", "-1E16"),
("S", "B2", "0"),
("S", "C1", "#N/A"),
("S", "C2", "=VLOOKUP(1,A1:A2,2)"),
("S", "D1", "=SUM(A1:B2)"),
("S", "D2", "=SUM(C1:C2)"),
("S", "D3", "=SUM(C1+0,C2:C2)"),
("S", "D4", "=AND(FALSE,C1)"),
("S", "D5", "=OR(TRUE,C1)"),
("S", "D6", "=MIN(Z1:Z3)"),
("S", "D7", "=MAX(Z1:Z3)"),
("S", "D8", "=MIN(A1:B2)"),
("S", "D9", "=MAX(A1:B2,-5)"),
("S", "D10", "=OR(A2:B2)"),
("S", "D11", "=AND(A1:B1,TRUE)"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
let err = |k| Computed::Value(Value::Error(k));
let by_rows = [1e16, 1.0, -1e16, 0.0].iter().fold(-0.0, |a, b| a + b);
let by_columns = [1e16, -1e16, 1.0, 0.0].iter().fold(-0.0, |a, b| a + b);
assert_ne!(
by_rows, by_columns,
"the fixture tells the two orders apart"
);
assert_eq!(number(&calc, "S", "D1"), by_rows);
assert_eq!(number(&calc, "S", "D1"), 0.0);
assert_eq!(
at(&calc, "S", "D2"),
Computed::NotComputed(Reason::Function("VLOOKUP".to_string())),
"a range is walked to its end: the cell not computed answers after the error"
);
assert_eq!(
at(&calc, "S", "D3"),
err(ErrorKind::NA),
"an error given directly answers before a later range is walked"
);
assert_eq!(at(&calc, "S", "D4"), err(ErrorKind::NA), "AND is strict");
assert_eq!(at(&calc, "S", "D5"), err(ErrorKind::NA), "OR is strict");
assert_eq!(number(&calc, "S", "D6"), 0.0, "MIN of no number");
assert_eq!(number(&calc, "S", "D7"), 0.0, "MAX of no number");
assert_eq!(number(&calc, "S", "D8"), -1e16);
assert_eq!(number(&calc, "S", "D9"), 1e16);
assert_eq!(
at(&calc, "S", "D10"),
Computed::Value(Value::Bool(true)),
"-1E16 is true, 0 false"
);
assert_eq!(at(&calc, "S", "D11"), Computed::Value(Value::Bool(true)));
}
#[test]
fn errors_are_values_and_propagate_as_excel_propagates_them() {
let wb = model(&[
("S", "A1", "=1/0"),
("S", "A2", "=A1+1"),
("S", "A3", "=IF(A1>0,1,2)"),
("S", "A4", "=IF(TRUE,1,A1)"),
("S", "A5", "=SUM(A1:A2)"),
("S", "A6", "#N/A"),
("S", "A7", "=A6"),
("S", "A8", "=\"x\"+1"),
("S", "A9", "=-\"5\""),
("S", "A10", "=50%"),
("S", "A11", "=1&2"),
("S", "A12", "=2^10"),
("S", "A13", "=B99"),
("S", "A14", "=#REF!+1"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
let err = |k| Computed::Value(Value::Error(k));
assert_eq!(at(&calc, "S", "A1"), err(ErrorKind::Div0));
assert_eq!(at(&calc, "S", "A2"), err(ErrorKind::Div0));
assert_eq!(
at(&calc, "S", "A3"),
err(ErrorKind::Div0),
"the condition's error"
);
assert_eq!(
number(&calc, "S", "A4"),
1.0,
"the branch not taken is not computed"
);
assert_eq!(at(&calc, "S", "A5"), err(ErrorKind::Div0));
assert_eq!(
at(&calc, "S", "A7"),
err(ErrorKind::NA),
"an error typed into a cell"
);
assert_eq!(at(&calc, "S", "A8"), err(ErrorKind::Value));
assert_eq!(number(&calc, "S", "A9"), -5.0);
assert_eq!(number(&calc, "S", "A10"), 0.5);
assert_eq!(
at(&calc, "S", "A11"),
Computed::Value(Value::Text("12".to_string()))
);
assert_eq!(number(&calc, "S", "A12"), 1024.0);
assert_eq!(
number(&calc, "S", "A13"),
0.0,
"an empty cell read is 0 in the cell"
);
assert_eq!(at(&calc, "S", "A14"), err(ErrorKind::Ref));
}
#[test]
fn the_day_one_functions_compute_exactly() {
let wb = model(&[
("S", "A1", "=IF(1>0,\"yes\",\"no\")"),
("S", "A2", "=IF(0)"),
("S", "A3", "=IF(FALSE,1)"),
("S", "A4", "=IF(TRUE,,1)"),
("S", "A5", "=AND(TRUE,1,\"TRUE\")"),
("S", "A6", "=AND(TRUE,0)"),
("S", "A7", "=OR(FALSE,0,2)"),
("S", "A8", "=NOT(0)"),
("S", "A9", "=AND(\"yes\")"),
("S", "A10", "=ABS(-3.5)"),
("S", "A11", "=INT(-1.5)"),
("S", "A12", "=MOD(-7,3)"),
("S", "A13", "=MOD(7,-3)"),
("S", "A14", "=MOD(7,0)"),
("S", "A15", "=ROW()"),
("S", "A16", "=COLUMN()"),
("S", "A17", "=ROW(C9)"),
("S", "A18", "=COLUMN(C9:D10)"),
("S", "A19", "=CHOOSE(2,\"a\",\"b\",\"c\")"),
("S", "A20", "=CHOOSE(4,\"a\",\"b\",\"c\")"),
("S", "A21", "=SIN(0)"),
("S", "A22", "=MOD(5.5,2)"),
("S", "A23", "=CHOOSE(1.9,\"a\",\"b\")"),
("S", "A24", "=AND()"),
("S", "B1", "=VLOOKUP(1,A1:A2,2)"),
("S", "B2", "=B1+1"),
("S", "B3", "=SUM(A1:A2 B1)"),
("S", "B4", "={1,2}"),
("S", "B5", "=A1:A2*2"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
let text = |s: &str| Computed::Value(Value::Text(s.to_string()));
let truth = |b: bool| Computed::Value(Value::Bool(b));
let err = |k| Computed::Value(Value::Error(k));
assert_eq!(at(&calc, "S", "A1"), text("yes"));
assert_eq!(
at(&calc, "S", "A2"),
err(ErrorKind::Value),
"IF wants a branch"
);
assert_eq!(at(&calc, "S", "A3"), truth(false), "no else is FALSE");
assert_eq!(number(&calc, "S", "A4"), 0.0, "a branch left out is 0");
assert_eq!(at(&calc, "S", "A5"), truth(true));
assert_eq!(at(&calc, "S", "A6"), truth(false));
assert_eq!(at(&calc, "S", "A7"), truth(true));
assert_eq!(at(&calc, "S", "A8"), truth(true));
assert_eq!(at(&calc, "S", "A9"), err(ErrorKind::Value));
assert_eq!(number(&calc, "S", "A10"), 3.5);
assert_eq!(number(&calc, "S", "A11"), -2.0);
assert_eq!(number(&calc, "S", "A12"), 2.0, "the divisor's sign");
assert_eq!(number(&calc, "S", "A13"), -2.0);
assert_eq!(at(&calc, "S", "A14"), err(ErrorKind::Div0));
assert_eq!(number(&calc, "S", "A15"), 15.0);
assert_eq!(number(&calc, "S", "A16"), 1.0);
assert_eq!(number(&calc, "S", "A17"), 9.0);
assert_eq!(number(&calc, "S", "A18"), 3.0);
assert_eq!(at(&calc, "S", "A19"), text("b"));
assert_eq!(at(&calc, "S", "A20"), err(ErrorKind::Value));
assert_eq!(number(&calc, "S", "A21"), 0.0);
assert_eq!(number(&calc, "S", "A22"), 1.5);
assert_eq!(
at(&calc, "S", "A23"),
text("a"),
"the index is read down to a whole number"
);
assert_eq!(
at(&calc, "S", "A24"),
err(ErrorKind::Value),
"nothing to AND"
);
assert_eq!(
at(&calc, "S", "B1"),
Computed::NotComputed(Reason::Function("VLOOKUP".to_string()))
);
assert_eq!(
at(&calc, "S", "B2"),
Computed::NotComputed(Reason::Function("VLOOKUP".to_string())),
"a reader inherits the reason"
);
assert_eq!(
at(&calc, "S", "B3"),
Computed::NotComputed(Reason::Construct("intersection".to_string()))
);
assert_eq!(
at(&calc, "S", "B4"),
Computed::NotComputed(Reason::Construct("{".to_string()))
);
assert_eq!(
at(&calc, "S", "B5"),
Computed::NotComputed(Reason::Construct("A1:A2".to_string())),
"a range where one value is wanted waits for KERNEL.8"
);
assert_eq!(
calc.missing_functions(),
vec![((0, 1, 2), "VLOOKUP".to_string())]
);
let refusal = calc.first_refusal().unwrap();
assert_eq!(refusal.id, "calc-function-not-computed");
assert!(
refusal.text.contains("VLOOKUP") && refusal.text.contains("S!B1"),
"{}",
refusal.text
);
assert_eq!(at(&calc, "S", "A1").spell(), "\"yes\"");
assert_eq!(at(&calc, "S", "B1").spell(), "(not-computed \"VLOOKUP\")");
}
#[test]
fn a_construct_not_read_is_the_third_thing_a_load_refuses() {
let wb = model(&[
("S", "A1", "=1+1"),
("S", "B2", "={1,2}"),
("S", "C3", "=@A1"),
]);
let lib = library::language();
let calc = Calc::new(&wb, &lib);
let r = calc.first_refusal().unwrap();
assert_eq!(r.id, "calc-construct-not-read");
assert!(
r.text.contains("S!B2") && r.text.contains("reaches {"),
"{}",
r.text
);
let wb = model(&[("S", "B2", "={1,2}"), ("S", "C3", "=NOW()")]);
let calc = Calc::new(&wb, &lib);
assert_eq!(
calc.first_refusal().unwrap().id,
"calc-function-not-computed"
);
let wb = model(&[("S", "A1", "=1+1")]);
assert!(Calc::new(&wb, &lib).first_refusal().is_none());
}
#[test]
fn a_chain_of_operators_evaluates_left_to_right_however_long() {
let long = format!("={}1", "1+".repeat(4095));
let wb = model(&[
("S", "A1", "=10-1-1-1"),
("S", "A2", "=2^3^2"),
("S", "A3", "=1+2*3-4/2+5"),
("S", "A4", "=\"a\"&\"b\"&\"c\""),
("S", "A5", long.as_str()),
("S", "A6", "=1/0+1+1"),
("S", "A7", "=1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1"),
]);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert_eq!(number(&calc, "S", "A1"), 7.0);
assert_eq!(number(&calc, "S", "A2"), 64.0);
assert_eq!(number(&calc, "S", "A3"), 10.0);
assert_eq!(
at(&calc, "S", "A4"),
Computed::Value(Value::Text("abc".to_string()))
);
assert_eq!(number(&calc, "S", "A5"), 4096.0);
assert_eq!(
at(&calc, "S", "A6"),
Computed::Value(Value::Error(ErrorKind::Div0))
);
assert_eq!(number(&calc, "S", "A7"), 20.0);
}
#[test]
fn excel_s_two_limits_are_refused_by_their_own_ids() {
let deep = format!("={}1{}", "(".repeat(65), ")".repeat(65));
let long = format!("={}1", "1+".repeat(4096));
let lib = library::language();
let wb = model(&[("S", "A1", deep.as_str())]);
let r = Calc::new(&wb, &lib).first_refusal().unwrap();
assert_eq!(r.id, "calc-formula-too-deep");
assert_eq!(
r.text,
"The formula in S!A1 nests more than 64 levels deep, more than Excel itself allows, so the formula cannot be computed."
);
let wb = model(&[("S", "A1", long.as_str())]);
let r = Calc::new(&wb, &lib).first_refusal().unwrap();
assert_eq!(r.id, "calc-formula-too-long");
assert_eq!(
r.text,
"The formula in S!A1 is longer than 8,192 characters, more than Excel itself allows, so the formula cannot be computed."
);
let calc = Calc::run(&wb, &lib);
assert_eq!(
at(&calc, "S", "A1"),
Computed::NotComputed(Reason::Construct(formula::TOO_LONG.to_string()))
);
}
#[test]
fn a_logical_test_coerces_a_text_read_through_a_reference() {
let mut wb = model(&[
("S", "C1", "=IF(A1,1,2)"),
("S", "C2", "=IF(A2,1,2)"),
("S", "C3", "=AND(A1)"),
("S", "C4", "=NOT(A1)"),
]);
for (row, text) in [(1, "TRUE"), (2, "x")] {
wb.sheets[0].set(
row,
1,
Cell {
content: Content::Text(text.to_string()),
style: 0,
},
);
}
let lib = library::language();
let calc = Calc::run(&wb, &lib);
let err = |k| Computed::Value(Value::Error(k));
assert_eq!(number(&calc, "S", "C1"), 1.0);
assert_eq!(at(&calc, "S", "C2"), err(ErrorKind::Value));
assert_eq!(at(&calc, "S", "C3"), err(ErrorKind::Value));
assert_eq!(at(&calc, "S", "C4"), Computed::Value(Value::Bool(false)));
}
#[test]
fn shared_and_dynamic_formulas_are_read_through_their_masters() {
let mut wb = Workbook::new();
let i = wb.ensure_sheet("Output");
let sheet: &mut Sheet = &mut wb.sheets[i];
sheet.set(
2,
2,
Cell {
content: Content::Number(5.0),
style: 0,
},
);
sheet.set(
3,
2,
Cell {
content: Content::Formula("B2*2".to_string()),
style: 0,
},
);
sheet.set_formula(parse_a1_range("C2:C4").unwrap(), "B2+B3", 0);
sheet.set_formula_dynamic(parse_a1_range("D2:D3").unwrap(), "B2*10", 0);
let lib = library::language();
let calc = Calc::run(&wb, &lib);
assert_eq!(
calc.formula((0, 3, 3)),
Some("B3+B4"),
"the child's text is the master's moved"
);
assert_eq!(number(&calc, "Output", "C2"), 15.0);
assert_eq!(number(&calc, "Output", "C3"), 10.0);
assert_eq!(number(&calc, "Output", "C4"), 0.0);
assert_eq!(number(&calc, "Output", "D2"), 50.0);
assert_eq!(number(&calc, "Output", "D3"), 100.0);
assert_eq!(calc.spell_cell((0, 3, 3)), "Output!C3");
}
}