Skip to main content

vla_lang/
sheet.rs

1//! The sheet model (PORT.7): what a workbook holds before it is written.
2//! The language's since PORT.12 (2026-10-08): the grid with its A1
3//! coordinates, its style table and the references a formula's text holds
4//! (`crate::refers`) are what an engine stands on; the file formats that
5//! write and read it stay in `frazaro-core`'s `sheet` module, which
6//! re-exports this one.
7//! Sheets of cells, each cell a text, a number, a truth value, an error
8//! value or a formula's text; column widths, a hidden column, a style per
9//! cell or per column from a small style table; and nothing here that
10//! computes: a formula is text the host evaluates when it opens the file,
11//! which `fullCalcOnLoad` (ooxml.rs) makes it do, and since KERNEL.7
12//! (2026-10-08) text `crate::calc` evaluates over this model in memory,
13//! behind a declared subset that refuses by name, as `web/CALLOSUM.md`
14//! section 7 decision 3 amended docs/HORIZON.md section 12.
15//!
16//! New ground, said plainly: the reference has no model of a sheet; it
17//! writes through Excel's object model. What is modelled is the little the
18//! room needs (`BuildWorkspace`, VLA_IDE.bas) and what slice 7b's static
19//! subset writes, and the model grows only as a slice needs it.
20
21use std::collections::{BTreeMap, HashMap};
22
23use crate::numfmt::Code;
24use crate::rows::Visibility;
25
26/// A colour, as `Interior.Color = RGB(r, g, b)` names it.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Rgb(pub u8, pub u8, pub u8);
29
30impl Rgb {
31    /// The file's spelling: opaque alpha, then the three bytes in hex.
32    pub fn argb_hex(self) -> String {
33        format!("FF{:02X}{:02X}{:02X}", self.0, self.1, self.2)
34    }
35}
36
37/// The number formats a style names: Excel's built-in General and Text
38/// (`NumberFormat = "@"`, so a sentence is never read as a formula), and
39/// since KERNEL.47 (2026-10-17) a code of Excel's, such as `0.00`
40/// (`crate::numfmt`).
41#[derive(Clone, Debug, Default, PartialEq, Eq)]
42pub enum NumFmt {
43    #[default]
44    General,
45    Text,
46    Code(Code),
47}
48
49impl NumFmt {
50    /// The id of the built-in format in a styles part, where Excel has one:
51    /// General's 0, Text's 49, and a code's own where it is a built-in that
52    /// Excel draws alike in every locale (`Code::builtin`); `None` for a
53    /// code a workbook must define.
54    pub fn builtin(&self) -> Option<u32> {
55        match self {
56            NumFmt::General => Some(0),
57            NumFmt::Text => Some(49),
58            NumFmt::Code(c) => c.builtin(),
59        }
60    }
61}
62
63/// One cell format: a solid fill or none, a number format, wrapped text or
64/// not. Index 0 of a [`Styles`] table is this type's default.
65#[derive(Clone, Debug, Default, PartialEq, Eq)]
66pub struct Style {
67    pub fill: Option<Rgb>,
68    pub num_fmt: NumFmt,
69    pub wrap: bool,
70}
71
72/// The workbook's cell formats, each once; a cell or a column names one by
73/// its index.
74#[derive(Clone, Debug)]
75pub struct Styles {
76    xfs: Vec<Style>,
77}
78
79impl Default for Styles {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Styles {
86    /// A table holding only the default format, at index 0.
87    pub fn new() -> Styles {
88        Styles {
89            xfs: vec![Style::default()],
90        }
91    }
92
93    /// The index of a format, added if it is new.
94    pub fn id(&mut self, style: Style) -> u32 {
95        if let Some(i) = self.xfs.iter().position(|s| *s == style) {
96            return i as u32;
97        }
98        self.xfs.push(style);
99        (self.xfs.len() - 1) as u32
100    }
101
102    /// A format put at the index a record names (KERNEL.22, the loader:
103    /// a `format` row says its own index), the table grown with default
104    /// formats up to it.
105    pub fn put(&mut self, index: u32, style: Style) {
106        let i = index as usize;
107        if self.xfs.len() <= i {
108            self.xfs.resize(i + 1, Style::default());
109        }
110        self.xfs[i] = style;
111    }
112
113    /// The format at an index, or `None` past the table.
114    pub fn get(&self, index: u32) -> Option<&Style> {
115        self.xfs.get(index as usize)
116    }
117
118    /// Every format, in index order.
119    pub fn xfs(&self) -> &[Style] {
120        &self.xfs
121    }
122
123    /// The distinct fills, in the order they were first used.
124    pub fn fills(&self) -> Vec<Rgb> {
125        let mut fills: Vec<Rgb> = Vec::new();
126        for s in &self.xfs {
127            if let Some(f) = s.fill {
128                if !fills.contains(&f) {
129                    fills.push(f);
130                }
131            }
132        }
133        fills
134    }
135
136    /// Whether any format holds a code (KERNEL.47): a record over a table
137    /// that holds none prints no `text` row and finds no cell's look.
138    pub fn has_code(&self) -> bool {
139        self.xfs
140            .iter()
141            .any(|s| matches!(s.num_fmt, NumFmt::Code(_)))
142    }
143}
144
145/// What a cell holds. A formula is its text without the leading `=`, as the
146/// file stores it; the host computes it. A formula written into a range of
147/// cells is a shared formula, which is what Excel itself writes when a
148/// formula is filled: the top-left cell is the master and holds the text
149/// and the range, the others point at it by its index, and the host
150/// adjusts the references for each cell as `Range.Formula2` would have.
151#[derive(Clone, Debug, PartialEq)]
152pub enum Content {
153    Text(String),
154    Number(f64),
155    Bool(bool),
156    /// An error value typed into a cell with no formula behind it (`t="e"`
157    /// in the file), by its text, `#N/A`; what a reader meets in a model
158    /// (KERNEL.7). The writer writes it as the file holds it; no sentence
159    /// puts one there.
160    Error(String),
161    Formula(String),
162    SharedMaster {
163        text: String,
164        range: String,
165        si: u32,
166    },
167    SharedChild {
168        si: u32,
169    },
170    /// A dynamic-array formula (`cm="1"`, `t="array"` over its own cell):
171    /// what Excel stores for a `Formula2` entry of a formula whose function
172    /// can return an array, and reads back without the `@` it puts on a
173    /// legacy formula of that kind.
174    DynamicFormula(String),
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub struct Cell {
179    pub content: Content,
180    pub style: u32,
181}
182
183/// A column's settings: `index` is 1-based; `width` is in characters, as
184/// `ColumnWidth` counts them; `style` is the format of its cells that have
185/// none of their own.
186#[derive(Clone, Debug, PartialEq)]
187pub struct Column {
188    pub index: u32,
189    pub width: Option<f64>,
190    pub hidden: bool,
191    pub style: Option<u32>,
192}
193
194/// A rectangle of cells, rows and columns 1-based and inclusive.
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct A1Range {
197    pub top: u32,
198    pub left: u32,
199    pub bottom: u32,
200    pub right: u32,
201}
202
203impl A1Range {
204    pub fn cells(&self) -> u64 {
205        u64::from(self.bottom - self.top + 1) * u64::from(self.right - self.left + 1)
206    }
207
208    /// `B2` for one cell, `B2:C4` for more.
209    pub fn text(&self) -> String {
210        if self.top == self.bottom && self.left == self.right {
211            cell_ref(self.top, self.left)
212        } else {
213            format!(
214                "{}:{}",
215                cell_ref(self.top, self.left),
216                cell_ref(self.bottom, self.right)
217            )
218        }
219    }
220}
221
222/// Excel's last column (XFD) and last row.
223pub const MAX_COLUMN: u32 = 16_384;
224pub const MAX_ROW: u32 = 1_048_576;
225
226fn parse_a1_cell(s: &str) -> Option<(u32, u32)> {
227    let s = s.replace('$', "");
228    let letters: String = s.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
229    let digits = &s[letters.len()..];
230    if letters.is_empty() || letters.len() > 3 || digits.is_empty() {
231        return None;
232    }
233    if !digits.chars().all(|c| c.is_ascii_digit()) {
234        return None;
235    }
236    let mut col: u32 = 0;
237    for c in letters.chars() {
238        col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
239    }
240    let row: u32 = digits.parse().ok()?;
241    if col == 0 || col > MAX_COLUMN || row == 0 || row > MAX_ROW {
242        return None;
243    }
244    Some((row, col))
245}
246
247/// An A1 reference as a program writes it (`b2`, `A1:B5`, `$C$2`), in any
248/// case, with the corners in any order; `None` for anything else, a whole
249/// column or row included.
250pub fn parse_a1_range(s: &str) -> Option<A1Range> {
251    let s = s.trim();
252    let (a, b) = match s.split_once(':') {
253        Some((a, b)) => (a, b),
254        None => (s, s),
255    };
256    let (r1, c1) = parse_a1_cell(a)?;
257    let (r2, c2) = parse_a1_cell(b)?;
258    Some(A1Range {
259        top: r1.min(r2),
260        left: c1.min(c2),
261        bottom: r1.max(r2),
262        right: c1.max(c2),
263    })
264}
265
266/// A row's settings, the twin of a [`Column`]'s (KERNEL.22; `Alonzo/SPEC.md`
267/// decision 19): its height in points, or `None` for the default, and
268/// whether it is hidden.
269#[derive(Clone, Debug, PartialEq)]
270pub struct RowSettings {
271    pub height: Option<f64>,
272    pub hidden: bool,
273}
274
275/// One sheet: its cells by (row, column), both 1-based.
276#[derive(Clone, Debug, PartialEq)]
277pub struct Sheet {
278    pub name: String,
279    pub cells: BTreeMap<(u32, u32), Cell>,
280    pub columns: Vec<Column>,
281    /// The rows with settings of their own, by number (KERNEL.22), printed
282    /// as the view record's `row` rows; empty for a sheet the writer built.
283    pub rows: BTreeMap<u32, RowSettings>,
284    /// The look map (KERNEL.22; `Alonzo/SPEC.md` decision 19): a value as a
285    /// `cell` row spells it, and the format a cell holding that value draws
286    /// with in place of its own, in the order declared; printed as the view
287    /// record's `look` rows.
288    pub looks: Vec<(String, u32)>,
289    /// Whether the sheet has a tab (KERNEL.22): the reader's three words.
290    /// Every sheet the writer builds is visible; an engine's twins are
291    /// hidden.
292    pub visibility: Visibility,
293    pub gridlines: bool,
294    /// The selected cell when the sheet opens, (row, column).
295    pub active_cell: (u32, u32),
296    /// How many shared formulas the sheet holds; the next one's index.
297    pub shared_formulas: u32,
298    /// The row of the sentence that wrote each cell, by (row, column): the
299    /// program line, which is the row of the `Frazaro` sheet that holds the
300    /// sentence (KERNEL.4, the view record). Filled by the build's walker;
301    /// empty for a sheet nothing wrote by sentence.
302    pub sentences: BTreeMap<(u32, u32), u32>,
303}
304
305impl Sheet {
306    pub fn new(name: &str) -> Sheet {
307        Sheet {
308            name: name.to_string(),
309            cells: BTreeMap::new(),
310            columns: Vec::new(),
311            rows: BTreeMap::new(),
312            looks: Vec::new(),
313            visibility: Visibility::Visible,
314            gridlines: true,
315            active_cell: (1, 1),
316            shared_formulas: 0,
317            sentences: BTreeMap::new(),
318        }
319    }
320
321    /// Record that the sentence on program row `row_of_sentence` wrote every
322    /// cell of `range`, after the cells are set.
323    pub fn wrote(&mut self, range: A1Range, row_of_sentence: u32) {
324        if row_of_sentence == 0 {
325            return;
326        }
327        for row in range.top..=range.bottom {
328            for col in range.left..=range.right {
329                self.sentences.insert((row, col), row_of_sentence);
330            }
331        }
332    }
333
334    pub fn set(&mut self, row: u32, col: u32, cell: Cell) {
335        self.cells.insert((row, col), cell);
336    }
337
338    /// A formula's text (without its `=`) into every cell of a range: one
339    /// plain formula for one cell, a shared formula for more.
340    /// A formula whose function can return an array, into every cell of a
341    /// range as Excel stores a `Formula2` entry of it: a dynamic-array
342    /// formula per cell (a dynamic-array formula cannot be shared), its
343    /// references moved by the cell's distance from the top-left one, as
344    /// Excel moves them when a formula is filled (the mover is
345    /// `crate::refers::shift_a1_references` since PORT.8, slice 8b).
346    pub fn set_formula_dynamic(&mut self, range: A1Range, text: &str, style: u32) {
347        for row in range.top..=range.bottom {
348            for col in range.left..=range.right {
349                let moved = crate::refers::shift_a1_references(
350                    text,
351                    i64::from(row) - i64::from(range.top),
352                    i64::from(col) - i64::from(range.left),
353                );
354                self.set(
355                    row,
356                    col,
357                    Cell {
358                        content: Content::DynamicFormula(moved),
359                        style,
360                    },
361                );
362            }
363        }
364    }
365
366    pub fn set_formula(&mut self, range: A1Range, text: &str, style: u32) {
367        let si = self.next_shared(range);
368        for row in range.top..=range.bottom {
369            for col in range.left..=range.right {
370                let content = filled(range, text, si, row, col);
371                self.set(row, col, Cell { content, style });
372            }
373        }
374    }
375
376    /// The index of the shared formula a formula written over `range` makes,
377    /// taken; `None` for a range of one cell, which holds a plain formula.
378    pub(crate) fn next_shared(&mut self, range: A1Range) -> Option<u32> {
379        (range.cells() > 1).then(|| {
380            self.shared_formulas += 1;
381            self.shared_formulas - 1
382        })
383    }
384
385    /// The used range as ((first row, first column), (last row, last
386    /// column)), or `None` for a sheet with no cell.
387    pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
388        #[cfg(test)]
389        work::extents(self.cells.len());
390        let mut it = self.cells.keys();
391        let &(r0, c0) = it.next()?;
392        let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
393        for &(r, c) in it {
394            top = top.min(r);
395            left = left.min(c);
396            bottom = bottom.max(r);
397            right = right.max(c);
398        }
399        Some(((top, left), (bottom, right)))
400    }
401}
402
403/// The shared formulas' masters of a sheet, by index: the text and the cell
404/// that holds it, so that a child's text is the master's with its
405/// references moved. The view record and the evaluator both read a child
406/// through this (KERNEL.4; KERNEL.7).
407pub fn shared_masters(sheet: &Sheet) -> HashMap<u32, (&str, u32, u32)> {
408    #[cfg(test)]
409    work::walked(sheet.cells.len());
410    let mut masters = HashMap::new();
411    for ((row, col), cell) in &sheet.cells {
412        if let Content::SharedMaster { text, si, .. } = &cell.content {
413            masters.insert(*si, (text.as_str(), *row, *col));
414        }
415    }
416    masters
417}
418
419/// A formula's text as the formula bar shows it, without its `=`, the
420/// file's prefixes dropped, as the reader spells the same cell of the
421/// written file: a shared child is its master's text moved by the cell's
422/// distance, moved first and stripped after, as the reader does it. `None`
423/// for a value, or for a child whose master the sheet has lost.
424pub fn formula_text(
425    content: &Content,
426    row: u32,
427    col: u32,
428    masters: &HashMap<u32, (&str, u32, u32)>,
429) -> Option<String> {
430    let raw = match content {
431        Content::Formula(t) | Content::DynamicFormula(t) => t.clone(),
432        Content::SharedMaster { text, .. } => text.clone(),
433        Content::SharedChild { si } => {
434            let (text, m_row, m_col) = masters.get(si)?;
435            crate::refers::shift_a1_references(
436                text,
437                i64::from(row) - i64::from(*m_row),
438                i64::from(col) - i64::from(*m_col),
439            )
440        }
441        Content::Text(_) | Content::Number(_) | Content::Bool(_) | Content::Error(_) => {
442            return None
443        }
444    };
445    Some(strip_future_prefixes(&raw))
446}
447
448/// What a formula written over `range` puts in one of its cells, the rule
449/// [`Sheet::set_formula`] and a machine's write share (KERNEL.34): the text
450/// for a range of one cell (`si` `None`); else the shared formula `si`, its
451/// text and its range at the range's first cell and its index at every
452/// other.
453pub(crate) fn filled(range: A1Range, text: &str, si: Option<u32>, row: u32, col: u32) -> Content {
454    match si {
455        None => Content::Formula(text.to_string()),
456        Some(si) if row == range.top && col == range.left => Content::SharedMaster {
457            text: text.to_string(),
458            range: range.text(),
459            si,
460        },
461        Some(si) => Content::SharedChild { si },
462    }
463}
464
465/// The work a machine's write does, counted in a test (KERNEL.34's oracle,
466/// as `crate::calc::vector::taken` notes a run's paths): the cells walked
467/// to find a sheet's shared formulas or a released fill's cells, the texts
468/// the mover moved, and the fills a write released, each whole or cell by
469/// cell. A write's cost is pinned by these counts and not by its time; an
470/// allocation count would miss the walks, which allocate once a master and
471/// not once a cell. Since KERNEL.37 a view's walks are counted too: the
472/// masters' and the extent's (`Sheet::extent`). A frame's end is counted the
473/// same way (KERNEL.36's oracle): the cells it copied into their twins, and
474/// the cells it walked; and since KERNEL.42 the cells a view printed or
475/// looked at, a window's every cell for its values and a changed cell
476/// alone for its changes. Counted on the thread that asked, between
477/// [`work::begin`] and [`work::end`], and only in a test build.
478#[cfg(test)]
479pub(crate) mod work {
480    use std::cell::Cell;
481
482    /// What the writes and views since [`begin`] did.
483    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
484    pub(crate) struct Work {
485        /// The cells walked.
486        pub(crate) walked: u64,
487        /// The texts moved.
488        pub(crate) moved: u64,
489        /// The fills released whole, their first cell left made their master.
490        pub(crate) whole: u64,
491        /// The fills released cell by cell, each cell given its own text.
492        pub(crate) split: u64,
493        /// The cells walked to find a sheet's extent.
494        pub(crate) extents: u64,
495        /// The cells a frame's end copied into their twins (KERNEL.36).
496        pub(crate) copied: u64,
497        /// The cells a frame's end walked, every cell of every sheet
498        /// (KERNEL.36).
499        pub(crate) swept: u64,
500        /// The cells a view visited: each cell of a window its values print,
501        /// each cell its changes name (KERNEL.42).
502        pub(crate) viewed: u64,
503    }
504
505    thread_local! {
506        static ON: Cell<bool> = const { Cell::new(false) };
507        static DONE: Cell<Work> = const {
508            Cell::new(Work {
509                walked: 0,
510                moved: 0,
511                whole: 0,
512                split: 0,
513                extents: 0,
514                copied: 0,
515                swept: 0,
516                viewed: 0,
517            })
518        };
519    }
520
521    /// Counting begins on this thread, from nothing.
522    pub(crate) fn begin() {
523        ON.with(|on| on.set(true));
524        DONE.with(|done| done.set(Work::default()));
525    }
526
527    /// What was counted since [`begin`]; counting ends.
528    pub(crate) fn end() -> Work {
529        ON.with(|on| on.set(false));
530        DONE.with(Cell::get)
531    }
532
533    fn add(f: impl FnOnce(&mut Work)) {
534        if ON.with(Cell::get) {
535            DONE.with(|done| {
536                let mut work = done.get();
537                f(&mut work);
538                done.set(work);
539            });
540        }
541    }
542
543    /// A walk over a sheet of `cells` cells.
544    pub(crate) fn walked(cells: usize) {
545        add(|w| w.walked += cells as u64);
546    }
547
548    /// A walk over a sheet of `cells` cells for its extent.
549    pub(crate) fn extents(cells: usize) {
550        add(|w| w.extents += cells as u64);
551    }
552
553    /// One text moved.
554    pub(crate) fn moved() {
555        add(|w| w.moved += 1);
556    }
557
558    /// One fill released, whole or cell by cell.
559    pub(crate) fn released(whole: bool) {
560        add(|w| {
561            if whole {
562                w.whole += 1;
563            } else {
564                w.split += 1;
565            }
566        });
567    }
568
569    /// One cell a frame's end copied into its twin.
570    pub(crate) fn copied() {
571        add(|w| w.copied += 1);
572    }
573
574    /// A frame's end's walk over a sheet of `cells` cells.
575    pub(crate) fn swept(cells: usize) {
576        add(|w| w.swept += cells as u64);
577    }
578
579    /// A view visiting `cells` cells (KERNEL.42).
580    pub(crate) fn viewed(cells: usize) {
581        add(|w| w.viewed += cells as u64);
582    }
583}
584
585/// The workbook: its sheets in tab order, their formats, and which tab is
586/// open when the file opens.
587#[derive(Clone, Debug)]
588pub struct Workbook {
589    pub sheets: Vec<Sheet>,
590    pub styles: Styles,
591    pub active_sheet: usize,
592    /// The workbook's defined names: each a name and its formula's text
593    /// without the `=` (a string constant is `"text"`, inner quotes doubled).
594    pub defined_names: Vec<(String, String)>,
595}
596
597impl Default for Workbook {
598    fn default() -> Self {
599        Self::new()
600    }
601}
602
603impl Workbook {
604    pub fn new() -> Workbook {
605        Workbook {
606            sheets: Vec::new(),
607            styles: Styles::new(),
608            active_sheet: 0,
609            defined_names: Vec::new(),
610        }
611    }
612
613    /// Whether any cell holds a dynamic-array formula, which the file then
614    /// needs its metadata part for.
615    pub fn has_dynamic_formulas(&self) -> bool {
616        self.sheets.iter().any(|s| {
617            s.cells
618                .values()
619                .any(|c| matches!(c.content, Content::DynamicFormula(_)))
620        })
621    }
622
623    /// The index of the sheet of that name, compared as Excel compares sheet
624    /// names, without case: `fold`'s test, which lowers the ASCII capitals
625    /// alone, made byte by byte in place (KERNEL.25). Every reference of
626    /// every evaluation asks it, and folding each name into a new string
627    /// was a third of a frame of Life.
628    pub fn find_sheet(&self, name: &str) -> Option<usize> {
629        self.sheets
630            .iter()
631            .position(|s| s.name.eq_ignore_ascii_case(name))
632    }
633
634    /// The sheet of that name, added last with the name as given when there
635    /// is none (`VlaEnsureSheet`'s shape).
636    pub fn ensure_sheet(&mut self, name: &str) -> usize {
637        if let Some(i) = self.find_sheet(name) {
638            return i;
639        }
640        self.sheets.push(Sheet::new(name));
641        self.sheets.len() - 1
642    }
643}
644
645/// A column's letters: 1 is A, 26 is Z, 27 is AA.
646pub fn column_letters(col: u32) -> String {
647    let mut n = col;
648    let mut letters = Vec::new();
649    while n > 0 {
650        let rem = (n - 1) % 26;
651        letters.push((b'A' + rem as u8) as char);
652        n = (n - 1) / 26;
653    }
654    letters.iter().rev().collect()
655}
656
657/// A cell's reference: row 1, column 2 is `B1`.
658pub fn cell_ref(row: u32, col: u32) -> String {
659    format!("{}{}", column_letters(col), row)
660}
661
662/// `ColumnWidth` in characters to the file's `width` attribute, as Excel
663/// computes it for the default font, Calibri 11, whose widest digit is 7
664/// pixels: the characters plus 5 pixels of padding, in 1/256ths of a
665/// character. 72 gives 72.7109375, which is what Excel itself writes for a
666/// column set to 72.
667pub fn column_width_attr(chars: f64) -> f64 {
668    const MAX_DIGIT_WIDTH: f64 = 7.0;
669    ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
670}
671
672/// A number as the file holds it, and as the view record spells it. Rust's
673/// shortest round-trip spelling, which never uses an exponent, is one Excel
674/// reads; a value no cell can hold (not finite) is `None`, which the writer
675/// and the view spell as Excel's `#NUM!` error.
676pub fn number_text(v: f64) -> Option<String> {
677    v.is_finite().then(|| format!("{v}"))
678}
679
680/// The first character of a name in a formula's text: a letter or `_`.
681pub fn is_name_start(c: char) -> bool {
682    c.is_ascii_alphabetic() || c == '_'
683}
684
685/// A character inside a name in a formula's text: a letter, a digit, `_`
686/// or `.`.
687pub fn is_name_char(c: char) -> bool {
688    c.is_ascii_alphanumeric() || c == '_' || c == '.'
689}
690
691/// A formula's text as the formula bar shows it: `_xlfn.`, `_xlfn._xlws.`
692/// and `_xlpm.`, the prefixes an OOXML file stores a newer function and its
693/// parameters under (the core's writer adds them, its reader drops them),
694/// dropped from every name outside a string literal. The names themselves
695/// are kept as the file spells them. Held here with the grid since the view
696/// record spells a formula this way (PORT.12); the inverse of the core's
697/// `sheet::xlfn::prefix_future_functions`.
698pub fn strip_future_prefixes(formula: &str) -> String {
699    let chars: Vec<char> = formula.chars().collect();
700    let mut out = String::with_capacity(formula.len());
701    let mut i = 0;
702    while i < chars.len() {
703        let c = chars[i];
704        if c == '"' {
705            out.push(c);
706            i += 1;
707            while i < chars.len() {
708                out.push(chars[i]);
709                if chars[i] == '"' {
710                    if i + 1 < chars.len() && chars[i + 1] == '"' {
711                        out.push('"');
712                        i += 2;
713                        continue;
714                    }
715                    i += 1;
716                    break;
717                }
718                i += 1;
719            }
720            continue;
721        }
722        if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
723            let start = i;
724            while i < chars.len() && is_name_char(chars[i]) {
725                i += 1;
726            }
727            let name: String = chars[start..i].iter().collect();
728            let mut bare = name.as_str();
729            loop {
730                let before = bare.len();
731                for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
732                    if bare.len() > prefix.len()
733                        && bare[..prefix.len()].eq_ignore_ascii_case(prefix)
734                    {
735                        bare = &bare[prefix.len()..];
736                    }
737                }
738                if bare.len() == before {
739                    break;
740                }
741            }
742            out.push_str(bare);
743            continue;
744        }
745        out.push(c);
746        i += 1;
747    }
748    out
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    #[test]
756    fn references_and_letters() {
757        assert_eq!(column_letters(1), "A");
758        assert_eq!(column_letters(26), "Z");
759        assert_eq!(column_letters(27), "AA");
760        assert_eq!(column_letters(702), "ZZ");
761        assert_eq!(column_letters(703), "AAA");
762        assert_eq!(cell_ref(1, 2), "B1");
763        assert_eq!(cell_ref(12, 3), "C12");
764    }
765
766    #[test]
767    fn a_sheet_is_found_as_the_fold_finds_it() {
768        // KERNEL.25: find_sheet compares in place what it once compared
769        // through `fold`, which stays here as the rule it must keep. Names
770        // whose folds differ only outside ASCII are different sheets.
771        use crate::intrinsics::fold;
772        let mut book = Workbook::new();
773        for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
774            book.sheets.push(Sheet::new(name));
775        }
776        let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
777        let asked = [
778            ("É", Some(0)),
779            ("é", Some(1)),
780            ("İstanbul", Some(2)),
781            ("istanbul", None),
782            ("straße", Some(3)),
783            ("STRASSE", None),
784            ("DATA", Some(4)),
785            ("SCREEN.LAST", Some(5)),
786            ("Screen", None),
787            ("", None),
788        ];
789        for (name, want) in asked {
790            assert_eq!(book.find_sheet(name), want, "{name}");
791            assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
792        }
793    }
794
795    #[test]
796    fn the_room_widths_as_excel_writes_them() {
797        assert_eq!(column_width_attr(72.0), 72.7109375);
798        assert_eq!(column_width_attr(60.0), 60.7109375);
799        assert_eq!(column_width_attr(8.43), 9.140625);
800    }
801
802    #[test]
803    fn styles_dedup_and_fills_keep_first_use_order() {
804        let mut s = Styles::new();
805        let a = s.id(Style {
806            fill: Some(Rgb(1, 2, 3)),
807            ..Style::default()
808        });
809        let b = s.id(Style {
810            fill: Some(Rgb(4, 5, 6)),
811            wrap: true,
812            ..Style::default()
813        });
814        let a2 = s.id(Style {
815            fill: Some(Rgb(1, 2, 3)),
816            ..Style::default()
817        });
818        assert_eq!((a, b, a2), (1, 2, 1));
819        assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
820        assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
821        // A format put at the index a record names, the table grown with
822        // defaults (KERNEL.22, the loader).
823        let wrap = Style {
824            wrap: true,
825            ..Style::default()
826        };
827        s.put(5, wrap.clone());
828        assert_eq!(s.xfs().len(), 6);
829        assert_eq!(s.get(5), Some(&wrap));
830        assert_eq!(s.get(4), Some(&Style::default()));
831        assert_eq!(s.get(6), None);
832    }
833
834    #[test]
835    fn a1_references_as_a_program_writes_them() {
836        let r = parse_a1_range("b2").unwrap();
837        assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
838        assert_eq!(r.text(), "B2");
839        let r = parse_a1_range("c4:A1").unwrap();
840        assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
841        assert_eq!(r.text(), "A1:C4");
842        assert_eq!(r.cells(), 12);
843        assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
844        assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
845        for bad in [
846            "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
847        ] {
848            assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
849        }
850    }
851
852    #[test]
853    fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
854        let mut sh = Sheet::new("S");
855        sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
856        sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
857        sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
858        assert_eq!(
859            sh.cells[&(2, 3)].content,
860            Content::SharedMaster {
861                text: "B2+B3".to_string(),
862                range: "C2:C3".to_string(),
863                si: 0
864            }
865        );
866        assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
867        assert_eq!(
868            sh.cells[&(2, 4)].content,
869            Content::Formula("B2*2".to_string())
870        );
871        assert_eq!(
872            sh.cells[&(2, 5)].content,
873            Content::SharedMaster {
874                text: "1".to_string(),
875                range: "E2:E3".to_string(),
876                si: 1
877            }
878        );
879        assert_eq!(sh.shared_formulas, 2);
880    }
881
882    #[test]
883    fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
884        let mut sh = Sheet::new("S");
885        sh.set_formula_dynamic(
886            parse_a1_range("c2:d3").unwrap(),
887            "IFS(B2>3,$A$1,TRUE,B$1)",
888            0,
889        );
890        assert_eq!(
891            sh.cells[&(2, 3)].content,
892            Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
893        );
894        assert_eq!(
895            sh.cells[&(3, 3)].content,
896            Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
897        );
898        assert_eq!(
899            sh.cells[&(2, 4)].content,
900            Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
901        );
902        assert_eq!(
903            sh.cells[&(3, 4)].content,
904            Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
905        );
906        let mut wb = Workbook::new();
907        assert!(!wb.has_dynamic_formulas());
908        wb.sheets.push(sh);
909        assert!(wb.has_dynamic_formulas());
910    }
911
912    #[test]
913    fn sheets_are_found_without_case_and_made_once() {
914        let mut wb = Workbook::new();
915        assert_eq!(wb.find_sheet("Output"), None);
916        let i = wb.ensure_sheet("Output");
917        assert_eq!(wb.ensure_sheet("output"), i);
918        assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
919        assert_eq!(wb.sheets.len(), 1);
920        assert_eq!(wb.sheets[0].name, "Output");
921    }
922
923    #[test]
924    fn a_sheet_knows_its_extent() {
925        let mut sh = Sheet::new("S");
926        assert_eq!(sh.extent(), None);
927        sh.set(
928            3,
929            2,
930            Cell {
931                content: Content::Number(1.0),
932                style: 0,
933            },
934        );
935        sh.set(
936            1,
937            3,
938            Cell {
939                content: Content::Bool(true),
940                style: 0,
941            },
942        );
943        assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
944    }
945
946    #[test]
947    fn numbers_as_the_file_holds_them() {
948        assert_eq!(number_text(5.0).as_deref(), Some("5"));
949        assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
950        assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
951        assert_eq!(number_text(f64::NAN), None);
952        assert_eq!(number_text(f64::INFINITY), None);
953    }
954
955    #[test]
956    fn the_prefixes_come_off_a_formula_read_back() {
957        assert_eq!(
958            strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
959            "IFS(B2>3,\"big\",TRUE,\"small\")"
960        );
961        assert_eq!(
962            strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
963            "FILTER(A:A,B:B>1)"
964        );
965        assert_eq!(
966            strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
967            "LET(x,1,x*2)"
968        );
969        assert_eq!(
970            strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
971            "SUM(A1:A3)&\"_xlfn.\""
972        );
973        assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
974        assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
975    }
976}