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