Skip to main content

vla_lang/
rows.rs

1//! The relation rows and their spelling (PORT.8, slice 8a; cut out of
2//! `frazaro-core`'s `reflect` with the language, PORT.12): one row a line,
3//! in the proof corpus's notation, so that an expected-relations file is a
4//! golden the core writes, the owner checks in Excel by eye, and
5//! `VlaReadForms` reads later for the VBA's own `REFLECT` test without a new
6//! parser. The reader of a workbook's file prints these rows from the file;
7//! the view record ([`crate::view`]) prints the `sheet`, `cell` and
8//! `formula` rows from the grid; an engine's loader reads them back as the
9//! inverse.
10//!
11//! The spelling, fixed by the treaty's amendment for oracle 8: a text is a
12//! VLA string as `WriteDatum` writes one; a number is the file's own text,
13//! never a float round trip; a truth value is `true` or `false`; an error is
14//! `(error "#DIV/0!")`; an ISO date cell is `(date "2026-10-03")`; a sheet's
15//! state is `visible`, `hidden` or `very-hidden`.
16
17use crate::form::Form;
18use crate::printer::write_datum;
19use crate::refers::{self, FormulaRef, Kind};
20
21/// A cell's value as the file holds it.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum Value {
24    /// The file's own text of a number, never a float round trip.
25    Number(String),
26    Text(String),
27    Bool(bool),
28    /// An error value's text, `#DIV/0!`.
29    Error(String),
30    /// An ISO 8601 date cell (`t="d"`; an OpenDocument date or time value),
31    /// as written.
32    Date(String),
33}
34
35/// A sheet's state, as the workbook part has it.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Visibility {
38    Visible,
39    Hidden,
40    /// `veryHidden`: not listed by Excel's Unhide dialog.
41    VeryHidden,
42}
43
44impl Visibility {
45    /// The word a `sheet` row prints.
46    pub fn word(self) -> &'static str {
47        match self {
48            Visibility::Visible => "visible",
49            Visibility::Hidden => "hidden",
50            Visibility::VeryHidden => "very-hidden",
51        }
52    }
53}
54
55/// The second field of a `refers` row: a reference in the treaty's
56/// spelling, or an unreadable call named.
57#[derive(Clone, Debug, PartialEq, Eq)]
58pub enum RefersTo {
59    /// `Model!B1`, `Data!A:A`, `Rate`, `Sales[Amount]`, `[Rates.xlsx]Sheet1!A1`.
60    Reference(String),
61    /// `(unreadable "INDIRECT")`, `(unreadable "OFFSET")`.
62    Unreadable(&'static str),
63}
64
65impl RefersTo {
66    /// What one record of a formula's scan refers to, from the sheet that
67    /// holds the formula.
68    pub fn of(r: &FormulaRef, home_sheet: &str) -> RefersTo {
69        match r.kind {
70            Kind::Unreadable => RefersTo::Unreadable(r.unreadable_name()),
71            _ => RefersTo::Reference(refers::spell(r, home_sheet)),
72        }
73    }
74}
75
76/// One row of a relation, borrowed from the walk that found it.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum Row<'r> {
79    Sheet {
80        name: &'r str,
81        visibility: Visibility,
82    },
83    Name {
84        name: &'r str,
85        refers_to: &'r str,
86    },
87    Table {
88        name: &'r str,
89        sheet: &'r str,
90        range: &'r str,
91    },
92    Cell {
93        sheet: &'r str,
94        addr: &'r str,
95        value: &'r Value,
96    },
97    Formula {
98        sheet: &'r str,
99        addr: &'r str,
100        text: &'r str,
101    },
102    /// `(refers "Model!B3" "Model!B1")`: the cell holding the formula, and
103    /// one thing it refers to.
104    Refers {
105        sheet: &'r str,
106        addr: &'r str,
107        to: &'r RefersTo,
108    },
109}
110
111/// Where rows go as a walk emits them.
112pub trait Sink {
113    fn row(&mut self, row: &Row<'_>);
114}
115
116/// A sheet's name as a reference spells it in front of `!`: the one quoting
117/// rule, `refers::quote_sheet` (the port of `RefersQuoteSheet`), so that a
118/// `name` row's sheet, a `refers` row's two ends and the formula text agree.
119pub fn sheet_prefix(name: &str) -> String {
120    refers::quote_sheet(name)
121}
122
123/// A text as a VLA string literal, as `WriteDatum` writes one.
124pub fn quoted(s: &str) -> String {
125    write_datum(&Form::string(s))
126}
127
128/// A `refers` row's second field as printed: a quoted reference, or the
129/// `(unreadable "...")` form. The rows of one cell sort by this text.
130pub fn target(to: &RefersTo) -> String {
131    match to {
132        RefersTo::Reference(text) => quoted(text),
133        RefersTo::Unreadable(name) => format!("(unreadable {})", quoted(name)),
134    }
135}
136
137/// A value as a datum.
138pub fn datum(v: &Value) -> String {
139    match v {
140        Value::Number(text) => text.clone(),
141        Value::Text(text) => quoted(text),
142        Value::Bool(true) => "true".to_string(),
143        Value::Bool(false) => "false".to_string(),
144        Value::Error(text) => format!("(error {})", quoted(text)),
145        Value::Date(text) => format!("(date {})", quoted(text)),
146    }
147}
148
149/// One row as its line, without the line break.
150pub fn line(row: &Row<'_>) -> String {
151    match row {
152        Row::Sheet { name, visibility } => {
153            format!("(sheet {} {})", quoted(name), visibility.word())
154        }
155        Row::Name { name, refers_to } => {
156            format!("(name {} {})", quoted(name), quoted(refers_to))
157        }
158        Row::Table { name, sheet, range } => {
159            format!(
160                "(table {} {} {})",
161                quoted(name),
162                quoted(sheet),
163                quoted(range)
164            )
165        }
166        Row::Cell { sheet, addr, value } => {
167            format!("(cell {} {} {})", quoted(sheet), quoted(addr), datum(value))
168        }
169        Row::Formula { sheet, addr, text } => {
170            format!(
171                "(formula {} {} {})",
172                quoted(sheet),
173                quoted(addr),
174                quoted(text)
175            )
176        }
177        Row::Refers { sheet, addr, to } => {
178            format!(
179                "(refers {} {})",
180                quoted(&format!("{}!{}", sheet_prefix(sheet), addr)),
181                target(to)
182            )
183        }
184    }
185}
186
187/// A sink that holds the lines as one text, each ending in a line feed.
188#[derive(Default)]
189pub struct Printer {
190    pub out: String,
191}
192
193impl Sink for Printer {
194    fn row(&mut self, row: &Row<'_>) {
195        self.out.push_str(&line(row));
196        self.out.push('\n');
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn each_row_prints_in_the_corpus_notation() {
206        assert_eq!(
207            line(&Row::Sheet {
208                name: "Q1 Data",
209                visibility: Visibility::VeryHidden
210            }),
211            "(sheet \"Q1 Data\" very-hidden)"
212        );
213        assert_eq!(
214            line(&Row::Name {
215                name: "Rate",
216                refers_to: "Model!$B$1"
217            }),
218            "(name \"Rate\" \"Model!$B$1\")"
219        );
220        assert_eq!(
221            line(&Row::Table {
222                name: "Sales",
223                sheet: "Data",
224                range: "A1:B4"
225            }),
226            "(table \"Sales\" \"Data\" \"A1:B4\")"
227        );
228        assert_eq!(
229            line(&Row::Cell {
230                sheet: "Model",
231                addr: "B1",
232                value: &Value::Number("1200".to_string())
233            }),
234            "(cell \"Model\" \"B1\" 1200)"
235        );
236        assert_eq!(
237            line(&Row::Cell {
238                sheet: "Model",
239                addr: "A1",
240                value: &Value::Text("say \"hi\"".to_string())
241            }),
242            "(cell \"Model\" \"A1\" \"say \\\"hi\\\"\")"
243        );
244        assert_eq!(datum(&Value::Bool(true)), "true");
245        assert_eq!(datum(&Value::Bool(false)), "false");
246        assert_eq!(
247            datum(&Value::Error("#DIV/0!".to_string())),
248            "(error \"#DIV/0!\")"
249        );
250        assert_eq!(
251            datum(&Value::Date("2026-10-03".to_string())),
252            "(date \"2026-10-03\")"
253        );
254        assert_eq!(
255            line(&Row::Formula {
256                sheet: "Model",
257                addr: "B3",
258                text: "=B1-B2"
259            }),
260            "(formula \"Model\" \"B3\" \"=B1-B2\")"
261        );
262        assert_eq!(
263            line(&Row::Refers {
264                sheet: "Q1 Data",
265                addr: "B3",
266                to: &RefersTo::Reference("Model!B1".to_string())
267            }),
268            "(refers \"'Q1 Data'!B3\" \"Model!B1\")"
269        );
270        assert_eq!(
271            line(&Row::Refers {
272                sheet: "Model",
273                addr: "D2",
274                to: &RefersTo::Unreadable("INDIRECT")
275            }),
276            "(refers \"Model!D2\" (unreadable \"INDIRECT\"))"
277        );
278        // A quoted reference sorts before the unreadable form: '"' < '('.
279        assert!(
280            target(&RefersTo::Reference("Model!B1".to_string()))
281                < target(&RefersTo::Unreadable("OFFSET"))
282        );
283        let mut p = Printer::default();
284        p.row(&Row::Sheet {
285            name: "A",
286            visibility: Visibility::Visible,
287        });
288        p.row(&Row::Sheet {
289            name: "B",
290            visibility: Visibility::Hidden,
291        });
292        assert_eq!(p.out, "(sheet \"A\" visible)\n(sheet \"B\" hidden)\n");
293    }
294
295    #[test]
296    fn a_sheet_name_is_quoted_as_a_reference_quotes_it() {
297        assert_eq!(sheet_prefix("Model"), "Model");
298        assert_eq!(sheet_prefix("Sheet.1"), "Sheet.1");
299        assert_eq!(sheet_prefix("Q1 Data"), "'Q1 Data'");
300        assert_eq!(sheet_prefix("It's"), "'It''s'");
301        assert_eq!(sheet_prefix("2026"), "'2026'");
302        assert_eq!(sheet_prefix("Donn\u{e9}es"), "Donn\u{e9}es");
303        assert_eq!(sheet_prefix("a-b"), "'a-b'");
304        // AXM.7's two clauses, confirmed in Excel on 2026-10-04.
305        assert_eq!(sheet_prefix("A1"), "'A1'");
306        assert_eq!(sheet_prefix("R1C1"), "'R1C1'");
307    }
308
309    #[test]
310    fn a_target_is_a_reference_or_an_unreadable_call() {
311        let refs = refers::scan("=OFFSET(B1,1,0)+'Q1 Data'!A1");
312        assert_eq!(
313            RefersTo::of(&refs[0], "Model"),
314            RefersTo::Unreadable("OFFSET")
315        );
316        assert_eq!(
317            RefersTo::of(&refs[1], "Model"),
318            RefersTo::Reference("Model!B1".to_string())
319        );
320        assert_eq!(
321            RefersTo::of(&refs[2], "Model"),
322            RefersTo::Reference("'Q1 Data'!A1".to_string())
323        );
324    }
325}