Skip to main content

frazaro_core/
excel.rs

1//! Excel's library, registered through the language's functions seam
2//! (KERNEL.7, 2026-10-08): the declared subset `frazaro calc` computes, each
3//! function with the evidence that admitted it.
4//!
5//! The rule is `KERNEL.6`'s: a function enters the subset with its fixture
6//! and its measured frequency, never by taste. The measure is the one the
7//! roadmap cites, the two spreadsheet corpora of Jansen and Hermans, "Enron
8//! versus EUSES: A Comparison of Two Spreadsheet Corpora" (2015, arXiv
9//! 1503.04055), Table IV: the fifteen most used functions in each corpus,
10//! which cover 69% of the Enron corpus's spreadsheets and 70% of EUSES's.
11//! Of the Enron fifteen (SUM, IF, AVERAGE, VLOOKUP, ROUND, SUBTOTAL, OFFSET,
12//! CONCATENATE, NOW, DAVERAGE, SUMIF, INDEX, MATCH, LOOKUP, MONTH) this
13//! slice computes the five that are arithmetic and logic over values; the
14//! lookups, the database functions, the date and the text functions are
15//! `KERNEL.8`'s, `OFFSET` is unreadable by the reader's own rule, and `NOW`
16//! is volatile, which an engine refuses and a door shows as not computed.
17//! Of the EUSES fifteen (SUM, IF, ROUND, HYPERLINK, CONCATENATE, AND,
18//! COUNTIF, AVERAGE, OR, INDIRECT, MIN, ISNUMBER, MAX, VLOOKUP, ISBLANK) it
19//! computes nine. `IFS` enters on its fixture alone,
20//! `scripts/reflect/saved.xlsx`, the owner's first Excel-saved workbook,
21//! whose cached values are this oracle's first row. The language's own
22//! fourteen (`vla_lang::calc::library`) are here because
23//! `Alonzo/SPEC.md` section 13 named them for the engine's first
24//! cartridges; `IF`, `SUM`, `AND`, `OR`, `MIN` and `MAX` are in the tables
25//! too.
26//!
27//! Every function here is held to the language's exact tests or to its
28//! fixture: `scripts/recalc/subset.txt`, built by the door into
29//! `subset.xlsx` with one formula per function and per coercion this slice
30//! claims. Its cached values are Excel's the day the owner opens it in
31//! Excel 365 and saves it beside as `subset_saved.xlsx`, and
32//! `tools/check_recalc_golden.ps1` then holds every row of that file to
33//! `agree`; until then the door prints `unchecked` beside each, which is
34//! the honest word. A function outside this list shows `not computed here`
35//! with its name (`HORIZON.md` section 12.6), and the one line that admits
36//! it is a row in [`SUBSET`] with its evidence.
37
38use vla_lang::calc::graph::cells_in;
39use vla_lang::calc::library::{language, number_arg, numbers};
40use vla_lang::calc::value::{round15, text_to_number};
41use vla_lang::calc::{Area, Arg, Ctx, ErrorKind, Expr, Library, Reason, Value};
42use vla_lang::intrinsics::fold;
43
44/// One function of the subset and why it is in.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct Entry {
47    pub name: &'static str,
48    /// The measurement or the fixture that admitted it.
49    pub evidence: &'static str,
50}
51
52/// The declared subset, the language's fourteen first, then Excel's.
53pub const SUBSET: &[Entry] = &[
54    Entry {
55        name: "IF",
56        evidence: "Enron 2, EUSES 2; SPEC.md s13",
57    },
58    Entry {
59        name: "AND",
60        evidence: "EUSES 6; SPEC.md s13",
61    },
62    Entry {
63        name: "OR",
64        evidence: "EUSES 9; SPEC.md s13",
65    },
66    Entry {
67        name: "NOT",
68        evidence: "SPEC.md s13",
69    },
70    Entry {
71        name: "SUM",
72        evidence: "Enron 1, EUSES 1; SPEC.md s13",
73    },
74    Entry {
75        name: "MIN",
76        evidence: "EUSES 11; SPEC.md s13",
77    },
78    Entry {
79        name: "MAX",
80        evidence: "EUSES 13; SPEC.md s13",
81    },
82    Entry {
83        name: "ABS",
84        evidence: "SPEC.md s13",
85    },
86    Entry {
87        name: "INT",
88        evidence: "SPEC.md s13",
89    },
90    Entry {
91        name: "MOD",
92        evidence: "SPEC.md s13: every counter wraps",
93    },
94    Entry {
95        name: "ROW",
96        evidence: "SPEC.md s13: a cell painting at the mouse",
97    },
98    Entry {
99        name: "COLUMN",
100        evidence: "SPEC.md s13: a cell painting at the mouse",
101    },
102    Entry {
103        name: "CHOOSE",
104        evidence: "SPEC.md s13: a score until INDEX comes",
105    },
106    Entry {
107        name: "SIN",
108        evidence: "SPEC.md s13: a slow oscillator",
109    },
110    Entry {
111        name: "AVERAGE",
112        evidence: "Enron 3, EUSES 8",
113    },
114    Entry {
115        name: "ROUND",
116        evidence: "Enron 5, EUSES 3",
117    },
118    Entry {
119        name: "SUMIF",
120        evidence: "Enron 11",
121    },
122    Entry {
123        name: "COUNTIF",
124        evidence: "EUSES 7",
125    },
126    Entry {
127        name: "ISNUMBER",
128        evidence: "EUSES 12",
129    },
130    Entry {
131        name: "ISBLANK",
132        evidence: "EUSES 15",
133    },
134    Entry {
135        name: "IFS",
136        evidence: "scripts/reflect/saved.xlsx, the first Excel-saved fixture",
137    },
138];
139
140/// The functions named by the two corpora's top fifteen that this slice
141/// does not compute, each with the slice that takes it: what the honest
142/// label names most often on a real workbook.
143pub const NAMED_NOT_COMPUTED: &[(&str, &str)] = &[
144    ("VLOOKUP", "KERNEL.8, the lookups"),
145    ("INDEX", "KERNEL.8, the lookups"),
146    ("MATCH", "KERNEL.8, the lookups"),
147    ("LOOKUP", "KERNEL.8, the lookups"),
148    ("SUBTOTAL", "KERNEL.8, the lookups"),
149    ("DAVERAGE", "KERNEL.8, the database functions"),
150    ("CONCATENATE", "KERNEL.8, the text functions"),
151    ("MONTH", "KERNEL.8, the dates"),
152    ("NOW", "volatile: an engine refuses it, a door shows it"),
153    ("OFFSET", "unreadable by the reader's rule"),
154    ("INDIRECT", "unreadable by the reader's rule"),
155    (
156        "HYPERLINK",
157        "a link, not a value; the formula sink refuses it",
158    ),
159];
160
161/// Excel's library: the language's functions and the seven above.
162pub fn library() -> Library {
163    let mut lib = language();
164    lib.register_form("IFS", ifs);
165    lib.register("AVERAGE", average);
166    lib.register("ROUND", round);
167    lib.register("SUMIF", sumif);
168    lib.register("COUNTIF", countif);
169    lib.register("ISNUMBER", isnumber);
170    lib.register("ISBLANK", isblank);
171    lib
172}
173
174/// `IFS(c1, v1, c2, v2, ...)`: the value beside the first condition that
175/// holds; a condition's error is the answer; none holding is `#N/A`. Only
176/// the value chosen is computed.
177fn ifs<'e>(ctx: &Ctx<'_>, args: &'e [Expr]) -> Result<Arg<'e>, Reason> {
178    if args.len() < 2 || args.len() % 2 == 1 {
179        return Ok(Arg::Scalar(Value::Error(ErrorKind::NA)));
180    }
181    for pair in args.chunks(2) {
182        match ctx.truth(&pair[0])? {
183            Ok(true) => return ctx.eval(&pair[1]),
184            Ok(false) => {}
185            Err(k) => return Ok(Arg::Scalar(Value::Error(k))),
186        }
187    }
188    Ok(Arg::Scalar(Value::Error(ErrorKind::NA)))
189}
190
191/// `AVERAGE` over the numbers `SUM` would read; none is `#DIV/0!`.
192fn average(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
193    Ok(match numbers(ctx, args)? {
194        Err(k) => Value::Error(k),
195        Ok(ns) if ns.is_empty() => Value::Error(ErrorKind::Div0),
196        Ok(ns) => Value::Number(ns.iter().sum::<f64>() / ns.len() as f64),
197    })
198}
199
200/// `ROUND(x, digits)`: half away from zero, the digits read down to a
201/// whole number, a negative count rounding to tens and hundreds. The scaled
202/// value is first rounded to Excel's fifteen significant digits, so that
203/// `ROUND(2.675, 2)` is 2.68 as Excel has it and not the 2.67 the double
204/// 267.49999999999997 would give.
205fn round(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
206    if args.len() != 2 {
207        return Ok(Value::Error(ErrorKind::Value));
208    }
209    let x = match number_arg(ctx, args, 0)? {
210        Ok(n) => n,
211        Err(k) => return Ok(Value::Error(k)),
212    };
213    let digits = match number_arg(ctx, args, 1)? {
214        Ok(n) => n.trunc(),
215        Err(k) => return Ok(Value::Error(k)),
216    };
217    let rounded = if digits >= 0.0 {
218        let m = 10f64.powi(digits.min(300.0) as i32);
219        round15(x * m).round() / m
220    } else {
221        let p = 10f64.powi((-digits).min(300.0) as i32);
222        round15(x / p).round() * p
223    };
224    Ok(vla_lang::calc::eval::finite(rounded))
225}
226
227/// What a criterion of `SUMIF` or `COUNTIF` asks of a cell.
228#[derive(Clone, Debug, PartialEq)]
229enum Criterion {
230    Eq(Target),
231    Ne(Target),
232    Lt(Target),
233    Gt(Target),
234    Le(Target),
235    Ge(Target),
236}
237
238#[derive(Clone, Debug, PartialEq)]
239enum Target {
240    Number(f64),
241    /// A text with `*`, `?` and `~` as Excel's wildcards, folded.
242    Text(String),
243    Bool(bool),
244    Error(ErrorKind),
245    Blank,
246}
247
248/// A criterion as Excel reads one: a number, a truth value or an error
249/// matches cells equal to it; a text may begin with `=`, `<>`, `<`, `>`,
250/// `<=` or `>=`, and what follows is a number when it spells one, `TRUE` or
251/// `FALSE` when it spells either, blank when empty, a text pattern
252/// otherwise.
253fn criterion(v: &Value) -> Criterion {
254    let text = match v {
255        Value::Number(n) => return Criterion::Eq(Target::Number(*n)),
256        Value::Bool(b) => return Criterion::Eq(Target::Bool(*b)),
257        Value::Error(k) => return Criterion::Eq(Target::Error(*k)),
258        Value::Empty => return Criterion::Eq(Target::Blank),
259        Value::Text(t) => t.as_str(),
260    };
261    let (op, rest): (fn(Target) -> Criterion, &str) = if let Some(r) = text.strip_prefix("<>") {
262        (Criterion::Ne, r)
263    } else if let Some(r) = text.strip_prefix("<=") {
264        (Criterion::Le, r)
265    } else if let Some(r) = text.strip_prefix(">=") {
266        (Criterion::Ge, r)
267    } else if let Some(r) = text.strip_prefix('<') {
268        (Criterion::Lt, r)
269    } else if let Some(r) = text.strip_prefix('>') {
270        (Criterion::Gt, r)
271    } else if let Some(r) = text.strip_prefix('=') {
272        (Criterion::Eq, r)
273    } else {
274        (Criterion::Eq, text)
275    };
276    let target = if rest.is_empty() {
277        Target::Blank
278    } else if let Some(n) = text_to_number(rest) {
279        Target::Number(n)
280    } else if let Some(k) = ErrorKind::parse(rest) {
281        Target::Error(k)
282    } else {
283        match fold(rest).as_str() {
284            "true" => Target::Bool(true),
285            "false" => Target::Bool(false),
286            _ => Target::Text(fold(rest)),
287        }
288    };
289    op(target)
290}
291
292/// Whether a cell's value meets the criterion. A text that spells a number
293/// counts as the number for equality, as `COUNTIF` counts it; a comparison
294/// by size holds only between values of one kind; `<>` holds for every
295/// cell that `=` would not count, an empty cell included.
296fn meets(cell: &Value, c: &Criterion) -> bool {
297    use std::cmp::Ordering::*;
298    let ordering = |target: &Target| -> Option<std::cmp::Ordering> {
299        match (cell, target) {
300            (Value::Number(x), Target::Number(n)) => x.partial_cmp(n),
301            (Value::Text(t), Target::Number(n)) => {
302                text_to_number(t).and_then(|x| (x == *n).then_some(Equal))
303            }
304            (Value::Text(t), Target::Text(pattern)) => {
305                if wildcard(pattern, &fold(t)) {
306                    Some(Equal)
307                } else {
308                    Some(fold(t).cmp(pattern))
309                }
310            }
311            (Value::Bool(b), Target::Bool(want)) => Some(b.cmp(want)),
312            (Value::Error(k), Target::Error(want)) => (k == want).then_some(Equal),
313            (Value::Empty, Target::Blank) => Some(Equal),
314            (Value::Text(t), Target::Blank) if t.is_empty() => Some(Equal),
315            _ => None,
316        }
317    };
318    match c {
319        Criterion::Eq(t) => ordering(t) == Some(Equal),
320        Criterion::Ne(t) => ordering(t) != Some(Equal),
321        Criterion::Lt(t) => matches!(
322            (ordering(t), t),
323            (Some(Less), Target::Number(_) | Target::Text(_))
324        ),
325        Criterion::Gt(t) => matches!(
326            (ordering(t), t),
327            (Some(Greater), Target::Number(_) | Target::Text(_))
328        ),
329        Criterion::Le(t) => matches!(
330            (ordering(t), t),
331            (
332                Some(Less) | Some(Equal),
333                Target::Number(_) | Target::Text(_)
334            )
335        ),
336        Criterion::Ge(t) => matches!(
337            (ordering(t), t),
338            (
339                Some(Greater) | Some(Equal),
340                Target::Number(_) | Target::Text(_)
341            )
342        ),
343    }
344}
345
346/// Excel's wildcards over folded text: `*` any run, `?` one character, `~`
347/// the next character itself.
348fn wildcard(pattern: &str, text: &str) -> bool {
349    fn go(p: &[char], t: &[char]) -> bool {
350        match p.split_first() {
351            None => t.is_empty(),
352            Some(('*', rest)) => (0..=t.len()).any(|i| go(rest, &t[i..])),
353            Some(('?', rest)) => !t.is_empty() && go(rest, &t[1..]),
354            Some(('~', rest)) => match (rest.split_first(), t.split_first()) {
355                (Some((want, rest2)), Some((have, t2))) => want == have && go(rest2, t2),
356                (None, _) => t.is_empty(),
357                _ => false,
358            },
359            Some((want, rest)) => match t.split_first() {
360                Some((have, t2)) => want == have && go(rest, t2),
361                None => false,
362            },
363        }
364    }
365    let p: Vec<char> = pattern.chars().collect();
366    let t: Vec<char> = text.chars().collect();
367    if !p.iter().any(|c| matches!(c, '*' | '?' | '~')) {
368        return p == t;
369    }
370    go(&p, &t)
371}
372
373/// One area from an argument, for the range of `SUMIF` and `COUNTIF`.
374fn one_area(arg: Option<&Arg>) -> Option<Area> {
375    match arg {
376        Some(Arg::Area { areas, .. }) if areas.len() == 1 => Some(areas[0]),
377        _ => None,
378    }
379}
380
381/// `COUNTIF(range, criterion)`: the cells of the range meeting the
382/// criterion; a criterion a blank meets counts the empty cells of the whole
383/// range, as Excel counts them, however far it reaches.
384fn countif(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
385    if args.len() != 2 {
386        return Ok(Value::Error(ErrorKind::Value));
387    }
388    let Some(area) = one_area(args.first()) else {
389        return Ok(Value::Error(ErrorKind::Value));
390    };
391    let c = criterion(&ctx.one(&args[1])?);
392    let model = ctx.env.model();
393    let Some(sheet) = model.sheets.get(area.sheet) else {
394        return Ok(Value::Error(ErrorKind::Ref));
395    };
396    let mut count = 0u64;
397    let mut present = 0u64;
398    for ((row, col), _) in cells_in(sheet, ctx.env.extent(area.sheet), &area) {
399        let v = ctx.env.read((area.sheet, row, col))?;
400        if v != Value::Empty {
401            present += 1;
402            if meets(&v, &c) {
403                count += 1;
404            }
405        }
406    }
407    let cells = u64::from(area.bottom - area.top + 1) * u64::from(area.right - area.left + 1);
408    if meets(&Value::Empty, &c) {
409        count += cells - present;
410    }
411    Ok(Value::Number(count as f64))
412}
413
414/// `SUMIF(range, criterion, [sum_range])`: the numbers of the sum range
415/// (the range itself when none is given) at the places where the range's
416/// cell meets the criterion; the sum range is read from its top-left cell
417/// in the range's shape, as Excel reads it.
418fn sumif(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
419    if args.len() < 2 || args.len() > 3 {
420        return Ok(Value::Error(ErrorKind::Value));
421    }
422    let Some(area) = one_area(args.first()) else {
423        return Ok(Value::Error(ErrorKind::Value));
424    };
425    let c = criterion(&ctx.one(&args[1])?);
426    let sum_area = match args.get(2) {
427        None => area,
428        Some(a) => match one_area(Some(a)) {
429            Some(s) => Area {
430                sheet: s.sheet,
431                top: s.top,
432                left: s.left,
433                bottom: s.top.saturating_add(area.bottom - area.top),
434                right: s.left.saturating_add(area.right - area.left),
435            },
436            None => return Ok(Value::Error(ErrorKind::Value)),
437        },
438    };
439    let model = ctx.env.model();
440    let Some(sum_sheet) = model.sheets.get(sum_area.sheet) else {
441        return Ok(Value::Error(ErrorKind::Ref));
442    };
443    // Every contribution is a present cell of the sum range; the criterion
444    // is read at the same offset in the range, an absent cell being empty.
445    let mut total = 0.0;
446    for ((row, col), _) in cells_in(sum_sheet, ctx.env.extent(sum_area.sheet), &sum_area) {
447        let value = ctx.env.read((sum_area.sheet, row, col))?;
448        let n = match value {
449            Value::Number(n) => n,
450            Value::Error(k) => return Ok(Value::Error(k)),
451            _ => continue,
452        };
453        let at = (
454            area.sheet,
455            area.top + (row - sum_area.top),
456            area.left + (col - sum_area.left),
457        );
458        let tested = ctx.env.read(at)?;
459        if meets(&tested, &c) {
460            total += n;
461        }
462    }
463    Ok(vla_lang::calc::eval::finite(total))
464}
465
466/// `ISNUMBER(x)`: whether the value is a number; an error is not, and does
467/// not propagate.
468fn isnumber(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
469    if args.len() != 1 {
470        return Ok(Value::Error(ErrorKind::Value));
471    }
472    Ok(Value::Bool(matches!(ctx.one(&args[0])?, Value::Number(_))))
473}
474
475/// `ISBLANK(x)`: whether the cell is empty; a formula's empty result is 0
476/// and so is not.
477fn isblank(ctx: &Ctx<'_>, args: &[Arg]) -> Result<Value, Reason> {
478    if args.len() != 1 {
479        return Ok(Value::Error(ErrorKind::Value));
480    }
481    Ok(Value::Bool(ctx.one(&args[0])? == Value::Empty))
482}
483
484/// The names the subset holds, for a page to list: the language's first.
485pub fn subset_names() -> Vec<&'static str> {
486    SUBSET.iter().map(|e| e.name).collect()
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use vla_lang::calc::library::NAMES as LANGUAGE_NAMES;
493    use vla_lang::calc::{Calc, Computed};
494    use vla_lang::sheet::{parse_a1_range, Cell, Content, Workbook};
495
496    fn model(rows: &[(&str, &str, &str)]) -> Workbook {
497        let mut wb = Workbook::new();
498        for (sheet, addr, text) in rows {
499            let i = wb.ensure_sheet(sheet);
500            let r = parse_a1_range(addr).unwrap();
501            let content = if let Some(f) = text.strip_prefix('=') {
502                Content::Formula(f.to_string())
503            } else if let Ok(n) = text.parse::<f64>() {
504                Content::Number(n)
505            } else if *text == "TRUE" || *text == "FALSE" {
506                Content::Bool(*text == "TRUE")
507            } else {
508                Content::Text(text.to_string())
509            };
510            wb.sheets[i].set(r.top, r.left, Cell { content, style: 0 });
511        }
512        wb
513    }
514
515    fn at(calc: &Calc<'_>, addr: &str) -> Computed {
516        let r = parse_a1_range(addr).unwrap();
517        calc.computed((0, r.top, r.left)).cloned().unwrap()
518    }
519
520    fn number(calc: &Calc<'_>, addr: &str) -> f64 {
521        match at(calc, addr) {
522            Computed::Value(Value::Number(n)) => n,
523            other => panic!("{addr}: {other:?}"),
524        }
525    }
526
527    #[test]
528    fn the_subset_table_is_the_library_and_the_language_comes_first() {
529        let lib = library();
530        let mut names: Vec<&str> = SUBSET.iter().map(|e| e.name).collect();
531        names.sort_unstable();
532        assert_eq!(
533            lib.names(),
534            names,
535            "every function admitted has its evidence row, and no other"
536        );
537        assert_eq!(
538            &SUBSET[..LANGUAGE_NAMES.len()]
539                .iter()
540                .map(|e| e.name)
541                .collect::<Vec<_>>(),
542            &LANGUAGE_NAMES.to_vec()
543        );
544        for e in SUBSET {
545            assert!(!e.evidence.is_empty(), "{} has no evidence", e.name);
546        }
547        for (name, _) in NAMED_NOT_COMPUTED {
548            assert!(!lib.has(name), "{name} is listed as not computed");
549        }
550    }
551
552    #[test]
553    fn excel_s_functions_compute_as_excel_does() {
554        let wb = model(&[
555            ("S", "A1", "10"),
556            ("S", "A2", "20"),
557            ("S", "A3", "x"),
558            ("S", "A4", "30"),
559            ("S", "B1", "1"),
560            ("S", "B2", "2"),
561            ("S", "B4", "4"),
562            ("S", "C1", "=AVERAGE(A1:A4)"),
563            ("S", "C2", "=AVERAGE(A3)"),
564            ("S", "C3", "=ROUND(2.675,2)"),
565            ("S", "C4", "=ROUND(-2.5,0)"),
566            ("S", "C5", "=ROUND(1234.5678,-2)"),
567            ("S", "C6", "=ROUND(1.234,1.9)"),
568            ("S", "C7", "=SUMIF(A1:A4,\">15\")"),
569            ("S", "C8", "=SUMIF(A1:A4,\">15\",B1:B4)"),
570            ("S", "C9", "=COUNTIF(A1:A4,\">15\")"),
571            ("S", "C10", "=COUNTIF(A1:A4,\"x\")"),
572            ("S", "C11", "=COUNTIF(A1:A6,\"\")"),
573            ("S", "C12", "=COUNTIF(A1:A4,\"<>10\")"),
574            ("S", "C13", "=COUNTIF(A1:A4,\"*\")"),
575            ("S", "C14", "=COUNTIF(A1:A4,10)"),
576            ("S", "C15", "=ISNUMBER(A1)"),
577            ("S", "C16", "=ISNUMBER(A3)"),
578            ("S", "C17", "=ISNUMBER(1/0)"),
579            ("S", "C18", "=ISBLANK(A5)"),
580            ("S", "C19", "=ISBLANK(A1)"),
581            ("S", "C20", "=IFS(A1>15,\"big\",TRUE,\"small\")"),
582            ("S", "C21", "=IFS(A1>15,\"big\")"),
583            ("S", "C22", "=IFS(A3,1,TRUE,2)"),
584            ("S", "C23", "=COUNTIF(B1:B4,\"<>\")"),
585            ("S", "C24", "=COUNTIF(A1:A4,\"?\")"),
586            ("S", "C25", "=SUMIF(A1:A4,\"x\",B1:B4)"),
587        ]);
588        let lib = library();
589        let calc = Calc::run(&wb, &lib);
590        assert_eq!(number(&calc, "C1"), 20.0, "the text is skipped");
591        assert_eq!(
592            at(&calc, "C2"),
593            Computed::Value(Value::Error(ErrorKind::Div0))
594        );
595        assert_eq!(number(&calc, "C3"), 2.68);
596        assert_eq!(number(&calc, "C4"), -3.0);
597        assert_eq!(number(&calc, "C5"), 1200.0);
598        assert_eq!(number(&calc, "C6"), 1.2);
599        assert_eq!(number(&calc, "C7"), 50.0);
600        assert_eq!(number(&calc, "C8"), 6.0);
601        assert_eq!(number(&calc, "C9"), 2.0);
602        assert_eq!(number(&calc, "C10"), 1.0);
603        assert_eq!(number(&calc, "C11"), 2.0, "A5 and A6 are blank");
604        assert_eq!(number(&calc, "C12"), 3.0, "the text counts as not 10");
605        assert_eq!(number(&calc, "C13"), 1.0, "* matches texts alone");
606        assert_eq!(number(&calc, "C14"), 1.0);
607        assert_eq!(at(&calc, "C15"), Computed::Value(Value::Bool(true)));
608        assert_eq!(at(&calc, "C16"), Computed::Value(Value::Bool(false)));
609        assert_eq!(
610            at(&calc, "C17"),
611            Computed::Value(Value::Bool(false)),
612            "an error is not a number and does not propagate"
613        );
614        assert_eq!(at(&calc, "C18"), Computed::Value(Value::Bool(true)));
615        assert_eq!(at(&calc, "C19"), Computed::Value(Value::Bool(false)));
616        assert_eq!(
617            at(&calc, "C20"),
618            Computed::Value(Value::Text("small".to_string()))
619        );
620        assert_eq!(
621            at(&calc, "C21"),
622            Computed::Value(Value::Error(ErrorKind::NA))
623        );
624        assert_eq!(
625            at(&calc, "C22"),
626            Computed::Value(Value::Error(ErrorKind::Value)),
627            "a text condition"
628        );
629        assert_eq!(number(&calc, "C23"), 3.0, "the non-blank cells");
630        assert_eq!(number(&calc, "C24"), 1.0);
631        assert_eq!(number(&calc, "C25"), 0.0, "B3 is absent");
632    }
633
634    #[test]
635    fn criteria_read_as_excel_reads_them() {
636        assert_eq!(
637            criterion(&Value::Text(">=5".into())),
638            Criterion::Ge(Target::Number(5.0))
639        );
640        assert_eq!(
641            criterion(&Value::Text("<>".into())),
642            Criterion::Ne(Target::Blank)
643        );
644        assert_eq!(
645            criterion(&Value::Text("=TRUE".into())),
646            Criterion::Eq(Target::Bool(true))
647        );
648        assert_eq!(
649            criterion(&Value::Text("a*".into())),
650            Criterion::Eq(Target::Text("a*".into()))
651        );
652        assert_eq!(
653            criterion(&Value::Number(3.0)),
654            Criterion::Eq(Target::Number(3.0))
655        );
656        assert!(wildcard("a*", "abc"));
657        assert!(wildcard("a?c", "abc"));
658        assert!(!wildcard("a?c", "abbc"));
659        assert!(wildcard("10~*", "10*"));
660        assert!(wildcard("*", ""));
661        assert!(meets(
662            &Value::Text("5".into()),
663            &Criterion::Eq(Target::Number(5.0))
664        ));
665        assert!(
666            !meets(
667                &Value::Text("5".into()),
668                &Criterion::Gt(Target::Number(1.0))
669            ),
670            "a text is not compared by size against a number"
671        );
672        assert!(meets(&Value::Empty, &Criterion::Ne(Target::Number(5.0))));
673        assert!(meets(
674            &Value::Text("ABC".into()),
675            &Criterion::Eq(Target::Text("abc".into()))
676        ));
677    }
678}