Skip to main content

vla_lang/
view.rs

1//! The view record (KERNEL.4, 2026-10-07): one window of the sheet model as
2//! lines, the first implementation of the kernel's projections seam
3//! ([`crate::projection::Projection`]; the design is `web/CALLOSUM.md` §7,
4//! decisions 1 and 2, and §8, slice 1).
5//!
6//! A view is a pure function of the model and a window: no handle kept, no
7//! state, nothing read back from the `.xlsx` bytes. The model is the one the
8//! writer builds (`build::build_workbook`), so a door that translates and
9//! builds a program draws it without writing a file, and a viewport
10//! (KERNEL.5) draws from this record alone. One form a line, in the proof
11//! corpus's notation, as `reflect` prints a file:
12//!
13//! ```text
14//! (sheet "Frazaro" visible)          every sheet of the model, in tab order, with
15//!                                    its own state: visible, hidden, very-hidden
16//! (window "Output" "A1:F20")         the window, its sheet as the model spells it
17//! (extent "Output" "B1:D4")          the sheet's used rectangle, or none
18//! (gridlines "Output" on)
19//! (column "Frazaro" "B" 72 shown 1)  a column of the window with settings:
20//!                                    width or none, shown or hidden, format or none
21//! (row "Board" 3 30 shown)           a row of the window with settings (KERNEL.22):
22//!                                    height or none, shown or hidden
23//! (format 1 "F7F4FC" text nowrap)    format 0 and every format the window uses:
24//!                                    fill or none, general or text, wrap or nowrap
25//! (look "Board" 1 2)                 the sheet's look map (KERNEL.22): a cell
26//!                                    holding the value draws with the format
27//! (cell "Output" "B2" 5)             a value, spelled as reflect spells it
28//! (formula "Output" "B3" "=B2*2")    a formula's text as the bar shows it
29//! (value "Output" "B3" 10)           its computed value, once values exist
30//! (style "Output" "B2" 1)            the cell's format, when it is not 0
31//! (sentence "Output" "B2" 4)         the row of the sentence that wrote the cell
32//! ```
33//!
34//! The cells come in row-major order, each `cell` or `formula` row followed
35//! by its `value` row (a formula's, once values exist), its `style` row and
36//! its `sentence` row. The `cell` and `formula` rows are the reader's own,
37//! spelled by `reflect::print`, which is the free oracle: the view of a
38//! built model's sheet, whole, is `reflect`'s reading of the file the
39//! writer writes from that model, row for row. A `cell` row is a value the
40//! sentences put there, a `formula` row is text the host would compute, and
41//! the record never shows one as the other. Since recalculation landed
42//! (KERNEL.7, 2026-10-08; `Alonzo/SPEC.md` section 4.5 and decision 14) a
43//! `(value "<sheet>" "<addr>" <v>)` row follows each formula row when the
44//! caller computed the model ([`view_text_valued`], a [`Grid`] holding a
45//! [`Calc`]): the value spelled as a `cell` row's is, or
46//! `(not-computed "<name>")` for a formula this version does not compute,
47//! naming the function or the construct, `(not-computed cycle)` for one in
48//! a cycle. The bare [`view_text`] prints no value row, which is what a
49//! grid shows before its first step.
50//!
51//! The second projection, the values ([`ValueGrid`], `values`; KERNEL.37,
52//! 2026-10-12), is the same record with its `formula` rows left out: every
53//! other row the grid's, in the grid's order, so that for any window it is
54//! the grid record with its `(formula ` lines removed, byte for byte. It is
55//! what an engine's host draws every frame ([`view_text_values`]), a
56//! formula's text being the bar's, one cell at a time, where the grid moves
57//! every shared child's text from its master at every view. A formula with
58//! no value yet prints no row of its own, its `style` and `sentence` rows
59//! kept; a shared formula's cell whose master the sheet has lost prints
60//! nothing in either record. Both read a sheet's `extent` row from the
61//! extent their values keep ([`Values::kept_extent`]), where they keep one.
62//!
63//! The changes of a window (KERNEL.42, 2026-10-15) are an engine's machine's
64//! to answer, since they need two frames, and are printed here: a `changes`
65//! row naming the sheet, the window, the frame they are since and the frame
66//! they are at, and how many cells changed, then each changed cell's rows as
67//! the values print them, by the same code, in the same order; a cell that
68//! now prints no row is `(empty "<sheet>" "<addr>")`. Where the machine
69//! cannot say which cells changed, the row ends `whole` and none follows,
70//! and the host views the window. A host holding the window's values at the
71//! first frame, and replacing each named cell's rows with the rows given it,
72//! holds the window's values at the second, byte for byte.
73
74use std::collections::{BTreeSet, HashMap};
75
76use crate::calc::{Extent, Values};
77use crate::messages::{raise, Refusal};
78use crate::projection::{Projection, Window};
79use crate::rows::{line as relation, quoted};
80use crate::rows::{Row, Value};
81use crate::sheet::{
82    cell_ref, column_letters, formula_text, number_text, parse_a1_range, shared_masters, A1Range,
83    Cell, Column, Content, NumFmt, Sheet, Style, Workbook,
84};
85
86/// The grid: the view record of one window, the first projection. With the
87/// values of a recalculation over the same model (a [`crate::calc::Calc`],
88/// or an engine's machine since KERNEL.22), each formula row is followed by
89/// its `value` row; without them, the record shows formulas as text alone.
90#[derive(Clone, Copy, Debug, Default)]
91pub struct Grid<'v> {
92    pub values: Option<&'v dyn Values>,
93}
94
95impl<'v> Grid<'v> {
96    /// The grid over a computed model.
97    pub fn valued(values: &'v dyn Values) -> Grid<'v> {
98        Grid {
99            values: Some(values),
100        }
101    }
102}
103
104impl Projection for Grid<'_> {
105    fn name(&self) -> &str {
106        "grid"
107    }
108
109    fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
110        self.rows(model, window, true, out);
111    }
112}
113
114/// The values: the view record of one window with its `formula` rows left
115/// out, the second projection (KERNEL.37). With the values of a
116/// recalculation or an engine's machine, each formula's `value` row stands
117/// where the grid prints it, after the row the grid prints before it; the
118/// record of a window holding no formula is the grid's.
119#[derive(Clone, Copy, Debug, Default)]
120pub struct ValueGrid<'v> {
121    pub values: Option<&'v dyn Values>,
122}
123
124impl<'v> ValueGrid<'v> {
125    /// The values of a computed model.
126    pub fn valued(values: &'v dyn Values) -> ValueGrid<'v> {
127        ValueGrid {
128            values: Some(values),
129        }
130    }
131}
132
133impl Projection for ValueGrid<'_> {
134    fn name(&self) -> &str {
135        "values"
136    }
137
138    fn project(&self, model: &Workbook, window: &Window, out: &mut dyn FnMut(&str)) {
139        Grid {
140            values: self.values,
141        }
142        .rows(model, window, false, out);
143    }
144}
145
146impl Grid<'_> {
147    /// The record's rows of one window: every row the grid prints, or with
148    /// `formulas` false every row but the `formula` rows, which is the values
149    /// (KERNEL.37).
150    fn rows(&self, model: &Workbook, window: &Window, formulas: bool, out: &mut dyn FnMut(&str)) {
151        for s in &model.sheets {
152            out(&relation(&Row::Sheet {
153                name: &s.name,
154                visibility: s.visibility,
155            }));
156        }
157        let Some(i) = model.find_sheet(&window.sheet) else {
158            // A window onto no sheet of the model, which `window_of` never
159            // makes: the sheet rows and the window asked for, nothing more.
160            out(&format!(
161                "(window {} {})",
162                quoted(&window.sheet),
163                quoted(&window.range.text())
164            ));
165            return;
166        };
167        let sheet = &model.sheets[i];
168        let name = sheet.name.as_str();
169        let range = window.range;
170        out(&format!(
171            "(window {} {})",
172            quoted(name),
173            quoted(&range.text())
174        ));
175        match extent_in(sheet, i, self.values) {
176            Some(e) => out(&format!("(extent {} {})", quoted(name), quoted(&e.text()))),
177            None => out(&format!("(extent {} none)", quoted(name))),
178        }
179        out(&format!(
180            "(gridlines {} {})",
181            quoted(name),
182            if sheet.gridlines { "on" } else { "off" }
183        ));
184        let mut columns: Vec<&Column> = sheet
185            .columns
186            .iter()
187            .filter(|c| c.index >= range.left && c.index <= range.right)
188            .collect();
189        columns.sort_by_key(|c| c.index);
190        let mut used: BTreeSet<u32> = BTreeSet::new();
191        used.insert(0);
192        for c in &columns {
193            let width = c
194                .width
195                .and_then(number_text)
196                .unwrap_or_else(|| "none".to_string());
197            let style = match c.style {
198                Some(s) => {
199                    used.insert(s);
200                    s.to_string()
201                }
202                None => "none".to_string(),
203            };
204            out(&format!(
205                "(column {} {} {} {} {})",
206                quoted(name),
207                quoted(&column_letters(c.index)),
208                width,
209                if c.hidden { "hidden" } else { "shown" },
210                style
211            ));
212        }
213        // The rows of the window with settings, the column rows' twins
214        // (KERNEL.22; Alonzo/SPEC.md decision 19).
215        for (n, r) in sheet.rows.range(range.top..=range.bottom) {
216            let height = r
217                .height
218                .and_then(number_text)
219                .unwrap_or_else(|| "none".to_string());
220            out(&format!(
221                "(row {} {} {} {})",
222                quoted(name),
223                n,
224                height,
225                if r.hidden { "hidden" } else { "shown" }
226            ));
227        }
228        let cells: Vec<(&(u32, u32), &Cell)> = sheet
229            .cells
230            .range((range.top, 0)..=(range.bottom, u32::MAX))
231            .filter(|((_, col), _)| *col >= range.left && *col <= range.right)
232            .collect();
233        for (_, cell) in &cells {
234            used.insert(cell.style);
235        }
236        for (_, index) in &sheet.looks {
237            used.insert(*index);
238        }
239        for index in used {
240            out(&format_row(index, model.styles.xfs().get(index as usize)));
241        }
242        // The sheet's look map, a value to the format a cell holding it draws
243        // with (KERNEL.22; Alonzo/SPEC.md decision 19).
244        for (value, index) in &sheet.looks {
245            out(&format!("(look {} {} {})", quoted(name), value, index));
246        }
247        // The sheet's shared formulas' masters, found by one walk of the
248        // sheet at the first cell that needs them (KERNEL.37): the grid moves
249        // a child's text from its master; the values ask only whether a
250        // child with no value has one.
251        let mut masters = None;
252        // A sheet whose cells are newer where its values say (KERNEL.27): an
253        // engine's twin, read through its values' own content.
254        let lags = self.values.filter(|v| v.lags(i));
255        #[cfg(test)]
256        crate::sheet::work::viewed(cells.len());
257        for (&(row, col), cell) in cells {
258            self.cell_rows(
259                sheet,
260                i,
261                (row, col),
262                cell,
263                lags,
264                formulas,
265                &mut masters,
266                out,
267            );
268        }
269    }
270
271    /// One cell's rows, as the record prints them (KERNEL.42: the changes
272    /// print a changed cell's by this code too): its `cell` row, or its
273    /// `formula` row where the record has them and its `value` row once it
274    /// has one, then its `style` and `sentence` rows; whether it printed
275    /// any. `lags` are the values of a sheet whose cells are newer where they
276    /// say, and `masters` the sheet's shared formulas', found once.
277    #[allow(clippy::too_many_arguments)]
278    fn cell_rows<'s>(
279        &self,
280        sheet: &'s Sheet,
281        i: usize,
282        (row, col): (u32, u32),
283        cell: &Cell,
284        lags: Option<&dyn Values>,
285        formulas: bool,
286        masters: &mut Option<HashMap<u32, (&'s str, u32, u32)>>,
287        out: &mut dyn FnMut(&str),
288    ) -> bool {
289        let name = sheet.name.as_str();
290        let addr = cell_ref(row, col);
291        let newer = lags.and_then(|v| v.content((i, row, col)));
292        let printed = match newer.as_ref().unwrap_or(&cell.content) {
293            Content::Text(t) => {
294                let value = Value::Text(t.clone());
295                out(&relation(&Row::Cell {
296                    sheet: name,
297                    addr: &addr,
298                    value: &value,
299                }));
300                true
301            }
302            Content::Number(n) => {
303                // What the writer puts in the file: the number's text,
304                // or Excel's #NUM! for one no cell can hold.
305                let value = match number_text(*n) {
306                    Some(text) => Value::Number(text),
307                    None => Value::Error("#NUM!".to_string()),
308                };
309                out(&relation(&Row::Cell {
310                    sheet: name,
311                    addr: &addr,
312                    value: &value,
313                }));
314                true
315            }
316            Content::Bool(b) => {
317                let value = Value::Bool(*b);
318                out(&relation(&Row::Cell {
319                    sheet: name,
320                    addr: &addr,
321                    value: &value,
322                }));
323                true
324            }
325            Content::Error(e) => {
326                let value = Value::Error(e.clone());
327                out(&relation(&Row::Cell {
328                    sheet: name,
329                    addr: &addr,
330                    value: &value,
331                }));
332                true
333            }
334            other => {
335                let computed = self.values.and_then(|calc| calc.computed((i, row, col)));
336                // A formula with a text: every one but a shared formula's
337                // cell whose master the sheet has lost, which shows
338                // nothing in either record.
339                let held = if formulas {
340                    let walked = masters.get_or_insert_with(|| shared_masters(sheet));
341                    match formula_text(other, row, col, walked) {
342                        Some(text) => {
343                            let text = format!("={text}");
344                            out(&relation(&Row::Formula {
345                                sheet: name,
346                                addr: &addr,
347                                text: &text,
348                            }));
349                            true
350                        }
351                        None => false,
352                    }
353                } else {
354                    match other {
355                        // A value is only ever computed for a formula with
356                        // a text, so a child with one has its master; a
357                        // child without one asks the sheet.
358                        Content::SharedChild { si } if computed.is_none() => masters
359                            .get_or_insert_with(|| shared_masters(sheet))
360                            .contains_key(si),
361                        _ => true,
362                    }
363                };
364                if let Some(computed) = computed.filter(|_| held) {
365                    out(&format!(
366                        "(value {} {} {})",
367                        quoted(name),
368                        quoted(&addr),
369                        computed.spell()
370                    ));
371                }
372                held
373            }
374        };
375        if !printed {
376            return false;
377        }
378        if cell.style != 0 {
379            out(&format!(
380                "(style {} {} {})",
381                quoted(name),
382                quoted(&addr),
383                cell.style
384            ));
385        }
386        if let Some(r) = sheet.sentences.get(&(row, col)) {
387            out(&format!(
388                "(sentence {} {} {})",
389                quoted(name),
390                quoted(&addr),
391                r
392            ));
393        }
394        true
395    }
396}
397
398/// A `format` row: the index, the fill as six hex digits or `none`, the
399/// number format, and whether text wraps. An index the style table does not
400/// hold reads as the default format.
401fn format_row(index: u32, style: Option<&Style>) -> String {
402    let default = Style::default();
403    let s = style.unwrap_or(&default);
404    let fill = match s.fill {
405        Some(rgb) => quoted(&format!("{:02X}{:02X}{:02X}", rgb.0, rgb.1, rgb.2)),
406        None => "none".to_string(),
407    };
408    let num = match s.num_fmt {
409        NumFmt::General => "general",
410        NumFmt::Text => "text",
411    };
412    format!(
413        "(format {index} {fill} {num} {})",
414        if s.wrap { "wrap" } else { "nowrap" }
415    )
416}
417
418/// The sheet's used rectangle, or `None` for a sheet with no cell.
419fn extent_of(sheet: &Sheet) -> Option<A1Range> {
420    rectangle(sheet.extent())
421}
422
423/// The used rectangle of sheet `i`, read from the extent `values` keep
424/// where they keep one, else walked (KERNEL.37).
425fn extent_in(sheet: &Sheet, i: usize, values: Option<&dyn Values>) -> Option<A1Range> {
426    match values.and_then(|v| v.kept_extent(i)) {
427        Some(kept) => rectangle(kept),
428        None => extent_of(sheet),
429    }
430}
431
432/// An extent as a rectangle of cells.
433fn rectangle(extent: Extent) -> Option<A1Range> {
434    extent.map(|((top, left), (bottom, right))| A1Range {
435        top,
436        left,
437        bottom,
438        right,
439    })
440}
441
442/// The window over a whole sheet: its extent, or `A1` alone for a sheet
443/// with no cell.
444pub fn whole(sheet: &Sheet) -> A1Range {
445    or_a1(extent_of(sheet))
446}
447
448/// A rectangle, or `A1` alone for none.
449fn or_a1(range: Option<A1Range>) -> A1Range {
450    range.unwrap_or(A1Range {
451        top: 1,
452        left: 1,
453        bottom: 1,
454        right: 1,
455    })
456}
457
458/// The window a door asked for, against the model: the sheet by name as
459/// Excel compares names, without case, spelled in the window as the model
460/// spells it; the range as written (`A1:F20`, `b2`, corners in any order),
461/// or the sheet's whole extent when none is given. A sheet the model does
462/// not hold is refused naming the ones it does (`view-sheet-unknown`); a
463/// text that is not a rectangle of cells, a whole column or row included,
464/// is refused as written (`view-window-not-a-range`).
465pub fn window_of(model: &Workbook, sheet: &str, range: Option<&str>) -> Result<Window, Refusal> {
466    window_over(model, sheet, range, None)
467}
468
469/// [`window_of`], a whole sheet's extent read from the extent `values` keep
470/// where they keep one (KERNEL.37): the window an engine's machine views.
471pub(crate) fn window_over(
472    model: &Workbook,
473    sheet: &str,
474    range: Option<&str>,
475    values: Option<&dyn Values>,
476) -> Result<Window, Refusal> {
477    let Some(i) = model.find_sheet(sheet) else {
478        let sheets = model
479            .sheets
480            .iter()
481            .map(|s| s.name.as_str())
482            .collect::<Vec<_>>()
483            .join(", ");
484        return Err(raise(
485            "view-sheet-unknown",
486            &[("name", sheet), ("sheets", &sheets)],
487        ));
488    };
489    let s = &model.sheets[i];
490    let range = match range {
491        Some(text) => parse_a1_range(text)
492            .ok_or_else(|| raise("view-window-not-a-range", &[("text", text)]))?,
493        None => or_a1(extent_in(s, i, values)),
494    };
495    Ok(Window {
496        sheet: s.name.clone(),
497        range,
498    })
499}
500
501/// The view record of one window as text, each line ending in a line feed:
502/// the API's surface, the tests' and a door's that holds the text.
503pub fn view_text(model: &Workbook, window: &Window) -> String {
504    let mut out = String::new();
505    Grid::default().project(model, window, &mut |line| {
506        out.push_str(line);
507        out.push('\n');
508    });
509    out
510}
511
512/// The view record with each formula's computed value after it: the record
513/// a door prints once it has recalculated the model (`frazaro view` through
514/// the core's Excel library; an engine after a step). `values` must be over
515/// `model`.
516pub fn view_text_valued(model: &Workbook, window: &Window, values: &dyn Values) -> String {
517    let mut out = String::new();
518    Grid::valued(values).project(model, window, &mut |line| {
519        out.push_str(line);
520        out.push('\n');
521    });
522    out
523}
524
525/// The values record of one window (KERNEL.37): [`view_text_valued`]'s
526/// record with its `formula` lines left out, what an engine's host draws
527/// every frame. `values` must be over `model`.
528pub fn view_text_values(model: &Workbook, window: &Window, values: &dyn Values) -> String {
529    let mut out = String::new();
530    ValueGrid::valued(values).project(model, window, &mut |line| {
531        out.push_str(line);
532        out.push('\n');
533    });
534    out
535}
536
537/// The changes of one window (KERNEL.42) as text, each line ending in a
538/// line feed: `(changes "<sheet>" "<range>" <since> <at> <n>)`, the window
539/// as its values record names it, then the rows of the `n` cells `cells`
540/// names, row by row, each as the values print it, a cell that prints none
541/// `(empty "<sheet>" "<addr>")`; or, `cells` being `None`, the row ending
542/// `whole` and no other. An engine's machine says which cells changed, and
543/// only it can (`crate::machine::Machine::view`); `values` must be over
544/// `model`, and `cells` inside the window, in row-major order.
545pub(crate) fn changes_text(
546    model: &Workbook,
547    window: &Window,
548    values: &dyn Values,
549    (since, at): (u64, u64),
550    cells: Option<&[(u32, u32)]>,
551) -> String {
552    let Some(i) = model.find_sheet(&window.sheet) else {
553        return String::new();
554    };
555    let sheet = &model.sheets[i];
556    let head = |n: &str| {
557        format!(
558            "(changes {} {} {since} {at} {n})\n",
559            quoted(&sheet.name),
560            quoted(&window.range.text())
561        )
562    };
563    let Some(cells) = cells else {
564        return head("whole");
565    };
566    let mut out = head(&cells.len().to_string());
567    #[cfg(test)]
568    crate::sheet::work::viewed(cells.len());
569    let grid = Grid::valued(values);
570    let lags = Some(values).filter(|v| v.lags(i));
571    let mut masters = None;
572    let mut push = |line: &str| {
573        out.push_str(line);
574        out.push('\n');
575    };
576    for &(row, col) in cells {
577        let printed = sheet.cells.get(&(row, col)).is_some_and(|cell| {
578            grid.cell_rows(
579                sheet,
580                i,
581                (row, col),
582                cell,
583                lags,
584                false,
585                &mut masters,
586                &mut push,
587            )
588        });
589        if !printed {
590            push(&format!(
591                "(empty {} {})",
592                quoted(&sheet.name),
593                quoted(&cell_ref(row, col))
594            ));
595        }
596    }
597    out
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::sheet::{Rgb, Styles};
604
605    fn range(text: &str) -> A1Range {
606        parse_a1_range(text).unwrap()
607    }
608
609    fn lines(text: &str) -> Vec<&str> {
610        text.lines().collect()
611    }
612
613    /// A small model made by hand: two sheets, the first with columns and
614    /// formats, a shared formula, a dynamic one, a truth value, a number no
615    /// cell can hold, and the rows of the sentences that wrote them.
616    fn a_model() -> Workbook {
617        let mut wb = Workbook::new();
618        let mut styles = Styles::new();
619        let lavender = styles.id(Style {
620            fill: Some(Rgb(247, 244, 252)),
621            num_fmt: NumFmt::Text,
622            wrap: false,
623        });
624        let gray = styles.id(Style {
625            fill: Some(Rgb(242, 242, 242)),
626            num_fmt: NumFmt::General,
627            wrap: true,
628        });
629        wb.styles = styles;
630        let mut model = Sheet::new("Model");
631        model.gridlines = false;
632        model.columns = vec![
633            Column {
634                index: 3,
635                width: Some(60.0),
636                hidden: false,
637                style: Some(gray),
638            },
639            Column {
640                index: 1,
641                width: None,
642                hidden: true,
643                style: None,
644            },
645            Column {
646                index: 2,
647                width: Some(72.0),
648                hidden: false,
649                style: Some(lavender),
650            },
651        ];
652        model.set(
653            1,
654            2,
655            Cell {
656                content: Content::Text("Revenue".to_string()),
657                style: lavender,
658            },
659        );
660        model.set(
661            2,
662            2,
663            Cell {
664                content: Content::Number(1200.5),
665                style: 0,
666            },
667        );
668        model.set(
669            3,
670            2,
671            Cell {
672                content: Content::Bool(true),
673                style: 0,
674            },
675        );
676        model.set(
677            4,
678            2,
679            Cell {
680                content: Content::Number(f64::INFINITY),
681                style: 0,
682            },
683        );
684        model.set_formula(range("C2:C4"), "B2*2", 0);
685        model.set_formula_dynamic(range("D2"), "_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")", 0);
686        model.wrote(range("B1"), 1);
687        model.wrote(range("B2:B4"), 2);
688        model.wrote(range("C2:C4"), 3);
689        model.wrote(range("D2"), 4);
690        wb.sheets.push(model);
691        wb.sheets.push(Sheet::new("Empty"));
692        wb
693    }
694
695    #[test]
696    fn the_record_s_rows_have_the_fixed_spelling_in_the_fixed_order() {
697        let wb = a_model();
698        let text = view_text(
699            &wb,
700            &Window {
701                sheet: "Model".to_string(),
702                range: range("A1:F20"),
703            },
704        );
705        assert_eq!(
706            lines(&text),
707            vec![
708                "(sheet \"Model\" visible)",
709                "(sheet \"Empty\" visible)",
710                "(window \"Model\" \"A1:F20\")",
711                "(extent \"Model\" \"B1:D4\")",
712                "(gridlines \"Model\" off)",
713                "(column \"Model\" \"A\" none hidden none)",
714                "(column \"Model\" \"B\" 72 shown 1)",
715                "(column \"Model\" \"C\" 60 shown 2)",
716                "(format 0 none general nowrap)",
717                "(format 1 \"F7F4FC\" text nowrap)",
718                "(format 2 \"F2F2F2\" general wrap)",
719                "(cell \"Model\" \"B1\" \"Revenue\")",
720                "(style \"Model\" \"B1\" 1)",
721                "(sentence \"Model\" \"B1\" 1)",
722                "(cell \"Model\" \"B2\" 1200.5)",
723                "(sentence \"Model\" \"B2\" 2)",
724                "(formula \"Model\" \"C2\" \"=B2*2\")",
725                "(sentence \"Model\" \"C2\" 3)",
726                "(formula \"Model\" \"D2\" \"=IFS(B2>3,\\\"big\\\",TRUE,\\\"small\\\")\")",
727                "(sentence \"Model\" \"D2\" 4)",
728                "(cell \"Model\" \"B3\" true)",
729                "(sentence \"Model\" \"B3\" 2)",
730                "(formula \"Model\" \"C3\" \"=B3*2\")",
731                "(sentence \"Model\" \"C3\" 3)",
732                "(cell \"Model\" \"B4\" (error \"#NUM!\"))",
733                "(sentence \"Model\" \"B4\" 2)",
734                "(formula \"Model\" \"C4\" \"=B4*2\")",
735                "(sentence \"Model\" \"C4\" 3)",
736            ]
737        );
738    }
739
740    #[test]
741    fn the_changes_print_each_cell_s_rows_as_the_values_print_them() {
742        // KERNEL.42: the changes' text of the cells a machine named, each
743        // cell's rows the values' own, printed by the same code: a value
744        // cell's with its style and sentence rows, a formula's value row and
745        // its sentence row; a cell that prints no row, absent or a shared
746        // formula's cell whose master the sheet has lost, spelled empty; and
747        // the row alone, ending whole, where the machine cannot say.
748        let mut wb = a_model();
749        let library = crate::calc::library::language();
750        let calc = crate::calc::Calc::run(&wb, &library);
751        let w = Window {
752            sheet: "Model".to_string(),
753            range: range("A1:F20"),
754        };
755        let values = view_text_values(&wb, &w, &calc);
756        let rows = |addr: &str| -> String {
757            let at = format!(" \"{addr}\" ");
758            values
759                .lines()
760                .filter(|l| l.contains(&at))
761                .map(|l| format!("{l}\n"))
762                .collect()
763        };
764        let text = changes_text(&wb, &w, &calc, (6, 7), Some(&[(1, 2), (2, 3), (9, 9)]));
765        assert_eq!(
766            text,
767            format!(
768                "(changes \"Model\" \"A1:F20\" 6 7 3)\n{}{}(empty \"Model\" \"I9\")\n",
769                rows("B1"),
770                rows("C2")
771            )
772        );
773        assert_eq!(rows("B1").lines().count(), 3, "{values}");
774        assert_eq!(
775            changes_text(&wb, &w, &calc, (6, 7), None),
776            "(changes \"Model\" \"A1:F20\" 6 7 whole)\n"
777        );
778        wb.sheets[0].cells.remove(&(2, 3));
779        let lost = crate::calc::Calc::run(&wb, &library);
780        assert_eq!(
781            changes_text(&wb, &w, &lost, (0, 1), Some(&[(3, 3)])),
782            "(changes \"Model\" \"A1:F20\" 0 1 1)\n(empty \"Model\" \"C3\")\n",
783            "a fill's cell whose master is lost"
784        );
785    }
786
787    #[test]
788    fn a_window_clips_to_its_rectangle_and_an_empty_one_prints_its_header_alone() {
789        let wb = a_model();
790        // The clip: the shared formula's children keep their moved text, and
791        // only column C's settings are in the window.
792        let text = view_text(
793            &wb,
794            &Window {
795                sheet: "Model".to_string(),
796                range: range("C3:D4"),
797            },
798        );
799        assert_eq!(
800            lines(&text),
801            vec![
802                "(sheet \"Model\" visible)",
803                "(sheet \"Empty\" visible)",
804                "(window \"Model\" \"C3:D4\")",
805                "(extent \"Model\" \"B1:D4\")",
806                "(gridlines \"Model\" off)",
807                "(column \"Model\" \"C\" 60 shown 2)",
808                "(format 0 none general nowrap)",
809                "(format 2 \"F2F2F2\" general wrap)",
810                "(formula \"Model\" \"C3\" \"=B3*2\")",
811                "(sentence \"Model\" \"C3\" 3)",
812                "(formula \"Model\" \"C4\" \"=B4*2\")",
813                "(sentence \"Model\" \"C4\" 3)",
814            ]
815        );
816        // Past the extent: the header says where the cells are, and no cell row follows.
817        let text = view_text(
818            &wb,
819            &Window {
820                sheet: "Model".to_string(),
821                range: range("H10:J12"),
822            },
823        );
824        assert_eq!(
825            lines(&text)[2..],
826            [
827                "(window \"Model\" \"H10:J12\")",
828                "(extent \"Model\" \"B1:D4\")",
829                "(gridlines \"Model\" off)",
830                "(format 0 none general nowrap)",
831            ]
832        );
833        // An empty sheet: no extent, gridlines on, the default format.
834        let text = view_text(
835            &wb,
836            &Window {
837                sheet: "Empty".to_string(),
838                range: whole(&wb.sheets[1]),
839            },
840        );
841        assert_eq!(
842            lines(&text)[2..],
843            [
844                "(window \"Empty\" \"A1\")",
845                "(extent \"Empty\" none)",
846                "(gridlines \"Empty\" on)",
847                "(format 0 none general nowrap)",
848            ]
849        );
850        // A window onto no sheet, which window_of never makes: the sheets,
851        // the window, and nothing else.
852        let text = view_text(
853            &wb,
854            &Window {
855                sheet: "Nowhere".to_string(),
856                range: range("A1"),
857            },
858        );
859        assert_eq!(lines(&text).len(), 3);
860        assert_eq!(lines(&text)[2], "(window \"Nowhere\" \"A1\")");
861    }
862
863    #[test]
864    fn row_settings_looks_and_hidden_sheets_print_in_their_places() {
865        // KERNEL.22: the `row` rows after the `column` rows, the `look` rows
866        // after the `format` rows with their formats among them, and each
867        // sheet's own visibility.
868        let mut wb = a_model();
869        wb.sheets[1].visibility = crate::rows::Visibility::Hidden;
870        let model = &mut wb.sheets[0];
871        model.rows.insert(
872            3,
873            crate::sheet::RowSettings {
874                height: Some(30.0),
875                hidden: false,
876            },
877        );
878        model.rows.insert(
879            9,
880            crate::sheet::RowSettings {
881                height: None,
882                hidden: true,
883            },
884        );
885        model.looks.push(("1".to_string(), 2));
886        let text = view_text(
887            &wb,
888            &Window {
889                sheet: "Model".to_string(),
890                range: range("B2:B4"),
891            },
892        );
893        assert_eq!(
894            lines(&text)[..10],
895            [
896                "(sheet \"Model\" visible)",
897                "(sheet \"Empty\" hidden)",
898                "(window \"Model\" \"B2:B4\")",
899                "(extent \"Model\" \"B1:D4\")",
900                "(gridlines \"Model\" off)",
901                "(column \"Model\" \"B\" 72 shown 1)",
902                "(row \"Model\" 3 30 shown)",
903                "(format 0 none general nowrap)",
904                "(format 1 \"F7F4FC\" text nowrap)",
905                "(format 2 \"F2F2F2\" general wrap)",
906            ]
907        );
908        assert_eq!(lines(&text)[10], "(look \"Model\" 1 2)");
909        assert!(
910            !text.contains("(row \"Model\" 9"),
911            "row 9 is outside the window"
912        );
913    }
914
915    #[test]
916    fn window_of_finds_the_sheet_without_case_and_refuses_the_rest_by_name() {
917        let wb = a_model();
918        let w = window_of(&wb, "MODEL", None).unwrap();
919        assert_eq!(w.sheet, "Model");
920        assert_eq!(w.range, range("B1:D4"));
921        let w = window_of(&wb, "empty", None).unwrap();
922        assert_eq!((w.sheet.as_str(), w.range.text().as_str()), ("Empty", "A1"));
923        let w = window_of(&wb, "Model", Some("f20:a1")).unwrap();
924        assert_eq!(w.range.text(), "A1:F20");
925        let r = window_of(&wb, "Summary", None).unwrap_err();
926        assert_eq!(
927            (r.id.as_str(), r.source.as_str()),
928            ("view-sheet-unknown", "VLA-View")
929        );
930        assert!(r.text.contains("Summary"), "{}", r.text);
931        assert!(r.text.contains("Model, Empty"), "{}", r.text);
932        for text in ["A:A", "3:3", "nonsense", "", "A1:B"] {
933            let r = window_of(&wb, "Model", Some(text)).unwrap_err();
934            assert_eq!(r.id, "view-window-not-a-range", "{text}");
935            assert!(r.text.contains(text), "{}", r.text);
936        }
937        assert_eq!(Grid::default().name(), "grid");
938    }
939
940    /// A record less its `formula` lines: what the values must be (KERNEL.37).
941    fn less_formulas(record: &str) -> String {
942        record
943            .lines()
944            .filter(|l| !l.starts_with("(formula "))
945            .map(|l| format!("{l}\n"))
946            .collect()
947    }
948
949    /// The values record of a window, over the values given or none.
950    fn values_text(wb: &Workbook, window: &Window, values: Option<&dyn Values>) -> String {
951        let mut out = String::new();
952        ValueGrid { values }.project(wb, window, &mut |line| {
953            out.push_str(line);
954            out.push('\n');
955        });
956        out
957    }
958
959    /// What `f` walked, counted (`sheet::work`).
960    fn walks(f: impl FnOnce()) -> crate::sheet::work::Work {
961        crate::sheet::work::begin();
962        f();
963        crate::sheet::work::end()
964    }
965
966    #[test]
967    fn the_values_are_the_grid_less_its_formula_rows() {
968        // KERNEL.37: every window of the model, computed and not, its values
969        // its grid record with the formula lines left out, a formula's style
970        // and sentence rows kept whether it has a value or not.
971        let mut wb = a_model();
972        wb.sheets[0].cells.get_mut(&(3, 3)).unwrap().style = 2;
973        let library = crate::calc::library::language();
974        let calc = crate::calc::Calc::run(&wb, &library);
975        for (sheet, window) in [
976            ("Model", "A1:F20"),
977            ("Model", "C3:D4"),
978            ("Model", "B1:B4"),
979            ("Model", "H10:J12"),
980            ("Empty", "A1"),
981        ] {
982            let w = Window {
983                sheet: sheet.to_string(),
984                range: range(window),
985            };
986            let bare = values_text(&wb, &w, None);
987            assert_eq!(bare, less_formulas(&view_text(&wb, &w)), "{window}");
988            let valued = view_text_values(&wb, &w, &calc);
989            assert_eq!(
990                valued,
991                less_formulas(&view_text_valued(&wb, &w, &calc)),
992                "{window}"
993            );
994            assert!(!valued.contains("(formula "), "{valued}");
995        }
996        let w = Window {
997            sheet: "Model".to_string(),
998            range: range("A1:F20"),
999        };
1000        let bare = values_text(&wb, &w, None);
1001        assert!(bare.contains("(style \"Model\" \"C3\" 2)\n(sentence \"Model\" \"C3\" 3)\n"));
1002        assert!(!bare.contains("(value "), "{bare}");
1003        let valued = view_text_values(&wb, &w, &calc);
1004        assert!(
1005            valued.contains("(value \"Model\" \"C2\" 2401)\n(sentence \"Model\" \"C2\" 3)\n"),
1006            "{valued}"
1007        );
1008        // A window of values alone is the grid's own record.
1009        let w = Window {
1010            sheet: "Model".to_string(),
1011            range: range("B1:B4"),
1012        };
1013        assert_eq!(values_text(&wb, &w, None), view_text(&wb, &w));
1014        assert_eq!(ValueGrid::default().name(), "values");
1015    }
1016
1017    #[test]
1018    fn a_fill_s_cell_whose_master_is_lost_shows_in_neither_record() {
1019        // KERNEL.37: a value over a fill's first cell, as the core's build
1020        // writes one, leaves the fill's other cells children of a master the
1021        // sheet no longer holds. Neither record shows them, not even their
1022        // style and sentence rows, and with no value to vouch for them the
1023        // values find that out by one walk of the sheet.
1024        let mut wb = Workbook::new();
1025        let mut sheet = Sheet::new("S");
1026        sheet.set(
1027            2,
1028            2,
1029            Cell {
1030                content: Content::Number(5.0),
1031                style: 0,
1032            },
1033        );
1034        sheet.set_formula(range("C2:C4"), "B2+1", 1);
1035        sheet.wrote(range("C2:C4"), 2);
1036        sheet.set(
1037            2,
1038            3,
1039            Cell {
1040                content: Content::Number(9.0),
1041                style: 0,
1042            },
1043        );
1044        sheet.wrote(range("C2"), 3);
1045        wb.sheets.push(sheet);
1046        let w = Window {
1047            sheet: "S".to_string(),
1048            range: range("A1:D5"),
1049        };
1050        let library = crate::calc::library::language();
1051        let calc = crate::calc::Calc::run(&wb, &library);
1052        let grid = view_text(&wb, &w);
1053        let valued = view_text_valued(&wb, &w, &calc);
1054        let mut bare = String::new();
1055        let work = walks(|| bare = values_text(&wb, &w, None));
1056        assert_eq!(bare, less_formulas(&grid));
1057        assert_eq!(work.walked, 4, "the sheet's four cells walked once");
1058        let mut values = String::new();
1059        let work = walks(|| values = view_text_values(&wb, &w, &calc));
1060        assert_eq!(values, less_formulas(&valued));
1061        assert_eq!(work.walked, 4, "no value vouches for C3, so one walk");
1062        assert_eq!(work.extents, 0, "the extent the calculation keeps");
1063        for record in [&grid, &valued, &bare, &values] {
1064            assert!(
1065                !record.contains("\"C3\"") && !record.contains("\"C4\""),
1066                "{record}"
1067            );
1068            assert!(record.contains("(cell \"S\" \"C2\" 9)\n(sentence \"S\" \"C2\" 3)\n"));
1069        }
1070    }
1071
1072    #[test]
1073    fn the_values_of_a_computed_fill_walk_nothing() {
1074        // KERNEL.37: where every cell of a fill has a value, the values walk
1075        // the sheet neither for its masters nor for its extent; the grid
1076        // walks it once for its masters, to move their texts; with no
1077        // values, both walk it for the extent.
1078        let mut wb = Workbook::new();
1079        let mut sheet = Sheet::new("S");
1080        for row in 1..=4 {
1081            sheet.set(
1082                row,
1083                1,
1084                Cell {
1085                    content: Content::Number(f64::from(row)),
1086                    style: 0,
1087                },
1088            );
1089        }
1090        sheet.set_formula(range("B1:C4"), "A1*2", 0);
1091        wb.sheets.push(sheet);
1092        let w = Window {
1093            sheet: "S".to_string(),
1094            range: range("B2:C3"),
1095        };
1096        let library = crate::calc::library::language();
1097        let calc = crate::calc::Calc::run(&wb, &library);
1098        let work = walks(|| {
1099            view_text_values(&wb, &w, &calc);
1100        });
1101        assert_eq!((work.walked, work.extents), (0, 0));
1102        let work = walks(|| {
1103            view_text_valued(&wb, &w, &calc);
1104        });
1105        assert_eq!((work.walked, work.extents), (12, 0));
1106        let work = walks(|| {
1107            values_text(&wb, &w, None);
1108        });
1109        assert_eq!((work.walked, work.extents), (12, 12));
1110        let work = walks(|| {
1111            view_text(&wb, &w);
1112        });
1113        assert_eq!((work.walked, work.extents), (12, 12));
1114    }
1115}