Skip to main content

vla_lang/
refers.rs

1//! The formula-reference reader (PORT.8, slice 8b): `VLA_Refers.bas`
2//! (`AXM.7`) ported one arm per arm, held to the refers golden.
3//!
4//! Pure text to records: the references inside a formula's text, read from
5//! the text alone, which is what `refers(from, to)` is built from. A parser
6//! for references, not for the formula language: operators and functions
7//! are stepped over, and the binders of `LET` and `LAMBDA` read as names (a
8//! recorded limit). The kinds, the record and the three readings are the
9//! reference's (`src/VLA_Refers.bas`, whose header says what each reads);
10//! `scripts/refers.txt` to `scripts/refers_golden.txt`, written by the
11//! reference's `VlaWriteRefersGolden`, is the golden
12//! `the_refers_golden_is_reproduced` holds this module to, and
13//! `tools/check_refers_golden.ps1` holds the pair's shape.
14//!
15//! Over the one scan, four renderers: [`spell`], the second field of a
16//! `refers` row in the treaty's spelling; [`r1c1`], the formula relative to
17//! a cell as Excel's `FormulaR1C1` writes it; [`shift_a1_references`], the
18//! writer's mover (every relative part moved by a fill's distance, `#REF!`
19//! off the sheet), which was `sheet/refs.rs`'s hand scanner until this
20//! slice and is held by the build golden; and [`resolve_books`], a file's
21//! `[1]Sheet1!A1` to the formula bar's `[Rates.xlsx]Sheet1!A1` through the
22//! external books the package names. [`quote_sheet`] is the one sheet
23//! quoting rule, used at both ends of a `refers` row, so that the relation
24//! joins to itself.
25
26use crate::intrinsics::fold;
27use crate::sheet::{column_letters, MAX_COLUMN, MAX_ROW};
28
29/// The eleven kinds of reference.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum Kind {
32    Cell,
33    Range,
34    Column,
35    Row,
36    Name,
37    Structured,
38    External,
39    /// A 3D span, `Jan:Dec!A1`.
40    ThreeD,
41    Spill,
42    /// `INDIRECT` or `OFFSET`, unreadable by name.
43    Unreadable,
44    /// `#REF!`, a broken reference.
45    Broken,
46}
47
48impl Kind {
49    /// The word the golden's first field spells.
50    pub fn word(self) -> &'static str {
51        match self {
52            Kind::Cell => "cell",
53            Kind::Range => "range",
54            Kind::Column => "column",
55            Kind::Row => "row",
56            Kind::Name => "name",
57            Kind::Structured => "structured",
58            Kind::External => "external",
59            Kind::ThreeD => "3d",
60            Kind::Spill => "spill",
61            Kind::Unreadable => "unreadable",
62            Kind::Broken => "broken",
63        }
64    }
65}
66
67/// The reference part's own shape where it has numbers.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum Shape {
70    None,
71    Cell,
72    Range,
73    Column,
74    Row,
75}
76
77/// One reference, as [`scan`] found it: `FormulaRef` of the reference.
78/// Positions are character indices into the text the scan was given.
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct FormulaRef {
81    pub kind: Kind,
82    pub shape: Shape,
83    /// External: the text inside the `[ ]` (`1`, `Book.xlsx`); empty otherwise.
84    pub book: String,
85    /// The sheet as named: unquoted, `''` undoubled; a span as `Jan:Dec`;
86    /// empty where none is written.
87    pub sheet_name: String,
88    /// The whole token as it stands in the formula.
89    pub written: String,
90    /// The part after any qualifier or `#REF!`: `$A$1`, `A1:B2`, `A1#`,
91    /// `Rate`, `Sales[Amount]`, `INDIRECT`, `#REF!`.
92    pub part: String,
93    pub row1: u32,
94    pub col1: u32,
95    pub row2: u32,
96    pub col2: u32,
97    pub row_abs1: bool,
98    pub col_abs1: bool,
99    pub row_abs2: bool,
100    pub col_abs2: bool,
101    /// Where the token begins.
102    pub pos: usize,
103    /// Its length in characters.
104    pub span: usize,
105    /// Where `part` begins, for a renderer.
106    pub part_pos: usize,
107}
108
109impl FormulaRef {
110    fn at(pos: usize) -> FormulaRef {
111        FormulaRef {
112            kind: Kind::Name,
113            shape: Shape::None,
114            book: String::new(),
115            sheet_name: String::new(),
116            written: String::new(),
117            part: String::new(),
118            row1: 0,
119            col1: 0,
120            row2: 0,
121            col2: 0,
122            row_abs1: false,
123            col_abs1: false,
124            row_abs2: false,
125            col_abs2: false,
126            pos,
127            span: 0,
128            part_pos: pos,
129        }
130    }
131
132    /// The canonical name of an unreadable call, as its row spells it.
133    pub fn unreadable_name(&self) -> &'static str {
134        if fold(&self.part) == "indirect" {
135            "INDIRECT"
136        } else {
137            "OFFSET"
138        }
139    }
140}
141
142// ---------------------------------------------------------------------
143//  The scan (RefersScan)
144// ---------------------------------------------------------------------
145
146/// Every reference in a formula's text, in formula order, duplicates kept.
147/// A leading `=` is stepped over.
148pub fn scan(formula_text: &str) -> Vec<FormulaRef> {
149    let s: Vec<char> = formula_text.chars().collect();
150    let n = s.len();
151    let mut refs = Vec::new();
152    let mut i = 0;
153    if n >= 1 && s[0] == '=' {
154        i = 1;
155    }
156    while i < n {
157        if s[i] == '"' {
158            i = skip_delimited(&s, i, '"');
159        } else if !read_reference(&s, &mut i, &mut refs) {
160            i += 1;
161        }
162    }
163    refs
164}
165
166/// What can begin at `i`: a qualifier and its part, a bare part, a bare
167/// structured reference, an error literal, a function name, a number, or a
168/// quoted text that qualifies nothing. Moves `i` past what it read and
169/// answers true; false where nothing of the kind begins.
170fn read_reference(s: &[char], i: &mut usize, refs: &mut Vec<FormulaRef>) -> bool {
171    let ch = s[*i];
172    let mut r = FormulaRef::at(*i);
173    let mut j = *i;
174    if read_qualifier(s, &mut j, &mut r) {
175        r.part_pos = j;
176        if read_part(s, &mut j, &mut r) {
177            finish_ref(s, j, r, refs);
178        }
179        *i = j;
180        return true;
181    }
182    if ch == '\'' {
183        *i = skip_delimited(s, *i, '\'');
184        true
185    } else if ch == '[' {
186        let end = bracket_group_end(s, *i);
187        r.kind = Kind::Structured;
188        r.part_pos = *i;
189        r.part = s[*i..end].iter().collect();
190        finish_ref(s, end, r, refs);
191        *i = end;
192        true
193    } else if ch == '#' || ch == '$' || is_ident_char(ch) {
194        r.part_pos = *i;
195        if read_part(s, &mut j, &mut r) {
196            finish_ref(s, j, r, refs);
197        }
198        if j == *i {
199            j = *i + 1;
200        }
201        *i = j;
202        true
203    } else {
204        false
205    }
206}
207
208/// A sheet qualifier at `j`: `'quoted'!`, `[book]sheet!`, `[book]!`,
209/// `sheet!`, or a span `first:last!`. Fills the book and the sheet, moves
210/// `j` past the `!` and answers true; otherwise leaves `j` and answers false.
211fn read_qualifier(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
212    let n = s.len();
213    let ch = s[*j];
214    if ch == '\'' {
215        let Some(k) = delimited_end(s, *j, '\'') else {
216            return false;
217        };
218        if s.get(k) != Some(&'!') {
219            return false;
220        }
221        let inner: String = s[*j + 1..k - 1].iter().collect();
222        let q = inner.replace("''", "'");
223        match (q.find('['), q.find(']')) {
224            (Some(b), Some(e)) if e > b => {
225                r.book = q[b + 1..e].to_string();
226                r.sheet_name = q[e + 1..].to_string();
227            }
228            _ => r.sheet_name = q,
229        }
230        *j = k + 1;
231        return true;
232    }
233    let mut k = *j;
234    let mut e = 0usize;
235    if ch == '[' {
236        let Some(e_rel) = s[*j + 1..].iter().position(|&c| c == ']') else {
237            return false;
238        };
239        e = *j + 1 + e_rel;
240        if let Some(b_rel) = s[*j + 1..].iter().position(|&c| c == '[') {
241            if *j + 1 + b_rel < e {
242                return false; // a nested [: a structured reference, not a book
243            }
244        }
245        k = e + 1;
246        if s.get(k) == Some(&'!') {
247            r.book = s[*j + 1..e].iter().collect();
248            r.sheet_name.clear();
249            *j = k + 1;
250            return true;
251        }
252    }
253    let mut p = k;
254    while p < n && is_ident_char(s[p]) {
255        p += 1;
256    }
257    if p == k {
258        return false;
259    }
260    let mut q: String = s[k..p].iter().collect();
261    if s.get(p) == Some(&':') {
262        let mut p2 = p + 1;
263        while p2 < n && is_ident_char(s[p2]) {
264            p2 += 1;
265        }
266        if p2 > p + 1 && s.get(p2) == Some(&'!') {
267            q.push(':');
268            q.extend(s[p + 1..p2].iter());
269            p = p2;
270        }
271    }
272    if s.get(p) != Some(&'!') {
273        return false;
274    }
275    if ch == '[' {
276        r.book = s[*j + 1..e].iter().collect();
277    }
278    r.sheet_name = q;
279    *j = p + 1;
280    true
281}
282
283/// The reference part at `j`, after a qualifier or bare: `#REF!` (with the
284/// cell or range a deleted sheet leaves after it), a cell, a range, a
285/// column, a row, a structured reference with its table, a name, either's
286/// spill `#`, or an unreadable call's name. A function name, `TRUE`,
287/// `FALSE`, a number or another error literal is stepped over with false;
288/// `j` stays only where nothing begins.
289fn read_part(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
290    if s.get(*j) == Some(&'#') {
291        let k = error_literal_end(s, *j);
292        let tok: String = s[*j..k].iter().collect();
293        *j = k;
294        if fold(&tok) != "#ref!" {
295            return false;
296        }
297        r.kind = Kind::Broken;
298        r.part = tok;
299        if read_cell_like(s, j, r) {
300            r.part_pos = *j - r.part.chars().count();
301        }
302        return true;
303    }
304    let k = token_end(s, *j);
305    if k == *j {
306        return false;
307    }
308    let tok: String = s[*j..k].iter().collect();
309    let nxt = s.get(k).copied();
310    if nxt == Some('(') {
311        *j = k;
312        let f = fold(&tok);
313        if f == "indirect" || f == "offset" {
314            r.kind = Kind::Unreadable;
315            r.part = tok;
316            return true;
317        }
318        return false;
319    }
320    if nxt == Some('[') {
321        let end = bracket_group_end(s, k);
322        r.kind = Kind::Structured;
323        r.part = s[*j..end].iter().collect();
324        *j = end;
325        return true;
326    }
327    if read_cell_like(s, j, r) {
328        r.kind = match r.shape {
329            Shape::Cell => {
330                if s.get(*j) == Some(&'#') {
331                    r.part.push('#');
332                    *j += 1;
333                    Kind::Spill
334                } else {
335                    Kind::Cell
336                }
337            }
338            Shape::Range => Kind::Range,
339            Shape::Column => Kind::Column,
340            Shape::Row | Shape::None => Kind::Row,
341        };
342        return true;
343    }
344    let mut first = tok.chars().next();
345    if first == Some('$') {
346        first = tok.chars().nth(1);
347    }
348    if first == Some('.') || first.is_some_and(|c| c.is_ascii_digit()) {
349        *j = k; // a number: 1.5, 2E (its +3 follows), .5
350        return false;
351    }
352    let f = fold(&tok);
353    if f == "true" || f == "false" {
354        *j = k;
355        return false;
356    }
357    r.kind = Kind::Name;
358    r.part = tok;
359    *j = k;
360    if s.get(*j) == Some(&'#') {
361        r.kind = Kind::Spill;
362        r.part.push('#');
363        *j += 1;
364    }
365    true
366}
367
368/// A cell, range, whole column or whole row at `j`, its second half read
369/// after a colon when the two halves are of one shape. Sets the shape, the
370/// part and the numbers, moves `j` and answers true; otherwise leaves
371/// everything as it was.
372fn read_cell_like(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
373    let k = token_end(s, *j);
374    if k == *j {
375        return false;
376    }
377    let tok: String = s[*j..k].iter().collect();
378    let mut tok2 = String::new();
379    let mut k2 = k;
380    if s.get(k) == Some(&':') {
381        k2 = token_end(s, k + 1);
382        if k2 > k + 1 {
383            tok2 = s[k + 1..k2].iter().collect();
384        }
385    }
386    let mut shape = Shape::None;
387    let (mut r1, mut c1, mut r2, mut c2) = (0u32, 0u32, 0u32, 0u32);
388    let (mut ra1, mut ca1, mut ra2, mut ca2) = (false, false, false, false);
389    if let Some((row, col, ra, ca)) = parse_cell_part(&tok) {
390        r1 = row;
391        c1 = col;
392        ra1 = ra;
393        ca1 = ca;
394        shape = Shape::Cell;
395        if !tok2.is_empty() {
396            if let Some((row, col, ra, ca)) = parse_cell_part(&tok2) {
397                r2 = row;
398                c2 = col;
399                ra2 = ra;
400                ca2 = ca;
401                shape = Shape::Range;
402            }
403        }
404    } else if !tok2.is_empty() {
405        if let (Some((col, ca)), Some((col2, ca_two))) =
406            (parse_col_part(&tok), parse_col_part(&tok2))
407        {
408            c1 = col;
409            ca1 = ca;
410            c2 = col2;
411            ca2 = ca_two;
412            shape = Shape::Column;
413        } else if let (Some((row, ra)), Some((row2, ra_two))) =
414            (parse_row_part(&tok), parse_row_part(&tok2))
415        {
416            r1 = row;
417            ra1 = ra;
418            r2 = row2;
419            ra2 = ra_two;
420            shape = Shape::Row;
421        }
422    }
423    if shape == Shape::None {
424        return false;
425    }
426    r.shape = shape;
427    r.row1 = r1;
428    r.col1 = c1;
429    r.row_abs1 = ra1;
430    r.col_abs1 = ca1;
431    if shape == Shape::Cell {
432        r.part = tok;
433        *j = k;
434    } else {
435        r.row2 = r2;
436        r.col2 = c2;
437        r.row_abs2 = ra2;
438        r.col_abs2 = ca2;
439        r.part = s[*j..k2].iter().collect();
440        *j = k2;
441    }
442    true
443}
444
445/// The record is complete: its text, and the kind a qualifier decides.
446fn finish_ref(s: &[char], j: usize, mut r: FormulaRef, refs: &mut Vec<FormulaRef>) {
447    r.span = j - r.pos;
448    r.written = s[r.pos..j].iter().collect();
449    if !r.book.is_empty() {
450        r.kind = Kind::External;
451    } else if r.sheet_name.contains(':') {
452        r.kind = Kind::ThreeD;
453    }
454    refs.push(r);
455}
456
457// ---------------------------------------------------------------------
458//  The pieces of a part
459// ---------------------------------------------------------------------
460
461/// `$?LETTERS$?DIGITS`, the whole token, within the sheet: (row, column,
462/// row absolute, column absolute).
463fn parse_cell_part(tok: &str) -> Option<(u32, u32, bool, bool)> {
464    let c: Vec<char> = tok.chars().collect();
465    let n = c.len();
466    let mut p = 0;
467    let mut col_abs = false;
468    if c.first() == Some(&'$') {
469        col_abs = true;
470        p = 1;
471    }
472    let mut k = p;
473    while k < n && c[k].is_ascii_alphabetic() {
474        k += 1;
475    }
476    let letters: String = c[p..k].iter().collect();
477    if letters.is_empty() || letters.len() > 3 {
478        return None;
479    }
480    let mut row_abs = false;
481    if c.get(k) == Some(&'$') {
482        row_abs = true;
483        k += 1;
484    }
485    let p2 = k;
486    while k < n && c[k].is_ascii_digit() {
487        k += 1;
488    }
489    let digits: String = c[p2..k].iter().collect();
490    if digits.is_empty() || digits.len() > 7 {
491        return None;
492    }
493    if k < n {
494        return None; // something after the digits: a name (ABC123X)
495    }
496    let col = letters_to_col(&letters)?;
497    let row: u32 = digits.parse().ok()?;
498    if row == 0 || row > MAX_ROW {
499        return None;
500    }
501    Some((row, col, row_abs, col_abs))
502}
503
504/// `$?LETTERS`, the whole token, for one end of a whole-column reference.
505fn parse_col_part(tok: &str) -> Option<(u32, bool)> {
506    let (letters, abs) = match tok.strip_prefix('$') {
507        Some(rest) => (rest, true),
508        None => (tok, false),
509    };
510    let count = letters.chars().count();
511    if count == 0 || count > 3 || !letters.chars().all(|c| c.is_ascii_alphabetic()) {
512        return None;
513    }
514    Some((letters_to_col(letters)?, abs))
515}
516
517/// `$?DIGITS`, the whole token, for one end of a whole-row reference.
518fn parse_row_part(tok: &str) -> Option<(u32, bool)> {
519    let (digits, abs) = match tok.strip_prefix('$') {
520        Some(rest) => (rest, true),
521        None => (tok, false),
522    };
523    let count = digits.chars().count();
524    if count == 0 || count > 7 || !digits.chars().all(|c| c.is_ascii_digit()) {
525        return None;
526    }
527    let row: u32 = digits.parse().ok()?;
528    if row == 0 || row > MAX_ROW {
529        return None;
530    }
531    Some((row, abs))
532}
533
534/// A = 1 ... XFD = 16384; `None` past the last column. The expander's
535/// `row-expand` and `column-expand` read a column by it too (VLA.3), as
536/// `VLA.bas` reads one by `VLA_Refers`' `RefersColumnNumber`.
537pub(crate) fn letters_to_col(letters: &str) -> Option<u32> {
538    let mut v: u32 = 0;
539    for ch in fold(letters).chars() {
540        let c = ch as u32;
541        if !(97..=122).contains(&c) {
542            return None;
543        }
544        v = v * 26 + (c - 96);
545    }
546    if v == 0 || v > MAX_COLUMN {
547        return None;
548    }
549    Some(v)
550}
551
552// ---------------------------------------------------------------------
553//  Walking the text
554// ---------------------------------------------------------------------
555
556/// The position after the run of identifier characters and `$` signs at `j`.
557fn token_end(s: &[char], j: usize) -> usize {
558    let mut k = j;
559    while k < s.len() && (s[k] == '$' || is_ident_char(s[k])) {
560        k += 1;
561    }
562    k
563}
564
565/// The position after the error literal at `j` (its `#`): letters, digits,
566/// `/` and `_`, then a closing `!` or `?` when there is one.
567fn error_literal_end(s: &[char], j: usize) -> usize {
568    let mut k = j + 1;
569    while k < s.len() && (s[k].is_ascii_alphanumeric() || s[k] == '/' || s[k] == '_') {
570        k += 1;
571    }
572    if matches!(s.get(k), Some('!') | Some('?')) {
573        k += 1;
574    }
575    k
576}
577
578/// The position after the closing delimiter of the delimited text at `j`;
579/// a doubled delimiter stays inside. `None` when it never closes.
580fn delimited_end(s: &[char], j: usize, d: char) -> Option<usize> {
581    let n = s.len();
582    let mut k = j + 1;
583    while k < n {
584        if s[k] != d {
585            k += 1;
586        } else if s.get(k + 1) == Some(&d) {
587            k += 2;
588        } else {
589            return Some(k + 1);
590        }
591    }
592    None
593}
594
595/// [`delimited_end`], or the end of the text when the delimiter never closes.
596/// Shared with the formula sink's scan (`egress`), which steps over the same
597/// three groups, so the crate has one string-skipping rule.
598pub fn skip_delimited(s: &[char], j: usize, d: char) -> usize {
599    delimited_end(s, j, d).unwrap_or(s.len())
600}
601
602/// The position after the `]` that closes the `[` at `i`, nested brackets
603/// counted and a `'` inside taken as the escape it is there; the end of the
604/// text when it never closes.
605pub fn bracket_group_end(s: &[char], i: usize) -> usize {
606    let n = s.len();
607    let mut k = i;
608    let mut depth: i32 = 0;
609    while k < n {
610        let ch = s[k];
611        if ch == '\'' {
612            k += 2;
613        } else {
614            if ch == '[' {
615                depth += 1;
616            }
617            if ch == ']' {
618                depth -= 1;
619            }
620            k += 1;
621            if depth == 0 {
622                return k;
623            }
624        }
625    }
626    n
627}
628
629/// A letter, a digit, `_` or `.`, or any character past ASCII (a letter in
630/// another alphabet, as a name or a sheet may hold).
631pub fn is_ident_char(ch: char) -> bool {
632    ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || (ch as u32) > 127
633}
634
635// ---------------------------------------------------------------------
636//  The spellings (RefersSpell, RefersQuoteSheet)
637// ---------------------------------------------------------------------
638
639/// The second field of a `refers` row for one record, the home sheet
640/// qualifying a reference written bare.
641pub fn spell(r: &FormulaRef, home_sheet: &str) -> String {
642    let q = if r.sheet_name.is_empty() {
643        format!("{}!", quote_sheet(home_sheet))
644    } else {
645        format!("{}!", quote_sheet(&r.sheet_name))
646    };
647    match r.kind {
648        Kind::Cell | Kind::Range | Kind::Column | Kind::Row => format!("{q}{}", spelled_part(r)),
649        Kind::Spill => {
650            if r.shape == Shape::Cell {
651                format!("{q}{}#", spelled_part(r))
652            } else {
653                r.written.clone()
654            }
655        }
656        Kind::Broken => {
657            if r.shape != Shape::None {
658                r.written.clone()
659            } else {
660                format!("{q}#REF!")
661            }
662        }
663        Kind::Unreadable => format!("(unreadable \"{}\")", r.unreadable_name()),
664        Kind::Name | Kind::Structured | Kind::External | Kind::ThreeD => r.written.clone(),
665    }
666}
667
668/// The part in A1 with its marks dropped and its letters upper-cased.
669fn spelled_part(r: &FormulaRef) -> String {
670    match r.shape {
671        Shape::Cell => format!("{}{}", column_letters(r.col1), r.row1),
672        Shape::Range => format!(
673            "{}{}:{}{}",
674            column_letters(r.col1),
675            r.row1,
676            column_letters(r.col2),
677            r.row2
678        ),
679        Shape::Column => format!("{}:{}", column_letters(r.col1), column_letters(r.col2)),
680        Shape::Row => format!("{}:{}", r.row1, r.row2),
681        Shape::None => String::new(),
682    }
683}
684
685/// A sheet name as a reference spells it: quoted when it must be, an
686/// apostrophe inside doubled.
687pub fn quote_sheet(sheet_name: &str) -> String {
688    if sheet_needs_quotes(sheet_name) {
689        format!("'{}'", sheet_name.replace('\'', "''"))
690    } else {
691        sheet_name.to_string()
692    }
693}
694
695/// Excel's rule as the owner's pass of 2026-10-04 confirmed it: a character
696/// outside letters, digits, `_` and `.`, a leading digit, or a name that is
697/// itself a cell or an R1C1 reference.
698fn sheet_needs_quotes(sheet_name: &str) -> bool {
699    let Some(first) = sheet_name.chars().next() else {
700        return true;
701    };
702    if first.is_ascii_digit() {
703        return true;
704    }
705    if !sheet_name.chars().all(is_ident_char) {
706        return true;
707    }
708    if parse_cell_part(sheet_name).is_some() {
709        return true;
710    }
711    is_r1c1_shaped(sheet_name)
712}
713
714/// `R`, `C`, `R1`, `C1`, `RC`, `R1C1`, `R12C34`: a sheet so named is quoted.
715fn is_r1c1_shaped(sheet_name: &str) -> bool {
716    let f = fold(sheet_name);
717    let mut chars = f.chars();
718    let mut seen_c = match chars.next() {
719        Some('c') => true,
720        Some('r') => false,
721        _ => return false,
722    };
723    for ch in chars {
724        if ch == 'c' && !seen_c {
725            seen_c = true;
726        } else if !ch.is_ascii_digit() {
727            return false;
728        }
729    }
730    true
731}
732
733// ---------------------------------------------------------------------
734//  R1C1, the mover, the books, and the home cell
735// ---------------------------------------------------------------------
736
737/// The text with every part that has numbers rewritten by `render`,
738/// everything else as written: the shape every renderer shares.
739fn rewrite(formula_text: &str, render: impl Fn(&FormulaRef) -> String) -> String {
740    let chars: Vec<char> = formula_text.chars().collect();
741    let mut out = String::with_capacity(formula_text.len() + 8);
742    let mut p = 0;
743    for r in &scan(formula_text) {
744        if r.shape != Shape::None {
745            out.extend(chars[p..r.part_pos].iter());
746            out.push_str(&render(r));
747            if r.kind == Kind::Spill {
748                out.push('#');
749            }
750            p = r.pos + r.span;
751        }
752    }
753    out.extend(chars[p..].iter());
754    out
755}
756
757/// The formula rendered in R1C1 relative to the cell at (`home_row`,
758/// `home_col`), as Excel's `FormulaR1C1` writes it.
759pub fn r1c1(formula_text: &str, home_row: u32, home_col: u32) -> String {
760    rewrite(formula_text, |r| r1c1_part(r, home_row, home_col))
761}
762
763/// One part in R1C1: a whole column or row whose two ends render alike is
764/// written once, as Excel writes it.
765fn r1c1_part(r: &FormulaRef, home_row: u32, home_col: u32) -> String {
766    let cell = |row: u32, ra: bool, col: u32, ca: bool| {
767        format!(
768            "R{}C{}",
769            rel_spell(row, ra, home_row),
770            rel_spell(col, ca, home_col)
771        )
772    };
773    match r.shape {
774        Shape::Cell => cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
775        Shape::Range => format!(
776            "{}:{}",
777            cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
778            cell(r.row2, r.row_abs2, r.col2, r.col_abs2)
779        ),
780        Shape::Column => {
781            let a = format!("C{}", rel_spell(r.col1, r.col_abs1, home_col));
782            let b = format!("C{}", rel_spell(r.col2, r.col_abs2, home_col));
783            if a == b {
784                a
785            } else {
786                format!("{a}:{b}")
787            }
788        }
789        Shape::Row => {
790            let a = format!("R{}", rel_spell(r.row1, r.row_abs1, home_row));
791            let b = format!("R{}", rel_spell(r.row2, r.row_abs2, home_row));
792            if a == b {
793                a
794            } else {
795                format!("{a}:{b}")
796            }
797        }
798        Shape::None => String::new(),
799    }
800}
801
802/// `5` for an absolute 5; `[2]`, `[-3]` or nothing for a relative one.
803fn rel_spell(n: u32, is_abs: bool, home: u32) -> String {
804    if is_abs {
805        n.to_string()
806    } else if n == home {
807        String::new()
808    } else {
809        format!("[{}]", i64::from(n) - i64::from(home))
810    }
811}
812
813/// The formula's text with every relative reference moved `d_row` rows down
814/// and `d_col` columns right, as the formula of a cell that far from the
815/// first cell of a fill; a part moved off the sheet becomes `#REF!`, as
816/// Excel writes it. The writer's mover (PORT.7, slice 7c), held by the build
817/// golden; a shared formula's children in the reader are rendered with it.
818pub fn shift_a1_references(formula: &str, d_row: i64, d_col: i64) -> String {
819    if d_row == 0 && d_col == 0 {
820        return formula.to_string();
821    }
822    #[cfg(test)]
823    crate::sheet::work::moved();
824    rewrite(formula, |r| shifted_part(r, d_row, d_col))
825}
826
827/// Whether moving the text changes its references alone, each by the
828/// offset, for every offset from `low` to `high`, rows then columns: every
829/// part with numbers stays on the sheet at both ends, by the mover's own
830/// rule, and the text holds no span of sheets nor a broken reference with a
831/// cell after it, which the mover writes as a construct (KERNEL.34). Where
832/// it does, the text moved twice is the text moved once by the sum, and a
833/// moved text holds the same references at the same places, each moved,
834/// with the same text between; where it does not, a part moved off the
835/// sheet is `#REF!` and stays so.
836pub(crate) fn moves_whole(formula: &str, low: (i64, i64), high: (i64, i64)) -> bool {
837    let on = |n: u32, is_abs: bool, (lo, hi): (i64, i64), max: u32| {
838        shifted(n, is_abs, lo, max).is_some() && shifted(n, is_abs, hi, max).is_some()
839    };
840    let rows = (low.0, high.0);
841    let cols = (low.1, high.1);
842    scan(formula).iter().all(|r| {
843        if r.kind == Kind::ThreeD || (r.kind == Kind::Broken && r.shape != Shape::None) {
844            return false;
845        }
846        match r.shape {
847            Shape::None => true,
848            Shape::Cell => {
849                on(r.row1, r.row_abs1, rows, MAX_ROW) && on(r.col1, r.col_abs1, cols, MAX_COLUMN)
850            }
851            Shape::Range => {
852                on(r.row1, r.row_abs1, rows, MAX_ROW)
853                    && on(r.col1, r.col_abs1, cols, MAX_COLUMN)
854                    && on(r.row2, r.row_abs2, rows, MAX_ROW)
855                    && on(r.col2, r.col_abs2, cols, MAX_COLUMN)
856            }
857            Shape::Column => {
858                on(r.col1, r.col_abs1, cols, MAX_COLUMN) && on(r.col2, r.col_abs2, cols, MAX_COLUMN)
859            }
860            Shape::Row => {
861                on(r.row1, r.row_abs1, rows, MAX_ROW) && on(r.row2, r.row_abs2, rows, MAX_ROW)
862            }
863        }
864    })
865}
866
867fn shifted(n: u32, is_abs: bool, d: i64, max: u32) -> Option<u32> {
868    if is_abs {
869        return Some(n);
870    }
871    let m = i64::from(n) + d;
872    (m >= 1 && m <= i64::from(max)).then_some(m as u32)
873}
874
875fn mark(is_abs: bool) -> &'static str {
876    if is_abs {
877        "$"
878    } else {
879        ""
880    }
881}
882
883fn shifted_part(r: &FormulaRef, d_row: i64, d_col: i64) -> String {
884    let cell = |row: u32, ra: bool, col: u32, ca: bool| -> Option<String> {
885        let col = shifted(col, ca, d_col, MAX_COLUMN)?;
886        let row = shifted(row, ra, d_row, MAX_ROW)?;
887        Some(format!(
888            "{}{}{}{}",
889            mark(ca),
890            column_letters(col),
891            mark(ra),
892            row
893        ))
894    };
895    let rendered = match r.shape {
896        Shape::Cell => cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
897        Shape::Range => match (
898            cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
899            cell(r.row2, r.row_abs2, r.col2, r.col_abs2),
900        ) {
901            (Some(a), Some(b)) => Some(format!("{a}:{b}")),
902            _ => None,
903        },
904        Shape::Column => match (
905            shifted(r.col1, r.col_abs1, d_col, MAX_COLUMN),
906            shifted(r.col2, r.col_abs2, d_col, MAX_COLUMN),
907        ) {
908            (Some(a), Some(b)) => Some(format!(
909                "{}{}:{}{}",
910                mark(r.col_abs1),
911                column_letters(a),
912                mark(r.col_abs2),
913                column_letters(b)
914            )),
915            _ => None,
916        },
917        Shape::Row => match (
918            shifted(r.row1, r.row_abs1, d_row, MAX_ROW),
919            shifted(r.row2, r.row_abs2, d_row, MAX_ROW),
920        ) {
921            (Some(a), Some(b)) => Some(format!(
922                "{}{}:{}{}",
923                mark(r.row_abs1),
924                a,
925                mark(r.row_abs2),
926                b
927            )),
928            _ => None,
929        },
930        Shape::None => None,
931    };
932    rendered.unwrap_or_else(|| "#REF!".to_string())
933}
934
935/// A file's `[1]Sheet1!A1` and `[1]!Rate` as the formula bar shows them,
936/// `[Rates.xlsx]Sheet1!A1`, through the external books in the order the
937/// file numbers them (`books[0]` is `[1]`); a number past the list, or a
938/// book with no name, stays as the file wrote it.
939pub fn resolve_books(formula_text: &str, books: &[String]) -> String {
940    let chars: Vec<char> = formula_text.chars().collect();
941    let mut out = String::with_capacity(formula_text.len() + 16);
942    let mut p = 0;
943    for r in &scan(formula_text) {
944        if r.kind != Kind::External || r.book.is_empty() {
945            continue;
946        }
947        if !r.book.chars().all(|c| c.is_ascii_digit()) {
948            continue;
949        }
950        let Some(name) = r
951            .book
952            .parse::<usize>()
953            .ok()
954            .and_then(|n| n.checked_sub(1))
955            .and_then(|i| books.get(i))
956            .filter(|name| !name.is_empty())
957        else {
958            continue;
959        };
960        // The book's text sits right after the token's first bracket.
961        let Some(open) = r.written.find('[') else {
962            continue;
963        };
964        let book_len = r.book.len();
965        let mut token = String::with_capacity(r.written.len() + name.len());
966        token.push_str(&r.written[..open + 1]);
967        token.push_str(name);
968        token.push_str(&r.written[open + 1 + book_len..]);
969        out.extend(chars[p..r.pos].iter());
970        out.push_str(&token);
971        p = r.pos + r.span;
972    }
973    out.extend(chars[p..].iter());
974    out
975}
976
977/// A formula's own cell as a `refers` row spells its first field,
978/// `Model!B3` or `'Q1 Data'!C5`, read with the same scan: (sheet, row,
979/// column). `None` for anything else: a bare `B3`, a range, a book, more
980/// text.
981pub fn parse_home(home_text: &str) -> Option<(String, u32, u32)> {
982    let refs = scan(home_text);
983    if refs.len() != 1 {
984        return None;
985    }
986    let r = &refs[0];
987    if r.kind != Kind::Cell || r.sheet_name.is_empty() {
988        return None;
989    }
990    if r.pos != 0 || r.span != home_text.chars().count() {
991        return None;
992    }
993    Some((r.sheet_name.clone(), r.row1, r.col1))
994}
995
996// ---------------------------------------------------------------------
997//  The golden's report (RefersReportFor, VlaWriteRefersGolden)
998// ---------------------------------------------------------------------
999
1000/// One case's record, as `RefersReportFor` writes it: the body's first
1001/// line is the cell, the rest the formula, blank lines at its end dropped;
1002/// one `<kind>\t<written>\t<spelled>` line per reference or `NONE`, then
1003/// the `R1C1` line. `BAD HOME` names a cell line that does not read.
1004pub fn report_for(body: &[&str]) -> String {
1005    let home_text = body.first().copied().unwrap_or("");
1006    let mut formula: String = if body.len() > 1 {
1007        body[1..].join("\n")
1008    } else {
1009        String::new()
1010    };
1011    while formula.ends_with('\n') {
1012        formula.pop();
1013    }
1014    let Some((home_sheet, home_row, home_col)) = parse_home(home_text) else {
1015        return format!("BAD HOME\t{home_text}\r\n");
1016    };
1017    let refs = scan(&formula);
1018    let mut out = String::new();
1019    if refs.is_empty() {
1020        out.push_str("NONE\r\n");
1021    } else {
1022        for r in &refs {
1023            out.push_str(&format!(
1024                "{}\t{}\t{}\r\n",
1025                r.kind.word(),
1026                r.written,
1027                spell(r, &home_sheet)
1028            ));
1029        }
1030    }
1031    out.push_str(&format!("R1C1\t{}\r\n", r1c1(&formula, home_row, home_col)));
1032    out
1033}
1034
1035/// `VlaWriteRefersGolden`'s text for a fixture: each `=== ` header, then
1036/// its case's report; the lines before the first header are ignored.
1037pub fn fixture_report(fixture: &str) -> String {
1038    let text = fixture.replace("\r\n", "\n");
1039    let mut out = String::new();
1040    let mut header: Option<&str> = None;
1041    let mut body: Vec<&str> = Vec::new();
1042    for line in text.split('\n') {
1043        if line.starts_with("=== ") {
1044            if let Some(h) = header {
1045                out.push_str(h);
1046                out.push_str("\r\n");
1047                out.push_str(&report_for(&body));
1048            }
1049            header = Some(line);
1050            body.clear();
1051        } else if header.is_some() {
1052            body.push(line);
1053        }
1054    }
1055    if let Some(h) = header {
1056        out.push_str(h);
1057        out.push_str("\r\n");
1058        out.push_str(&report_for(&body));
1059    }
1060    out
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065    use super::*;
1066
1067    const FIXTURE: &str = include_str!("../testdata/refers.txt");
1068    const GOLDEN: &str = include_str!("../testdata/refers_golden.txt");
1069
1070    #[test]
1071    fn the_refers_golden_is_reproduced() {
1072        // scripts/refers_golden.txt is written by VlaWriteRefersGolden
1073        // (VLA_Tests.bas) from the reference's VLA_Refers; this is the
1074        // reader's oracle, compared as the treaty compares a golden (LF,
1075        // trailing blank lines dropped).
1076        let norm = |s: &str| s.replace("\r\n", "\n").trim_end_matches('\n').to_string();
1077        let got = norm(&fixture_report(FIXTURE));
1078        let want = norm(GOLDEN);
1079        if got != want {
1080            let at = got
1081                .chars()
1082                .zip(want.chars())
1083                .position(|(a, b)| a != b)
1084                .unwrap_or(got.len().min(want.len()));
1085            let line = want[..at].matches('\n').count() + 1;
1086            let context = |s: &str| {
1087                let start = s[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
1088                let end = s[at..].find('\n').map(|i| at + i).unwrap_or(s.len());
1089                s[start..end].to_string()
1090            };
1091            panic!(
1092                "the refers golden differs at char {at} (line {line}):\n  want: {}\n  got:  {}",
1093                context(&want),
1094                context(&got)
1095            );
1096        }
1097    }
1098
1099    /// Prints the fixture's report, for checking a prediction of the golden:
1100    /// `cargo test -p frazaro-core print_refers_report -- --ignored --nocapture`.
1101    #[test]
1102    #[ignore]
1103    fn print_refers_report() {
1104        print!("{}", fixture_report(FIXTURE));
1105    }
1106
1107    #[test]
1108    fn the_record_carries_its_fields() {
1109        let refs = scan("='Q1 Data'!$A$1:B2");
1110        assert_eq!(refs.len(), 1);
1111        let r = &refs[0];
1112        assert_eq!((r.kind, r.shape), (Kind::Range, Shape::Range));
1113        assert_eq!((r.sheet_name.as_str(), r.book.as_str()), ("Q1 Data", ""));
1114        assert_eq!(r.part, "$A$1:B2");
1115        assert_eq!((r.row1, r.col1, r.row2, r.col2), (1, 1, 2, 2));
1116        assert_eq!(
1117            (r.row_abs1, r.col_abs1, r.row_abs2, r.col_abs2),
1118            (true, true, false, false)
1119        );
1120        assert_eq!((r.pos, r.span, r.part_pos), (1, 17, 11));
1121        let refs = scan("=#REF!A1");
1122        assert_eq!((refs[0].kind, refs[0].shape), (Kind::Broken, Shape::Cell));
1123        assert_eq!((refs[0].part.as_str(), refs[0].part_pos), ("A1", 6));
1124        let refs = scan("=INDIRECT(A1)");
1125        assert_eq!(refs.len(), 2);
1126        assert_eq!(refs[0].kind, Kind::Unreadable);
1127        assert_eq!(refs[0].unreadable_name(), "INDIRECT");
1128        assert_eq!(refs[1].kind, Kind::Cell);
1129        assert_eq!(scan("A1+B1").len(), 2);
1130        assert!(scan("=1+1").is_empty());
1131    }
1132
1133    #[test]
1134    fn the_home_cell_parses() {
1135        assert_eq!(parse_home("Model!B3"), Some(("Model".to_string(), 3, 2)));
1136        assert_eq!(
1137            parse_home("'Q1 Data'!C5"),
1138            Some(("Q1 Data".to_string(), 5, 3))
1139        );
1140        assert_eq!(parse_home("B3"), None);
1141        assert_eq!(parse_home("Model!A:A"), None);
1142        assert_eq!(parse_home("Model!B3+1"), None);
1143    }
1144
1145    #[test]
1146    fn a_sheet_is_quoted_as_a_reference_quotes_it() {
1147        assert_eq!(quote_sheet("Data"), "Data");
1148        assert_eq!(quote_sheet("Q1 Data"), "'Q1 Data'");
1149        assert_eq!(quote_sheet("It's"), "'It''s'");
1150        assert_eq!(quote_sheet("A1"), "'A1'");
1151        assert_eq!(quote_sheet("2024"), "'2024'");
1152        assert_eq!(quote_sheet("Q1.Data"), "Q1.Data");
1153        assert_eq!(quote_sheet("R1C1"), "'R1C1'");
1154        assert_eq!(quote_sheet("RC"), "'RC'");
1155        assert_eq!(quote_sheet("Tax_Rate"), "Tax_Rate");
1156        assert_eq!(quote_sheet("Umsatz\u{d6}"), "Umsatz\u{d6}");
1157        assert_eq!(quote_sheet(""), "''");
1158    }
1159
1160    #[test]
1161    fn a_name_past_ascii_is_a_name_and_a_sheet_past_ascii_is_a_sheet() {
1162        let refs = scan("=Umsatz_\u{d6}*2");
1163        assert_eq!((refs.len(), refs[0].kind), (1, Kind::Name));
1164        assert_eq!(refs[0].written, "Umsatz_\u{d6}");
1165        let refs = scan("=\u{dc}bersicht!A1");
1166        assert_eq!(
1167            (refs[0].kind, refs[0].sheet_name.as_str()),
1168            (Kind::Cell, "\u{dc}bersicht")
1169        );
1170        assert_eq!(spell(&refs[0], "Model"), "\u{dc}bersicht!A1");
1171        assert_eq!(
1172            r1c1("=\u{dc}bersicht!A1", 3, 2),
1173            "=\u{dc}bersicht!R[-2]C[-1]"
1174        );
1175    }
1176
1177    #[test]
1178    fn books_resolve_to_their_file_names() {
1179        let books = vec!["Rates.xlsx".to_string(), String::new()];
1180        assert_eq!(
1181            resolve_books("=[1]Sheet1!A1+[1]!Rate", &books),
1182            "=[Rates.xlsx]Sheet1!A1+[Rates.xlsx]!Rate"
1183        );
1184        assert_eq!(
1185            resolve_books("='[1]Q1 Data'!A1", &books),
1186            "='[Rates.xlsx]Q1 Data'!A1"
1187        );
1188        assert_eq!(resolve_books("=[2]Sheet1!A1", &books), "=[2]Sheet1!A1");
1189        assert_eq!(resolve_books("=[3]Sheet1!A1", &books), "=[3]Sheet1!A1");
1190        assert_eq!(
1191            resolve_books("=[Book.xlsx]Sheet1!A1", &books),
1192            "=[Book.xlsx]Sheet1!A1"
1193        );
1194        assert_eq!(resolve_books("=A1+[Amount]", &books), "=A1+[Amount]");
1195    }
1196
1197    // The mover's tests, as sheet/refs.rs held them (PORT.7, slice 7c).
1198    fn s(f: &str, r: i64, c: i64) -> String {
1199        shift_a1_references(f, r, c)
1200    }
1201
1202    #[test]
1203    fn relative_parts_move_and_absolute_parts_stay() {
1204        assert_eq!(s("B2+B3", 1, 0), "B3+B4");
1205        assert_eq!(s("$B$2+B$2+$B2", 1, 1), "$B$2+C$2+$B3");
1206        assert_eq!(s("SUM(A1:A3)", 0, 1), "SUM(B1:B3)");
1207        assert_eq!(
1208            s("IFS(B2>3,\"big\",TRUE,\"small\")", 2, 0),
1209            "IFS(B4>3,\"big\",TRUE,\"small\")"
1210        );
1211        assert_eq!(s("b2*2", 1, 0), "B3*2");
1212        assert_eq!(s("Z1+AA1", 0, 1), "AA1+AB1");
1213    }
1214
1215    #[test]
1216    fn whole_columns_rows_and_sheets() {
1217        assert_eq!(s("SUM(A:A)", 0, 1), "SUM(B:B)");
1218        assert_eq!(s("SUM($A:A)", 0, 1), "SUM($A:B)");
1219        assert_eq!(s("SUM(1:3)", 1, 0), "SUM(2:4)");
1220        assert_eq!(s("Data!A1+'Q1 Data'!B2", 1, 0), "Data!A2+'Q1 Data'!B3");
1221        assert_eq!(s("'It''s'!A1", 1, 0), "'It''s'!A2");
1222        assert_eq!(
1223            s("SUM(Jan:Dec!A1)+[1]Sheet1!A1+A1#", 1, 0),
1224            "SUM(Jan:Dec!A2)+[1]Sheet1!A2+A2#"
1225        );
1226    }
1227
1228    #[test]
1229    fn what_is_not_a_reference_is_left_alone() {
1230        assert_eq!(s("\"A1\"&A1", 1, 0), "\"A1\"&A2");
1231        assert_eq!(s("\"say \"\"A1\"\"\"&A1", 1, 0), "\"say \"\"A1\"\"\"&A2");
1232        assert_eq!(s("LOG10(A1)", 1, 0), "LOG10(A2)");
1233        assert_eq!(s("Table1[Col]+A1", 1, 0), "Table1[Col]+A2");
1234        assert_eq!(
1235            s("Table1[[#This Row],[A1]]", 1, 0),
1236            "Table1[[#This Row],[A1]]"
1237        );
1238        assert_eq!(s("TRUE+A1", 1, 0), "TRUE+A2");
1239        assert_eq!(s("ABC123X+A1", 1, 0), "ABC123X+A2");
1240        assert_eq!(s("Rate*A1", 1, 0), "Rate*A2");
1241        assert_eq!(s("_xlfn.IFS(A1,1)", 1, 0), "_xlfn.IFS(A2,1)");
1242        assert_eq!(s("1.5+A1", 1, 0), "1.5+A2");
1243        assert_eq!(s("A1", 0, 0), "A1");
1244        assert_eq!(s("INDIRECT(\"A1\")+A1", 1, 0), "INDIRECT(\"A1\")+A2");
1245    }
1246
1247    #[test]
1248    fn off_the_sheet_is_ref_error() {
1249        assert_eq!(s("A1", -1, 0), "#REF!");
1250        assert_eq!(s("A1+B1", 0, -1), "#REF!+A1");
1251        assert_eq!(s("XFD1", 0, 1), "#REF!");
1252        assert_eq!(s("A1:B2", -1, 0), "#REF!");
1253        assert_eq!(s("$A$1", -5, -5), "$A$1");
1254        assert_eq!(s("SUM(A:A)", 0, -1), "SUM(#REF!)");
1255    }
1256}