Skip to main content

visi_core/core/
parser.rs

1use crate::core::{CompiledFormula, FormulaPart, RefType, Sheet, SheetSection, generate_unique_id};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
4pub enum Op {
5    Add,
6    Sub,
7    Mul,
8    Div,
9    Exp,
10    Percent,
11    ImplicitIntersection,
12    Spill,
13    Concat,
14    Eq,
15    Ne,
16    Lt,
17    Gt,
18    Le,
19    Ge,
20    Intersect,
21    Union,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum Expr {
26    Number(f64),
27    String(String),
28    Boolean(bool),
29    Error(&'static str),
30    CellRef {
31        sheet: Option<String>,
32        row: usize,
33        col: usize,
34        row_abs: bool,
35        col_abs: bool,
36    },
37    RangeRef {
38        sheet: Option<String>,
39        start_row: usize,
40        start_col: usize,
41        end_row: usize,
42        end_col: usize,
43        start_row_abs: bool,
44        start_col_abs: bool,
45        end_row_abs: bool,
46        end_col_abs: bool,
47    },
48    List(Vec<Expr>),
49    Slice {
50        expr: Box<Expr>,
51        start: Option<Box<Expr>>,
52        end: Option<Box<Expr>>,
53    },
54    FunctionCall {
55        name: String,
56        args: Vec<Expr>,
57    },
58    BinaryOp {
59        op: Op,
60        left: Box<Expr>,
61        right: Box<Expr>,
62    },
63    UnaryOp {
64        op: Op,
65        expr: Box<Expr>,
66    },
67    Identifier(String),
68    StructuredRef {
69        sheet: Option<String>,
70        column: Option<String>,
71        is_this_row: bool,
72        section: SheetSection,
73    },
74}
75
76#[derive(Debug, Clone, PartialEq)]
77pub enum EvalToken {
78    Number(f64),
79    String(String),
80    Boolean(bool),
81    Identifier(String),
82    Error(&'static str),
83    Op(Op),
84    OpenParen,
85    CloseParen,
86    OpenBracket,
87    CloseBracket,
88    Comma,
89    Colon,
90    Exclamation,
91    Dot,
92    StructuredRef {
93        sheet: Option<String>,
94        column: Option<String>,
95        is_this_row: bool,
96        section: SheetSection,
97    },
98}
99
100/// Renders a 0-based column index in its letter form: 0 is `A`, 25 is
101/// `Z`, 26 is `AA`.
102pub fn col_idx_to_letters(mut col: usize) -> String {
103    let mut letters = String::new();
104    loop {
105        let remainder = col % 26;
106        letters.insert(0, (b'A' + remainder as u8) as char);
107        if col < 26 {
108            break;
109        }
110        col = col / 26 - 1;
111    }
112    letters
113}
114
115/// Convert Excel letter notation (e.g. "A", "Z", "AA")
116/// to 0-based column index. Clamps result to 0 if input is invalid.
117pub fn col_letters_to_idx(col_str: &str) -> usize {
118    let mut col = 0;
119    for c in col_str.chars() {
120        if c.is_ascii_alphabetic() {
121            col = col * 26 + (c.to_ascii_uppercase() as usize - 'A' as usize + 1);
122        }
123    }
124    if col > 0 { col - 1 } else { 0 }
125}
126
127/// Converts an already-split A1 reference into a 0-based `(row, col)`.
128///
129/// `col_str` is the letters and `row_str` the digits, so `("B", "3")` gives
130/// `(2, 1)`. Case-insensitive, and non-alphabetic characters in `col_str` are
131/// skipped.
132///
133/// Clamps to index 0 instead of failing.
134pub fn parse_a1_coordinates(col_str: &str, row_str: &str) -> (usize, usize) {
135    let col_idx = col_letters_to_idx(col_str);
136
137    let row_val: usize = row_str.parse().unwrap_or(1);
138    let row_idx = if row_val > 0 { row_val - 1 } else { 0 };
139
140    (row_idx, col_idx)
141}
142
143/// Parse a cell reference string like "A1", "C10", or "Sheet1!B5"
144/// Returns (optional_sheet_name, row_idx, col_idx)
145pub fn parse_cell_ref(cell_str: &str) -> Result<(Option<String>, usize, usize), String> {
146    let trimmed = cell_str.trim();
147    if trimmed.is_empty() {
148        return Err("Cell reference cannot be empty".to_string());
149    }
150
151    let (sheet_part, cell_part) = split_sheet_reference(trimmed);
152    let (row_idx, col_idx, _, _) = parse_cell_ref_parts(cell_part)
153        .ok_or_else(|| format!("Invalid cell reference format: '{}'", cell_str))?;
154
155    Ok((sheet_part, row_idx, col_idx))
156}
157
158/// Parse a range reference string (e.g."A1:C10", "Sheet1!A1:B5", or "A1")
159/// Returns (optional_sheet_name, start_row, start_col, end_row, end_col)
160pub fn parse_range_ref(
161    range_str: &str,
162) -> Result<(Option<String>, usize, usize, usize, usize), String> {
163    let trimmed = range_str.trim();
164    if trimmed.is_empty() {
165        return Err("Range reference cannot be empty".to_string());
166    }
167
168    let (sheet_part, range_part) = split_sheet_reference(trimmed);
169
170    if let Some((start_str, end_str)) = range_part.split_once(':') {
171        let (_, start_row, start_col) = parse_cell_ref(start_str)?;
172        let (_, end_row, end_col) = parse_cell_ref(end_str)?;
173
174        Ok((
175            sheet_part,
176            start_row.min(end_row),
177            start_col.min(end_col),
178            start_row.max(end_row),
179            start_col.max(end_col),
180        ))
181    } else {
182        let (_, row_idx, col_idx) = parse_cell_ref(range_part)?;
183        Ok((sheet_part, row_idx, col_idx, row_idx, col_idx))
184    }
185}
186
187fn split_sheet_reference(reference: &str) -> (Option<String>, &str) {
188    if let Some(pos) = reference.rfind('!') {
189        (
190            Some(reference[..pos].trim_matches('\'').to_string()),
191            &reference[pos + 1..],
192        )
193    } else {
194        (None, reference)
195    }
196}
197
198fn parse_cell_ref_parts(s: &str) -> Option<(usize, usize, bool, bool)> {
199    let chars: Vec<char> = s.chars().collect();
200    let mut idx = 0;
201
202    let mut col_abs = false;
203    if idx < chars.len() && chars[idx] == '$' {
204        col_abs = true;
205        idx += 1;
206    }
207
208    let mut col_str = String::new();
209    while idx < chars.len() && chars[idx].is_ascii_alphabetic() {
210        col_str.push(chars[idx]);
211        idx += 1;
212    }
213    if col_str.is_empty() || col_str.len() > 3 {
214        return None;
215    }
216
217    let mut row_abs = false;
218    if idx < chars.len() && chars[idx] == '$' {
219        row_abs = true;
220        idx += 1;
221    }
222
223    let mut row_str = String::new();
224    while idx < chars.len() && chars[idx].is_ascii_digit() {
225        row_str.push(chars[idx]);
226        idx += 1;
227    }
228    if row_str.is_empty() {
229        return None;
230    }
231    if row_str.parse::<usize>().ok()? == 0 {
232        return None;
233    }
234
235    if idx < chars.len() {
236        return None;
237    }
238
239    let (row_idx, col_idx) = parse_a1_coordinates(&col_str, &row_str);
240    Some((row_idx, col_idx, row_abs, col_abs))
241}
242
243fn parse_column_ref(s: &str) -> Option<(usize, bool)> {
244    let chars: Vec<char> = s.chars().collect();
245    let mut idx = 0;
246    let mut col_abs = false;
247    if idx < chars.len() && chars[idx] == '$' {
248        col_abs = true;
249        idx += 1;
250    }
251    let mut col_str = String::new();
252    while idx < chars.len() && chars[idx].is_ascii_alphabetic() {
253        col_str.push(chars[idx]);
254        idx += 1;
255    }
256    if col_str.is_empty() || col_str.len() > 3 {
257        return None;
258    }
259    if idx < chars.len() {
260        return None;
261    }
262    let mut col = 0;
263    for c in col_str.chars() {
264        col = col * 26 + (c.to_ascii_uppercase() as usize - 'A' as usize + 1);
265    }
266    let col_idx = if col > 0 { col - 1 } else { 0 };
267    Some((col_idx, col_abs))
268}
269
270fn parse_row_ref(s: &str) -> Option<(usize, bool)> {
271    let chars: Vec<char> = s.chars().collect();
272    let mut idx = 0;
273    let mut row_abs = false;
274    if idx < chars.len() && chars[idx] == '$' {
275        row_abs = true;
276        idx += 1;
277    }
278    let mut row_str = String::new();
279    while idx < chars.len() && chars[idx].is_ascii_digit() {
280        row_str.push(chars[idx]);
281        idx += 1;
282    }
283    if row_str.is_empty() || idx < chars.len() {
284        return None;
285    }
286    let row_val: usize = row_str.parse().ok()?;
287    Some((row_val.saturating_sub(1), row_abs))
288}
289
290fn parse_cell_pattern(chars: &[char], mut idx: usize) -> Option<(usize, usize, bool, bool, usize)> {
291    let mut col_abs = false;
292    if idx < chars.len() && chars[idx] == '$' {
293        col_abs = true;
294        idx += 1;
295    }
296
297    let mut col_str = String::new();
298    while idx < chars.len() && chars[idx].is_ascii_alphabetic() {
299        col_str.push(chars[idx]);
300        idx += 1;
301    }
302    if col_str.is_empty() || col_str.len() > 3 {
303        return None;
304    }
305
306    let mut row_abs = false;
307    if idx < chars.len() && chars[idx] == '$' {
308        row_abs = true;
309        idx += 1;
310    }
311
312    let mut row_str = String::new();
313    while idx < chars.len() && chars[idx].is_ascii_digit() {
314        row_str.push(chars[idx]);
315        idx += 1;
316    }
317    if row_str.is_empty() {
318        return None;
319    }
320
321    if idx < chars.len() && (chars[idx].is_ascii_alphabetic() || chars[idx] == '_') {
322        return None;
323    }
324
325    let (row_idx, col_idx) = parse_a1_coordinates(&col_str, &row_str);
326    Some((row_idx, col_idx, row_abs, col_abs, idx))
327}
328
329fn parse_col_pattern(chars: &[char], mut idx: usize) -> Option<(usize, bool, usize)> {
330    let mut col_abs = false;
331    if idx < chars.len() && chars[idx] == '$' {
332        col_abs = true;
333        idx += 1;
334    }
335    let mut col_str = String::new();
336    while idx < chars.len() && chars[idx].is_ascii_alphabetic() {
337        col_str.push(chars[idx]);
338        idx += 1;
339    }
340    if col_str.is_empty() || col_str.len() > 3 {
341        return None;
342    }
343    if idx < chars.len() && chars[idx].is_ascii_digit() {
344        return None;
345    }
346    if idx < chars.len() && (chars[idx].is_ascii_alphabetic() || chars[idx] == '_') {
347        return None;
348    }
349    let mut col = 0;
350    for c in col_str.chars() {
351        col = col * 26 + (c.to_ascii_uppercase() as usize - 'A' as usize + 1);
352    }
353    let col_idx = if col > 0 { col - 1 } else { 0 };
354    Some((col_idx, col_abs, idx))
355}
356
357fn parse_row_pattern(chars: &[char], mut idx: usize) -> Option<(usize, bool, usize)> {
358    let mut row_abs = false;
359    if idx < chars.len() && chars[idx] == '$' {
360        row_abs = true;
361        idx += 1;
362    }
363    let mut row_str = String::new();
364    while idx < chars.len() && chars[idx].is_ascii_digit() {
365        row_str.push(chars[idx]);
366        idx += 1;
367    }
368    if row_str.is_empty() {
369        return None;
370    }
371    if idx < chars.len() && (chars[idx].is_ascii_alphabetic() || chars[idx] == '_') {
372        return None;
373    }
374    let row_val: usize = row_str.parse().ok()?;
375    Some((row_val.saturating_sub(1), row_abs, idx))
376}
377
378#[derive(Debug, Clone)]
379enum FoundRef {
380    Cell {
381        sheet: Option<String>,
382        row: usize,
383        col: usize,
384        row_abs: bool,
385        col_abs: bool,
386    },
387    Range {
388        sheet: Option<String>,
389        start_row: usize,
390        start_col: usize,
391        end_row: usize,
392        end_col: usize,
393        start_row_abs: bool,
394        start_col_abs: bool,
395        end_row_abs: bool,
396        end_col_abs: bool,
397    },
398    Structured {
399        sheet: Option<String>,
400        column: Option<String>,
401        is_this_row: bool,
402        section: SheetSection,
403    },
404}
405
406fn parse_bracketed_term(chars: &[char], mut idx: usize) -> Option<(String, usize)> {
407    if idx >= chars.len() || chars[idx] != '[' {
408        return None;
409    }
410    idx += 1;
411    while idx < chars.len() && chars[idx].is_whitespace() {
412        idx += 1;
413    }
414    let mut quote = None;
415    if idx < chars.len() && (chars[idx] == '"' || chars[idx] == '\'') {
416        quote = Some(chars[idx]);
417        idx += 1;
418    }
419    let mut term = String::new();
420    while idx < chars.len() {
421        if let Some(q) = quote {
422            if chars[idx] == q {
423                idx += 1;
424                break;
425            }
426        } else {
427            if chars[idx] == ']' {
428                break;
429            }
430            if chars[idx] == ','
431                || chars[idx] == ':'
432                || chars[idx] == '+'
433                || chars[idx] == '-'
434                || chars[idx] == '*'
435                || chars[idx] == '/'
436            {
437                return None;
438            }
439        }
440        term.push(chars[idx]);
441        idx += 1;
442    }
443    while idx < chars.len() && chars[idx].is_whitespace() {
444        idx += 1;
445    }
446    if idx < chars.len() && chars[idx] == ']' {
447        Some((term.trim().to_string(), idx + 1))
448    } else {
449        None
450    }
451}
452
453fn parse_structured_specifier(
454    chars: &[char],
455    mut idx: usize,
456) -> Option<(Option<String>, bool, SheetSection, usize)> {
457    if idx >= chars.len() || chars[idx] != '[' {
458        return None;
459    }
460    idx += 1;
461
462    while idx < chars.len() && chars[idx].is_whitespace() {
463        idx += 1;
464    }
465
466    let mut is_this_row = false;
467    let mut section = SheetSection::Data;
468    let mut column = None;
469
470    if idx < chars.len() && chars[idx] == '@' {
471        is_this_row = true;
472        idx += 1;
473        if idx < chars.len() && chars[idx] == '[' {
474            if let Some((col_name, next_idx)) = parse_bracketed_term(chars, idx) {
475                if !col_name.is_empty() {
476                    column = Some(col_name);
477                }
478                idx = next_idx;
479            } else {
480                return None;
481            }
482        } else {
483            let mut col_name = String::new();
484            while idx < chars.len() && chars[idx] != ']' {
485                col_name.push(chars[idx]);
486                idx += 1;
487            }
488            let trimmed = col_name.trim().to_string();
489            column = if trimmed.is_empty() {
490                None
491            } else {
492                Some(trimmed)
493            };
494        }
495        if idx < chars.len() && chars[idx] == ']' {
496            return Some((column, is_this_row, section, idx + 1));
497        } else {
498            return None;
499        }
500    }
501
502    if idx < chars.len() && chars[idx] == '[' {
503        if let Some((term1, next_idx)) = parse_bracketed_term(chars, idx) {
504            idx = next_idx;
505            if term1.starts_with('#') {
506                match term1.as_str() {
507                    "#This Row" => is_this_row = true,
508                    "#Headers" => section = SheetSection::Headers,
509                    "#Totals" => section = SheetSection::Totals,
510                    "#Data" => section = SheetSection::Data,
511                    "#All" => section = SheetSection::All,
512                    _ => {}
513                }
514                while idx < chars.len() && (chars[idx].is_whitespace() || chars[idx] == ',') {
515                    idx += 1;
516                }
517                if idx < chars.len() && chars[idx] == '[' {
518                    if let Some((col_name, next_idx2)) = parse_bracketed_term(chars, idx) {
519                        if !col_name.is_empty() {
520                            column = Some(col_name);
521                        }
522                        idx = next_idx2;
523                    } else {
524                        return None;
525                    }
526                }
527            } else if !term1.is_empty() {
528                column = Some(term1);
529            }
530        } else {
531            return None;
532        }
533        while idx < chars.len() && chars[idx].is_whitespace() {
534            idx += 1;
535        }
536        if idx < chars.len() && chars[idx] == ']' {
537            return Some((column, is_this_row, section, idx + 1));
538        } else {
539            return None;
540        }
541    }
542
543    let mut term = String::new();
544    let mut quote = None;
545    if idx < chars.len() && (chars[idx] == '"' || chars[idx] == '\'') {
546        quote = Some(chars[idx]);
547        idx += 1;
548    }
549    while idx < chars.len() {
550        if let Some(q) = quote {
551            if chars[idx] == q {
552                idx += 1;
553                break;
554            }
555        } else {
556            if chars[idx] == ']' {
557                break;
558            }
559            if chars[idx] == ','
560                || chars[idx] == ':'
561                || chars[idx] == '+'
562                || chars[idx] == '-'
563                || chars[idx] == '*'
564                || chars[idx] == '/'
565            {
566                return None;
567            }
568        }
569        term.push(chars[idx]);
570        idx += 1;
571    }
572    term = term.trim().to_string();
573    if term.starts_with('#') {
574        match term.as_str() {
575            "#This Row" => is_this_row = true,
576            "#Headers" => section = SheetSection::Headers,
577            "#Totals" => section = SheetSection::Totals,
578            "#Data" => section = SheetSection::Data,
579            "#All" => section = SheetSection::All,
580            _ => {}
581        }
582    } else if !term.is_empty() {
583        column = Some(term);
584    }
585
586    if idx < chars.len() && chars[idx] == ']' {
587        return Some((column, is_this_row, section, idx + 1));
588    }
589
590    None
591}
592
593fn try_parse_ref(chars: &[char], start_idx: usize) -> Option<(FoundRef, usize)> {
594    if start_idx > 0 {
595        let prev = chars[start_idx - 1];
596        if prev.is_ascii_alphanumeric() || prev == '_' {
597            return None;
598        }
599    }
600    let mut idx = start_idx;
601    let mut sheet = None;
602
603    let mut has_explicit_table = false;
604    if idx < chars.len() && chars[idx] == '\'' {
605        let mut t_name = String::new();
606        let mut j = idx + 1;
607        while j < chars.len() && chars[j] != '\'' {
608            t_name.push(chars[j]);
609            j += 1;
610        }
611        if j < chars.len() && chars[j] == '\'' {
612            if j + 1 < chars.len() && chars[j + 1] == '!' {
613                sheet = Some(t_name);
614                idx = j + 2;
615                has_explicit_table = true;
616            } else if j + 1 < chars.len() && chars[j + 1] == '[' {
617                sheet = Some(t_name);
618                idx = j + 1;
619                has_explicit_table = true;
620            }
621        }
622    }
623
624    if !has_explicit_table {
625        let mut t_name = String::new();
626        let mut j = idx;
627        if j < chars.len() && (chars[j].is_ascii_alphabetic() || chars[j] == '_') {
628            while j < chars.len()
629                && (chars[j].is_ascii_alphanumeric() || chars[j] == '_' || chars[j] == '.')
630            {
631                t_name.push(chars[j]);
632                j += 1;
633            }
634            if j < chars.len() && chars[j] == '!' {
635                if t_name != "iloc" && t_name != "get" {
636                    sheet = Some(t_name);
637                    idx = j + 1;
638                }
639            } else if j < chars.len() && chars[j] == '[' && t_name != "iloc" && t_name != "get" {
640                let mut k = j + 1;
641                let mut quote = None;
642                if k < chars.len() && (chars[k] == '"' || chars[k] == '\'') {
643                    quote = Some(chars[k]);
644                    k += 1;
645                }
646                let mut is_column = true;
647                let mut has_chars = false;
648                while k < chars.len() {
649                    if let Some(q) = quote {
650                        if chars[k] == q {
651                            break;
652                        }
653                    } else {
654                        if chars[k] == ']' {
655                            break;
656                        }
657                        if chars[k] == ':'
658                            || chars[k] == ','
659                            || chars[k] == '+'
660                            || chars[k] == '-'
661                            || chars[k] == '*'
662                            || chars[k] == '/'
663                        {
664                            is_column = false;
665                            break;
666                        }
667                    }
668                    has_chars = true;
669                    k += 1;
670                }
671                if is_column && has_chars {
672                    sheet = Some(t_name);
673                    idx = j;
674                }
675            }
676        }
677    }
678
679    if idx < chars.len()
680        && chars[idx] == '['
681        && let Some((column, is_this_row, section, next_idx)) =
682            parse_structured_specifier(chars, idx)
683    {
684        return Some((
685            FoundRef::Structured {
686                sheet,
687                column,
688                is_this_row,
689                section,
690            },
691            next_idx,
692        ));
693    }
694
695    if let Some((start_col, start_col_abs, next_idx)) = parse_col_pattern(chars, idx)
696        && next_idx < chars.len()
697        && chars[next_idx] == ':'
698        && let Some((end_col, end_col_abs, end_idx)) = parse_col_pattern(chars, next_idx + 1)
699    {
700        return Some((
701            FoundRef::Range {
702                sheet: sheet.clone(),
703                start_row: 0,
704                start_col,
705                end_row: usize::MAX,
706                end_col,
707                start_row_abs: true,
708                start_col_abs,
709                end_row_abs: true,
710                end_col_abs,
711            },
712            end_idx,
713        ));
714    }
715
716    if let Some((start_row, start_row_abs, next_idx)) = parse_row_pattern(chars, idx)
717        && next_idx < chars.len()
718        && chars[next_idx] == ':'
719        && let Some((end_row, end_row_abs, end_idx)) = parse_row_pattern(chars, next_idx + 1)
720    {
721        return Some((
722            FoundRef::Range {
723                sheet,
724                start_row,
725                start_col: 0,
726                end_row,
727                end_col: usize::MAX,
728                start_row_abs,
729                start_col_abs: true,
730                end_row_abs,
731                end_col_abs: true,
732            },
733            end_idx,
734        ));
735    }
736
737    if let Some((start_row, start_col, start_row_abs, start_col_abs, next_idx)) =
738        parse_cell_pattern(chars, idx)
739    {
740        if next_idx < chars.len()
741            && chars[next_idx] == ':'
742            && let Some((end_row, end_col, end_row_abs, end_col_abs, end_idx)) =
743                parse_cell_pattern(chars, next_idx + 1)
744        {
745            return Some((
746                FoundRef::Range {
747                    sheet,
748                    start_row,
749                    start_col,
750                    end_row,
751                    end_col,
752                    start_row_abs,
753                    start_col_abs,
754                    end_row_abs,
755                    end_col_abs,
756                },
757                end_idx,
758            ));
759        }
760
761        return Some((
762            FoundRef::Cell {
763                sheet,
764                row: start_row,
765                col: start_col,
766                row_abs: start_row_abs,
767                col_abs: start_col_abs,
768            },
769            next_idx,
770        ));
771    }
772
773    None
774}
775
776pub(crate) fn encode_xlsx_spill_references(code: &str) -> String {
777    if !code.contains('#') {
778        return code.to_string();
779    }
780    let chars: Vec<char> = code.chars().collect();
781    let mut out = String::with_capacity(code.len());
782    let mut i = 0;
783    while i < chars.len() {
784        if let Some((_, end)) = try_parse_ref(&chars, i) {
785            let reference: String = chars[i..end].iter().collect();
786            if chars.get(end) == Some(&'#') {
787                out.push_str("_xlfn.ANCHORARRAY(");
788                out.push_str(&reference);
789                out.push(')');
790                i = end + 1;
791            } else {
792                out.push_str(&reference);
793                i = end;
794            }
795        } else if matches!(chars[i], '"' | '\'') {
796            let quote = chars[i];
797            out.push(quote);
798            i += 1;
799            while i < chars.len() {
800                let c = chars[i];
801                out.push(c);
802                i += 1;
803                if c == quote {
804                    if chars.get(i) == Some(&quote) {
805                        out.push(quote);
806                        i += 1;
807                    } else {
808                        break;
809                    }
810                }
811            }
812        } else {
813            out.push(chars[i]);
814            i += 1;
815        }
816    }
817    out
818}
819
820pub fn compile_formula(code: &str, sheets: &[Sheet]) -> CompiledFormula {
821    let chars: Vec<char> = code.chars().collect();
822    let mut parts = Vec::new();
823    let mut last_idx = 0;
824
825    let mut in_quote = None;
826    let mut i = 0;
827    while i < chars.len() {
828        let c = chars[i];
829        if let Some(q) = in_quote {
830            if c == '\\' {
831                i = (i + 2).min(chars.len());
832                continue;
833            } else if c == q {
834                in_quote = None;
835            }
836            i += 1;
837            continue;
838        }
839
840        if let Some((found_ref, next_i)) = try_parse_ref(&chars, i) {
841            if i > last_idx {
842                let text: String = chars[last_idx..i].iter().collect();
843                parts.push(FormulaPart::Text(text));
844            }
845
846            match found_ref {
847                FoundRef::Cell {
848                    sheet,
849                    row,
850                    col,
851                    row_abs,
852                    col_abs,
853                } => {
854                    let sheet_id = if let Some(ref name) = sheet {
855                        if let Some(t) = sheets.iter().find(|t| t.name == *name) {
856                            t.id
857                        } else if !sheets.is_empty() {
858                            sheets[0].id
859                        } else {
860                            0
861                        }
862                    } else if !sheets.is_empty() {
863                        sheets[0].id
864                    } else {
865                        0
866                    };
867
868                    parts.push(FormulaPart::SheetReference {
869                        sheet_id,
870                        row,
871                        col,
872                        row_ref_type: if row_abs {
873                            RefType::Absolute
874                        } else {
875                            RefType::Relative
876                        },
877                        col_ref_type: if col_abs {
878                            RefType::Absolute
879                        } else {
880                            RefType::Relative
881                        },
882                    });
883                }
884                FoundRef::Range {
885                    sheet,
886                    start_row,
887                    start_col,
888                    end_row,
889                    end_col,
890                    start_row_abs,
891                    start_col_abs,
892                    end_row_abs,
893                    end_col_abs,
894                } => {
895                    let sheet_id = if let Some(ref name) = sheet {
896                        if let Some(t) = sheets.iter().find(|t| t.name == *name) {
897                            t.id
898                        } else if !sheets.is_empty() {
899                            sheets[0].id
900                        } else {
901                            0
902                        }
903                    } else if !sheets.is_empty() {
904                        sheets[0].id
905                    } else {
906                        0
907                    };
908
909                    parts.push(FormulaPart::RangeReference {
910                        sheet_id,
911                        start_row,
912                        start_col,
913                        end_row,
914                        end_col,
915                        start_row_ref_type: if start_row_abs {
916                            RefType::Absolute
917                        } else {
918                            RefType::Relative
919                        },
920                        start_col_ref_type: if start_col_abs {
921                            RefType::Absolute
922                        } else {
923                            RefType::Relative
924                        },
925                        end_row_ref_type: if end_row_abs {
926                            RefType::Absolute
927                        } else {
928                            RefType::Relative
929                        },
930                        end_col_ref_type: if end_col_abs {
931                            RefType::Absolute
932                        } else {
933                            RefType::Relative
934                        },
935                    });
936                }
937                FoundRef::Structured {
938                    sheet,
939                    column,
940                    is_this_row,
941                    section,
942                } => {
943                    let default_sheet_name = sheets.first().map(|s| s.name.clone());
944                    let ref_name = sheet.clone().or(default_sheet_name);
945                    let matches_real_table = ref_name.is_some_and(|name| {
946                        sheets
947                            .iter()
948                            .any(|s| s.tables.iter().any(|t| t.name.eq_ignore_ascii_case(&name)))
949                    });
950
951                    if matches_real_table {
952                        let text: String = chars[i..next_i].iter().collect();
953                        parts.push(FormulaPart::Text(text));
954                    } else {
955                        let sheet_id = if let Some(ref name) = sheet {
956                            if let Some(named_sheet) = sheets.iter().find(|s| s.name == *name) {
957                                named_sheet.id
958                            } else if !sheets.is_empty() {
959                                sheets[0].id
960                            } else {
961                                0
962                            }
963                        } else if !sheets.is_empty() {
964                            sheets[0].id
965                        } else {
966                            0
967                        };
968
969                        let col_id = if let Some(ref col_name) = column {
970                            if let Some(sheet_obj) = sheets.iter().find(|s| s.id == sheet_id) {
971                                if let Some(col) =
972                                    sheet_obj.columns.iter().find(|c| c.name == *col_name)
973                                {
974                                    Some(col.id)
975                                } else {
976                                    Some(generate_unique_id())
977                                }
978                            } else {
979                                Some(generate_unique_id())
980                            }
981                        } else {
982                            None
983                        };
984
985                        parts.push(FormulaPart::StructuredReference {
986                            sheet_id,
987                            col_id,
988                            is_this_row,
989                            section,
990                        });
991                    }
992                }
993            }
994
995            i = next_i;
996            last_idx = i;
997        } else {
998            if c == '"' || c == '\'' {
999                in_quote = Some(c);
1000            }
1001            i += 1;
1002        }
1003    }
1004
1005    if i > last_idx {
1006        let text: String = chars[last_idx..i].iter().collect();
1007        parts.push(FormulaPart::Text(text));
1008    }
1009
1010    CompiledFormula { parts }
1011}
1012
1013pub fn serialize_formula(formula: &CompiledFormula, sheets: &[Sheet]) -> String {
1014    let get_sheet_name = |sheet_id: u64, sheets: &[Sheet]| -> String {
1015        if let Some(sheet) = sheets.iter().find(|s| s.id == sheet_id) {
1016            if sheet.name.contains(' ') {
1017                format!("'{}'", sheet.name)
1018            } else {
1019                sheet.name.clone()
1020            }
1021        } else {
1022            "table_deleted".to_string()
1023        }
1024    };
1025
1026    let mut result = String::new();
1027    for part in &formula.parts {
1028        match part {
1029            FormulaPart::Text(s) => result.push_str(s),
1030            FormulaPart::SheetReference {
1031                sheet_id,
1032                row,
1033                col,
1034                row_ref_type,
1035                col_ref_type,
1036            } => {
1037                let col_letter = col_idx_to_letters(*col);
1038                let r_prefix = match row_ref_type {
1039                    RefType::Absolute => "$",
1040                    RefType::Relative => "",
1041                };
1042                let c_prefix = match col_ref_type {
1043                    RefType::Absolute => "$",
1044                    RefType::Relative => "",
1045                };
1046
1047                let has_prefix = if sheets.is_empty() {
1048                    false
1049                } else {
1050                    *sheet_id != sheets[0].id
1051                };
1052
1053                if has_prefix {
1054                    let sheet_name = get_sheet_name(*sheet_id, sheets);
1055                    result.push_str(&format!(
1056                        "{}!{}{}{}{}",
1057                        sheet_name,
1058                        c_prefix,
1059                        col_letter,
1060                        r_prefix,
1061                        row + 1
1062                    ));
1063                } else {
1064                    result.push_str(&format!(
1065                        "{}{}{}{}",
1066                        c_prefix,
1067                        col_letter,
1068                        r_prefix,
1069                        row + 1
1070                    ));
1071                }
1072            }
1073            FormulaPart::RangeReference {
1074                sheet_id,
1075                start_row,
1076                start_col,
1077                end_row,
1078                end_col,
1079                start_row_ref_type,
1080                start_col_ref_type,
1081                end_row_ref_type,
1082                end_col_ref_type,
1083            } => {
1084                let has_prefix = if sheets.is_empty() {
1085                    false
1086                } else {
1087                    *sheet_id != sheets[0].id
1088                };
1089
1090                if *end_row == usize::MAX {
1091                    let start_col_letter = col_idx_to_letters(*start_col);
1092                    let end_col_letter = col_idx_to_letters(*end_col);
1093                    let sc_prefix = match start_col_ref_type {
1094                        RefType::Absolute => "$",
1095                        RefType::Relative => "",
1096                    };
1097                    let ec_prefix = match end_col_ref_type {
1098                        RefType::Absolute => "$",
1099                        RefType::Relative => "",
1100                    };
1101                    if has_prefix {
1102                        let sheet_name = get_sheet_name(*sheet_id, sheets);
1103                        result.push_str(&format!(
1104                            "{}!{}{}:{}{}",
1105                            sheet_name, sc_prefix, start_col_letter, ec_prefix, end_col_letter,
1106                        ));
1107                    } else {
1108                        result.push_str(&format!(
1109                            "{}{}:{}{}",
1110                            sc_prefix, start_col_letter, ec_prefix, end_col_letter,
1111                        ));
1112                    }
1113                } else if *end_col == usize::MAX {
1114                    let sr_prefix = match start_row_ref_type {
1115                        RefType::Absolute => "$",
1116                        RefType::Relative => "",
1117                    };
1118                    let er_prefix = match end_row_ref_type {
1119                        RefType::Absolute => "$",
1120                        RefType::Relative => "",
1121                    };
1122                    if has_prefix {
1123                        let sheet_name = get_sheet_name(*sheet_id, sheets);
1124                        result.push_str(&format!(
1125                            "{}!{}{}:{}{}",
1126                            sheet_name,
1127                            sr_prefix,
1128                            start_row + 1,
1129                            er_prefix,
1130                            end_row + 1,
1131                        ));
1132                    } else {
1133                        result.push_str(&format!(
1134                            "{}{}:{}{}",
1135                            sr_prefix,
1136                            start_row + 1,
1137                            er_prefix,
1138                            end_row + 1,
1139                        ));
1140                    }
1141                } else {
1142                    let start_col_letter = col_idx_to_letters(*start_col);
1143                    let end_col_letter = col_idx_to_letters(*end_col);
1144                    let sc_prefix = match start_col_ref_type {
1145                        RefType::Absolute => "$",
1146                        RefType::Relative => "",
1147                    };
1148                    let ec_prefix = match end_col_ref_type {
1149                        RefType::Absolute => "$",
1150                        RefType::Relative => "",
1151                    };
1152                    let sr_prefix = match start_row_ref_type {
1153                        RefType::Absolute => "$",
1154                        RefType::Relative => "",
1155                    };
1156                    let er_prefix = match end_row_ref_type {
1157                        RefType::Absolute => "$",
1158                        RefType::Relative => "",
1159                    };
1160
1161                    if has_prefix {
1162                        let sheet_name = get_sheet_name(*sheet_id, sheets);
1163                        result.push_str(&format!(
1164                            "{}!{}{}{}{}:{}{}{}{}",
1165                            sheet_name,
1166                            sc_prefix,
1167                            start_col_letter,
1168                            sr_prefix,
1169                            start_row + 1,
1170                            ec_prefix,
1171                            end_col_letter,
1172                            er_prefix,
1173                            end_row + 1,
1174                        ));
1175                    } else {
1176                        result.push_str(&format!(
1177                            "{}{}{}{}:{}{}{}{}",
1178                            sc_prefix,
1179                            start_col_letter,
1180                            sr_prefix,
1181                            start_row + 1,
1182                            ec_prefix,
1183                            end_col_letter,
1184                            er_prefix,
1185                            end_row + 1,
1186                        ));
1187                    }
1188                }
1189            }
1190            FormulaPart::ColumnReference { sheet_id, col_id } => {
1191                let sheet_name = get_sheet_name(*sheet_id, sheets);
1192                let col_name = if let Some(sheet) = sheets.iter().find(|s| s.id == *sheet_id) {
1193                    if let Some(col) = sheet.columns.iter().find(|c| c.id == *col_id) {
1194                        col.name.clone()
1195                    } else {
1196                        "col_deleted".to_string()
1197                    }
1198                } else {
1199                    "col_deleted".to_string()
1200                };
1201                result.push_str(&format!("{}[{}]", sheet_name, col_name));
1202            }
1203            FormulaPart::StructuredReference {
1204                sheet_id,
1205                col_id,
1206                is_this_row,
1207                section,
1208            } => {
1209                let sheet_name = get_sheet_name(*sheet_id, sheets);
1210                let col_name = if let Some(col_id_val) = col_id {
1211                    if let Some(sheet) = sheets.iter().find(|s| s.id == *sheet_id) {
1212                        if let Some(col) = sheet.columns.iter().find(|c| c.id == *col_id_val) {
1213                            col.name.clone()
1214                        } else {
1215                            "col_deleted".to_string()
1216                        }
1217                    } else {
1218                        "col_deleted".to_string()
1219                    }
1220                } else {
1221                    String::new()
1222                };
1223
1224                let has_prefix = if sheets.is_empty() {
1225                    false
1226                } else {
1227                    *sheet_id != sheets[0].id
1228                };
1229
1230                let prefix = if has_prefix {
1231                    sheet_name
1232                } else {
1233                    String::new()
1234                };
1235
1236                result.push_str(&render_structured_ref_text(
1237                    &prefix,
1238                    &col_name,
1239                    *is_this_row,
1240                    *section,
1241                ));
1242            }
1243        }
1244    }
1245    result
1246}
1247
1248fn render_structured_ref_text(
1249    prefix: &str,
1250    col_name: &str,
1251    is_this_row: bool,
1252    section: SheetSection,
1253) -> String {
1254    if is_this_row {
1255        if col_name.is_empty() {
1256            format!("{}[@]", prefix)
1257        } else {
1258            format!("{}[@{}]", prefix, col_name)
1259        }
1260    } else {
1261        match section {
1262            SheetSection::Headers => {
1263                if col_name.is_empty() {
1264                    format!("{}[#Headers]", prefix)
1265                } else {
1266                    format!("{}[[#Headers], [{}]]", prefix, col_name)
1267                }
1268            }
1269            SheetSection::Totals => {
1270                if col_name.is_empty() {
1271                    format!("{}[#Totals]", prefix)
1272                } else {
1273                    format!("{}[[#Totals], [{}]]", prefix, col_name)
1274                }
1275            }
1276            SheetSection::Data => {
1277                if col_name.is_empty() {
1278                    format!("{}[#Data]", prefix)
1279                } else {
1280                    format!("{}[{}]", prefix, col_name)
1281                }
1282            }
1283            SheetSection::All => {
1284                if col_name.is_empty() {
1285                    format!("{}[#All]", prefix)
1286                } else {
1287                    format!("{}[[#All], [{}]]", prefix, col_name)
1288                }
1289            }
1290        }
1291    }
1292}
1293
1294/// Structural rewrite, provides plumbing for table renames
1295pub fn rewrite_structured_table_reference(
1296    formula_src: &str,
1297    table_name: &str,
1298    new_table_name: Option<&str>,
1299    col_rename: Option<(&str, &str)>,
1300) -> Option<String> {
1301    if !formula_src.starts_with('=') {
1302        return None;
1303    }
1304    let chars: Vec<char> = formula_src.chars().collect();
1305    let mut result = String::new();
1306    let mut last_idx = 0;
1307    let mut changed = false;
1308
1309    let mut in_quote = None;
1310    let mut i = 0;
1311    while i < chars.len() {
1312        let c = chars[i];
1313        if let Some(q) = in_quote {
1314            if c == '\\' {
1315                i = (i + 2).min(chars.len());
1316                continue;
1317            } else if c == q {
1318                in_quote = None;
1319            }
1320            i += 1;
1321            continue;
1322        }
1323
1324        if let Some((found_ref, next_i)) = try_parse_ref(&chars, i) {
1325            if let FoundRef::Structured {
1326                sheet,
1327                column,
1328                is_this_row,
1329                section,
1330            } = &found_ref
1331            {
1332                let matches_table = sheet
1333                    .as_deref()
1334                    .is_some_and(|s| s.eq_ignore_ascii_case(table_name));
1335                if matches_table {
1336                    let rendered_table_name = new_table_name.unwrap_or(table_name);
1337                    let rendered_col_name = match (column, col_rename) {
1338                        (Some(col), Some((old_col, new_col)))
1339                            if col.eq_ignore_ascii_case(old_col) =>
1340                        {
1341                            new_col.to_string()
1342                        }
1343                        (Some(col), _) => col.clone(),
1344                        (None, _) => String::new(),
1345                    };
1346                    let rendered = render_structured_ref_text(
1347                        rendered_table_name,
1348                        &rendered_col_name,
1349                        *is_this_row,
1350                        *section,
1351                    );
1352
1353                    let original: String = chars[i..next_i].iter().collect();
1354                    if rendered != original {
1355                        result.push_str(&chars[last_idx..i].iter().collect::<String>());
1356                        result.push_str(&rendered);
1357                        changed = true;
1358                        last_idx = next_i;
1359                    }
1360                }
1361            }
1362            i = next_i;
1363        } else {
1364            if c == '"' || c == '\'' {
1365                in_quote = Some(c);
1366            }
1367            i += 1;
1368        }
1369    }
1370
1371    if !changed {
1372        return None;
1373    }
1374    result.push_str(&chars[last_idx..].iter().collect::<String>());
1375    Some(result)
1376}
1377
1378fn take_number_exponent(chars: &[char], i: &mut usize, num_str: &mut String) {
1379    if *i >= chars.len() || !matches!(chars[*i], 'e' | 'E') {
1380        return;
1381    }
1382    let mut peek = *i + 1;
1383    if peek < chars.len() && matches!(chars[peek], '+' | '-') {
1384        peek += 1;
1385    }
1386    if peek >= chars.len() || !chars[peek].is_ascii_digit() {
1387        return;
1388    }
1389    num_str.push('E');
1390    if matches!(chars[*i + 1], '+' | '-') {
1391        num_str.push(chars[*i + 1]);
1392    }
1393    *i = peek;
1394    while *i < chars.len() && chars[*i].is_ascii_digit() {
1395        num_str.push(chars[*i]);
1396        *i += 1;
1397    }
1398}
1399
1400fn match_error_code(chars: &[char], start: usize) -> Option<&'static str> {
1401    crate::core::engine::result_data::EXCEL_ERROR_CODES
1402        .iter()
1403        .filter(|code| {
1404            let wanted: Vec<char> = code.chars().collect();
1405            chars.len() - start >= wanted.len()
1406                && chars[start..start + wanted.len()]
1407                    .iter()
1408                    .zip(&wanted)
1409                    .all(|(a, b)| a.eq_ignore_ascii_case(b))
1410        })
1411        .max_by_key(|code| code.len())
1412        .copied()
1413}
1414
1415fn token_can_end_reference(tok: Option<&EvalToken>) -> bool {
1416    matches!(
1417        tok,
1418        Some(
1419            EvalToken::Identifier(_)
1420                | EvalToken::Number(_)
1421                | EvalToken::String(_)
1422                | EvalToken::CloseParen
1423                | EvalToken::StructuredRef { .. }
1424        )
1425    )
1426}
1427
1428fn char_can_start_reference(c: char) -> bool {
1429    c.is_ascii_alphanumeric() || c == '$' || c == '_' || c == '\''
1430}
1431
1432pub fn lex_eval(input: &str) -> Result<Vec<EvalToken>, String> {
1433    let chars: Vec<char> = input.chars().collect();
1434    let mut tokens = Vec::new();
1435    let mut i = 0;
1436
1437    while i < chars.len() {
1438        let c = chars[i];
1439        if c.is_whitespace() {
1440            let can_end = token_can_end_reference(tokens.last());
1441            while i < chars.len() && chars[i].is_whitespace() {
1442                i += 1;
1443            }
1444            if can_end
1445                && i < chars.len()
1446                && char_can_start_reference(chars[i])
1447                && !matches!(tokens.last(), Some(EvalToken::String(_)))
1448            {
1449                tokens.push(EvalToken::Op(Op::Intersect));
1450            }
1451            continue;
1452        }
1453
1454        if c == '"' || c == '\'' {
1455            let quote = c;
1456            i += 1;
1457            let mut s = String::new();
1458            while i < chars.len() {
1459                if chars[i] == '\\' && i + 1 < chars.len() {
1460                    s.push(chars[i + 1]);
1461                    i += 2;
1462                } else if chars[i] == quote {
1463                    i += 1;
1464                    break;
1465                } else {
1466                    s.push(chars[i]);
1467                    i += 1;
1468                }
1469            }
1470            tokens.push(EvalToken::String(s));
1471            continue;
1472        }
1473
1474        if c == '['
1475            && let Some((column, is_this_row, section, next_i)) =
1476                parse_structured_specifier(&chars, i)
1477        {
1478            let mut sheet = None;
1479            if let Some(EvalToken::Identifier(_) | EvalToken::String(_)) = tokens.last() {
1480                let last = tokens.pop().unwrap();
1481                sheet = match last {
1482                    EvalToken::Identifier(s) => Some(s),
1483                    EvalToken::String(s) => Some(s),
1484                    _ => unreachable!(),
1485                };
1486            }
1487            tokens.push(EvalToken::StructuredRef {
1488                sheet,
1489                column,
1490                is_this_row,
1491                section,
1492            });
1493            i = next_i;
1494            continue;
1495        }
1496
1497        if c == '#'
1498            && let Some(code) = match_error_code(&chars, i)
1499        {
1500            i += code.chars().count();
1501            tokens.push(EvalToken::Error(code));
1502            continue;
1503        }
1504
1505        match c {
1506            '(' => {
1507                tokens.push(EvalToken::OpenParen);
1508                i += 1;
1509                continue;
1510            }
1511            ')' => {
1512                tokens.push(EvalToken::CloseParen);
1513                i += 1;
1514                continue;
1515            }
1516            '[' => {
1517                tokens.push(EvalToken::OpenBracket);
1518                i += 1;
1519                continue;
1520            }
1521            ']' => {
1522                tokens.push(EvalToken::CloseBracket);
1523                i += 1;
1524                continue;
1525            }
1526            ',' => {
1527                tokens.push(EvalToken::Comma);
1528                i += 1;
1529                continue;
1530            }
1531            ':' => {
1532                tokens.push(EvalToken::Colon);
1533                i += 1;
1534                continue;
1535            }
1536            _ => {}
1537        }
1538
1539        if c == '<' {
1540            if i + 1 < chars.len() && chars[i + 1] == '>' {
1541                tokens.push(EvalToken::Op(Op::Ne));
1542                i += 2;
1543            } else if i + 1 < chars.len() && chars[i + 1] == '=' {
1544                tokens.push(EvalToken::Op(Op::Le));
1545                i += 2;
1546            } else {
1547                tokens.push(EvalToken::Op(Op::Lt));
1548                i += 1;
1549            }
1550            continue;
1551        }
1552        if c == '>' {
1553            if i + 1 < chars.len() && chars[i + 1] == '=' {
1554                tokens.push(EvalToken::Op(Op::Ge));
1555                i += 2;
1556            } else {
1557                tokens.push(EvalToken::Op(Op::Gt));
1558                i += 1;
1559            }
1560            continue;
1561        }
1562        if c == '=' {
1563            if i + 1 < chars.len() && chars[i + 1] == '=' {
1564                tokens.push(EvalToken::Op(Op::Eq));
1565                i += 2;
1566            } else {
1567                tokens.push(EvalToken::Op(Op::Eq));
1568                i += 1;
1569            }
1570            continue;
1571        }
1572        if c == '!' {
1573            if i + 1 < chars.len() && chars[i + 1] == '=' {
1574                tokens.push(EvalToken::Op(Op::Ne));
1575                i += 2;
1576            } else {
1577                tokens.push(EvalToken::Exclamation);
1578                i += 1;
1579            }
1580            continue;
1581        }
1582
1583        match c {
1584            '+' => {
1585                tokens.push(EvalToken::Op(Op::Add));
1586                i += 1;
1587                continue;
1588            }
1589            '-' => {
1590                tokens.push(EvalToken::Op(Op::Sub));
1591                i += 1;
1592                continue;
1593            }
1594            '*' => {
1595                if i + 1 < chars.len() && chars[i + 1] == '*' {
1596                    tokens.push(EvalToken::Op(Op::Exp));
1597                    i += 2;
1598                } else {
1599                    tokens.push(EvalToken::Op(Op::Mul));
1600                    i += 1;
1601                }
1602                continue;
1603            }
1604            '/' => {
1605                tokens.push(EvalToken::Op(Op::Div));
1606                i += 1;
1607                continue;
1608            }
1609            '^' => {
1610                tokens.push(EvalToken::Op(Op::Exp));
1611                i += 1;
1612                continue;
1613            }
1614            '%' => {
1615                tokens.push(EvalToken::Op(Op::Percent));
1616                i += 1;
1617                continue;
1618            }
1619            '@' => {
1620                tokens.push(EvalToken::Op(Op::ImplicitIntersection));
1621                i += 1;
1622                continue;
1623            }
1624            '#' => {
1625                tokens.push(EvalToken::Op(Op::Spill));
1626                i += 1;
1627                continue;
1628            }
1629            '&' => {
1630                tokens.push(EvalToken::Op(Op::Concat));
1631                i += 1;
1632                continue;
1633            }
1634            _ => {}
1635        }
1636
1637        if c == '.' {
1638            if i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
1639                let mut num_str = String::new();
1640                num_str.push('.');
1641                i += 1;
1642                while i < chars.len() && chars[i].is_ascii_digit() {
1643                    num_str.push(chars[i]);
1644                    i += 1;
1645                }
1646                take_number_exponent(&chars, &mut i, &mut num_str);
1647                if let Ok(val) = num_str.parse::<f64>() {
1648                    tokens.push(EvalToken::Number(val));
1649                } else {
1650                    return Err(format!("Invalid number: {}", num_str));
1651                }
1652            } else {
1653                tokens.push(EvalToken::Dot);
1654                i += 1;
1655            }
1656            continue;
1657        }
1658
1659        if c.is_ascii_digit() {
1660            let mut num_str = String::new();
1661            while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
1662                num_str.push(chars[i]);
1663                i += 1;
1664            }
1665            take_number_exponent(&chars, &mut i, &mut num_str);
1666            if let Ok(val) = num_str.parse::<f64>() {
1667                tokens.push(EvalToken::Number(val));
1668            } else {
1669                return Err(format!("Invalid number: {}", num_str));
1670            }
1671            continue;
1672        }
1673
1674        if c.is_ascii_alphabetic() || c == '_' || c == '$' {
1675            let mut id_str = String::new();
1676            while i < chars.len()
1677                && (chars[i].is_ascii_alphanumeric()
1678                    || chars[i] == '_'
1679                    || chars[i] == '$'
1680                    || chars[i] == '.')
1681            {
1682                id_str.push(chars[i]);
1683                i += 1;
1684            }
1685
1686            let upper = id_str.to_uppercase();
1687            let mut lookahead = i;
1688            while lookahead < chars.len() && chars[lookahead].is_whitespace() {
1689                lookahead += 1;
1690            }
1691            let followed_by_paren = lookahead < chars.len() && chars[lookahead] == '(';
1692            if upper == "TRUE" && !followed_by_paren {
1693                tokens.push(EvalToken::Boolean(true));
1694            } else if upper == "FALSE" && !followed_by_paren {
1695                tokens.push(EvalToken::Boolean(false));
1696            } else {
1697                tokens.push(EvalToken::Identifier(id_str));
1698            }
1699            continue;
1700        }
1701
1702        return Err(format!("Unexpected character: {}", c));
1703    }
1704
1705    Ok(tokens)
1706}
1707
1708fn ref_token_text(tok: &EvalToken) -> Option<String> {
1709    match tok {
1710        EvalToken::Identifier(s) => Some(s.clone()),
1711        EvalToken::Number(n) if n.is_finite() && n.fract() == 0.0 && *n >= 1.0 => {
1712            Some(format!("{n:.0}"))
1713        }
1714        _ => None,
1715    }
1716}
1717
1718fn range_ref_from_texts(sheet: Option<String>, start: &str, end: &str) -> Result<Expr, String> {
1719    if let (
1720        Some((s_row, s_col, s_row_abs, s_col_abs)),
1721        Some((e_row, e_col, e_row_abs, e_col_abs)),
1722    ) = (parse_cell_ref_parts(start), parse_cell_ref_parts(end))
1723    {
1724        Ok(Expr::RangeRef {
1725            sheet,
1726            start_row: s_row,
1727            start_col: s_col,
1728            end_row: e_row,
1729            end_col: e_col,
1730            start_row_abs: s_row_abs,
1731            start_col_abs: s_col_abs,
1732            end_row_abs: e_row_abs,
1733            end_col_abs: e_col_abs,
1734        })
1735    } else if let (Some((s_col, s_col_abs)), Some((e_col, e_col_abs))) =
1736        (parse_column_ref(start), parse_column_ref(end))
1737    {
1738        Ok(Expr::RangeRef {
1739            sheet,
1740            start_row: 0,
1741            start_col: s_col,
1742            end_row: usize::MAX,
1743            end_col: e_col,
1744            start_row_abs: true,
1745            start_col_abs: s_col_abs,
1746            end_row_abs: true,
1747            end_col_abs: e_col_abs,
1748        })
1749    } else if let (Some((s_row, s_row_abs)), Some((e_row, e_row_abs))) =
1750        (parse_row_ref(start), parse_row_ref(end))
1751    {
1752        Ok(Expr::RangeRef {
1753            sheet,
1754            start_row: s_row,
1755            start_col: 0,
1756            end_row: e_row,
1757            end_col: usize::MAX,
1758            start_row_abs: s_row_abs,
1759            start_col_abs: true,
1760            end_row_abs: e_row_abs,
1761            end_col_abs: true,
1762        })
1763    } else {
1764        Err(format!("Invalid range reference: {start}:{end}"))
1765    }
1766}
1767
1768struct Parser<'a> {
1769    tokens: &'a [EvalToken],
1770    pos: usize,
1771}
1772
1773impl<'a> Parser<'a> {
1774    fn new(tokens: &'a [EvalToken]) -> Self {
1775        Self { tokens, pos: 0 }
1776    }
1777
1778    fn peek(&self) -> Option<&EvalToken> {
1779        self.tokens.get(self.pos)
1780    }
1781
1782    fn next(&mut self) -> Option<&EvalToken> {
1783        if self.pos < self.tokens.len() {
1784            let tok = &self.tokens[self.pos];
1785            self.pos += 1;
1786            Some(tok)
1787        } else {
1788            None
1789        }
1790    }
1791
1792    fn consume(&mut self, expected: EvalToken) -> Result<(), String> {
1793        match self.next() {
1794            Some(tok) if *tok == expected => Ok(()),
1795            Some(tok) => Err(format!("Expected {:?}, got {:?}", expected, tok)),
1796            None => Err(format!("Expected {:?}, got EOF", expected)),
1797        }
1798    }
1799
1800    fn parse(&mut self) -> Result<Expr, String> {
1801        self.parse_expr(true)
1802    }
1803
1804    fn parse_expr(&mut self, allow_union: bool) -> Result<Expr, String> {
1805        let mut lhs = self.parse_binary(0, allow_union)?;
1806
1807        while let Some(tok) = self.peek() {
1808            match tok {
1809                EvalToken::OpenBracket => {
1810                    self.next();
1811                    let mut inner_tokens = Vec::new();
1812                    let mut depth = 1;
1813                    while let Some(t) = self.next() {
1814                        if *t == EvalToken::OpenBracket {
1815                            depth += 1;
1816                            inner_tokens.push(t.clone());
1817                        } else if *t == EvalToken::CloseBracket {
1818                            depth -= 1;
1819                            if depth == 0 {
1820                                break;
1821                            }
1822                            inner_tokens.push(t.clone());
1823                        } else {
1824                            inner_tokens.push(t.clone());
1825                        }
1826                    }
1827
1828                    if let Some(colon_pos) =
1829                        inner_tokens.iter().position(|t| *t == EvalToken::Colon)
1830                    {
1831                        let start_toks = &inner_tokens[0..colon_pos];
1832                        let end_toks = &inner_tokens[colon_pos + 1..];
1833
1834                        let start_val = if start_toks.is_empty() {
1835                            None
1836                        } else {
1837                            let mut p = Parser::new(start_toks);
1838                            Some(p.parse_expr(false)?)
1839                        };
1840
1841                        let end_val = if end_toks.is_empty() {
1842                            None
1843                        } else {
1844                            let mut p = Parser::new(end_toks);
1845                            Some(p.parse_expr(false)?)
1846                        };
1847
1848                        lhs = Expr::Slice {
1849                            expr: Box::new(lhs),
1850                            start: start_val.map(Box::new),
1851                            end: end_val.map(Box::new),
1852                        };
1853                    } else {
1854                        let mut p = Parser::new(&inner_tokens);
1855                        let index_expr = p.parse_expr(false)?;
1856
1857                        if let Expr::Identifier(ref sheet_name) = lhs {
1858                            let col_name_opt = match &index_expr {
1859                                Expr::String(s) => Some(s.clone()),
1860                                Expr::Identifier(s) => Some(s.clone()),
1861                                _ => None,
1862                            };
1863                            if let Some(col_name) = col_name_opt {
1864                                lhs = Expr::StructuredRef {
1865                                    sheet: Some(sheet_name.clone()),
1866                                    column: Some(col_name),
1867                                    is_this_row: false,
1868                                    section: SheetSection::Data,
1869                                };
1870                                continue;
1871                            }
1872                        }
1873
1874                        lhs = Expr::FunctionCall {
1875                            name: "INDEX".to_string(),
1876                            args: vec![lhs, index_expr],
1877                        };
1878                    }
1879                }
1880                EvalToken::Dot => {
1881                    return Err("Dot member access is not supported".to_string());
1882                }
1883                _ => break,
1884            }
1885        }
1886
1887        Ok(lhs)
1888    }
1889
1890    fn parse_binary(&mut self, min_prec: u8, allow_union: bool) -> Result<Expr, String> {
1891        let mut lhs = self.parse_prefix(allow_union)?;
1892
1893        while matches!(self.peek(), Some(EvalToken::Op(Op::Percent | Op::Spill))) {
1894            let EvalToken::Op(op) = self.next().unwrap() else {
1895                unreachable!()
1896            };
1897            lhs = Expr::UnaryOp {
1898                op: *op,
1899                expr: Box::new(lhs),
1900            };
1901        }
1902
1903        while let Some(tok) = self.peek() {
1904            let op = match tok {
1905                EvalToken::Op(Op::Percent | Op::Spill | Op::ImplicitIntersection) => break,
1906                EvalToken::Op(o) => *o,
1907                EvalToken::Comma if allow_union => Op::Union,
1908                _ => break,
1909            };
1910
1911            let prec = op_precedence(op);
1912            if prec < min_prec {
1913                break;
1914            }
1915
1916            self.next();
1917
1918            let rhs = self.parse_binary(prec + 1, allow_union)?;
1919            lhs = Expr::BinaryOp {
1920                op,
1921                left: Box::new(lhs),
1922                right: Box::new(rhs),
1923            };
1924        }
1925
1926        Ok(lhs)
1927    }
1928
1929    fn parse_prefix(&mut self, allow_union: bool) -> Result<Expr, String> {
1930        let tok = self
1931            .next()
1932            .ok_or_else(|| "Unexpected EOF".to_string())?
1933            .clone();
1934        match tok {
1935            EvalToken::Number(val) => {
1936                if self.peek() == Some(&EvalToken::Colon)
1937                    && val.is_finite()
1938                    && val.fract() == 0.0
1939                    && val >= 1.0
1940                {
1941                    self.next();
1942                    let end_tok = self
1943                        .next()
1944                        .ok_or_else(|| "Expected row reference after `:`".to_string())?;
1945                    let start_str = format!("{val:.0}");
1946                    let end_str = ref_token_text(end_tok).ok_or_else(|| {
1947                        format!("Expected row reference after `:`, got {:?}", end_tok)
1948                    })?;
1949                    return range_ref_from_texts(None, &start_str, &end_str);
1950                }
1951                Ok(Expr::Number(val))
1952            }
1953            EvalToken::String(val) => {
1954                if self.peek() == Some(&EvalToken::Exclamation) {
1955                    self.next();
1956                    let target_tok = self
1957                        .next()
1958                        .ok_or_else(|| "Expected cell or column reference after `!`".to_string())?
1959                        .clone();
1960                    let target_str = ref_token_text(&target_tok).ok_or_else(|| {
1961                        format!(
1962                            "Expected cell, row, or column reference after `!`, got {:?}",
1963                            target_tok
1964                        )
1965                    })?;
1966
1967                    if self.peek() == Some(&EvalToken::Colon) {
1968                        self.next();
1969                        let end_tok = self
1970                            .next()
1971                            .ok_or_else(|| {
1972                                "Expected cell, row, or column reference after `:`".to_string()
1973                            })?
1974                            .clone();
1975                        let end_str = ref_token_text(&end_tok).ok_or_else(|| {
1976                            format!(
1977                                "Expected cell, row, or column reference after `:`, got {:?}",
1978                                end_tok
1979                            )
1980                        })?;
1981
1982                        return range_ref_from_texts(Some(val), &target_str, &end_str);
1983                    } else {
1984                        let (row, col, row_abs, col_abs) = parse_cell_ref_parts(&target_str)
1985                            .ok_or_else(|| format!("Invalid cell: {}", target_str))?;
1986                        return Ok(Expr::CellRef {
1987                            sheet: Some(val),
1988                            row,
1989                            col,
1990                            row_abs,
1991                            col_abs,
1992                        });
1993                    }
1994                }
1995                Ok(Expr::String(val.clone()))
1996            }
1997            EvalToken::Boolean(val) => Ok(Expr::Boolean(val)),
1998            EvalToken::Error(code) => Ok(Expr::Error(code)),
1999            EvalToken::OpenParen => {
2000                let expr = self.parse_expr(true)?;
2001                self.consume(EvalToken::CloseParen)?;
2002                Ok(expr)
2003            }
2004            EvalToken::OpenBracket => {
2005                let mut list = Vec::new();
2006                if self.peek() != Some(&EvalToken::CloseBracket) {
2007                    loop {
2008                        list.push(self.parse_expr(false)?);
2009                        if self.peek() == Some(&EvalToken::Comma) {
2010                            self.next();
2011                        } else {
2012                            break;
2013                        }
2014                    }
2015                }
2016                self.consume(EvalToken::CloseBracket)?;
2017                Ok(Expr::List(list))
2018            }
2019            EvalToken::Op(Op::Sub) => {
2020                let expr = self.parse_binary(100, allow_union)?;
2021                Ok(Expr::UnaryOp {
2022                    op: Op::Sub,
2023                    expr: Box::new(expr),
2024                })
2025            }
2026            EvalToken::Op(Op::Add) => {
2027                let expr = self.parse_binary(100, allow_union)?;
2028                Ok(expr)
2029            }
2030            EvalToken::Op(Op::ImplicitIntersection) => {
2031                let expr = self.parse_binary(100, allow_union)?;
2032                Ok(Expr::UnaryOp {
2033                    op: Op::ImplicitIntersection,
2034                    expr: Box::new(expr),
2035                })
2036            }
2037            EvalToken::Identifier(id_name) => {
2038                if self.peek() == Some(&EvalToken::OpenParen) {
2039                    self.next();
2040                    let mut args = Vec::new();
2041                    if self.peek() != Some(&EvalToken::CloseParen) {
2042                        loop {
2043                            args.push(self.parse_expr(false)?);
2044                            if self.peek() == Some(&EvalToken::Comma) {
2045                                self.next();
2046                            } else {
2047                                break;
2048                            }
2049                        }
2050                    }
2051                    self.consume(EvalToken::CloseParen)?;
2052                    return Ok(Expr::FunctionCall {
2053                        name: id_name.clone(),
2054                        args,
2055                    });
2056                }
2057
2058                if self.peek() == Some(&EvalToken::Exclamation) {
2059                    self.next();
2060                    let target_tok = self
2061                        .next()
2062                        .ok_or_else(|| "Expected cell or column reference after `!`".to_string())?;
2063                    let target_str = ref_token_text(target_tok).ok_or_else(|| {
2064                        format!(
2065                            "Expected cell, row, or column reference after `!`, got {:?}",
2066                            target_tok
2067                        )
2068                    })?;
2069
2070                    if self.peek() == Some(&EvalToken::Colon) {
2071                        self.next();
2072                        let end_tok = self.next().ok_or_else(|| {
2073                            "Expected cell, row, or column reference after `:`".to_string()
2074                        })?;
2075                        let end_str = ref_token_text(end_tok).ok_or_else(|| {
2076                            format!(
2077                                "Expected cell, row, or column reference after `:`, got {:?}",
2078                                end_tok
2079                            )
2080                        })?;
2081
2082                        return range_ref_from_texts(Some(id_name.clone()), &target_str, &end_str);
2083                    } else {
2084                        let (row, col, row_abs, col_abs) = parse_cell_ref_parts(&target_str)
2085                            .ok_or_else(|| format!("Invalid cell: {}", target_str))?;
2086                        return Ok(Expr::CellRef {
2087                            sheet: Some(id_name.clone()),
2088                            row,
2089                            col,
2090                            row_abs,
2091                            col_abs,
2092                        });
2093                    }
2094                }
2095
2096                if let Some((row, col, row_abs, col_abs)) = parse_cell_ref_parts(&id_name) {
2097                    if self.peek() == Some(&EvalToken::Colon) {
2098                        self.next();
2099                        let end_tok = self
2100                            .next()
2101                            .ok_or_else(|| "Expected cell reference after `:`".to_string())?;
2102                        let end_str = match end_tok {
2103                            EvalToken::Identifier(s) => s.clone(),
2104                            _ => {
2105                                return Err(format!(
2106                                    "Expected cell reference after `:`, got {:?}",
2107                                    end_tok
2108                                ));
2109                            }
2110                        };
2111                        let (e_row, e_col, e_row_abs, e_col_abs) =
2112                            parse_cell_ref_parts(&end_str)
2113                                .ok_or_else(|| format!("Invalid end cell: {}", end_str))?;
2114                        return Ok(Expr::RangeRef {
2115                            sheet: None,
2116                            start_row: row,
2117                            start_col: col,
2118                            end_row: e_row,
2119                            end_col: e_col,
2120                            start_row_abs: row_abs,
2121                            start_col_abs: col_abs,
2122                            end_row_abs: e_row_abs,
2123                            end_col_abs: e_col_abs,
2124                        });
2125                    }
2126
2127                    return Ok(Expr::CellRef {
2128                        sheet: None,
2129                        row,
2130                        col,
2131                        row_abs,
2132                        col_abs,
2133                    });
2134                }
2135
2136                if let Some((col, col_abs)) = parse_column_ref(&id_name)
2137                    && self.peek() == Some(&EvalToken::Colon)
2138                {
2139                    self.next();
2140                    let end_tok = self
2141                        .next()
2142                        .ok_or_else(|| "Expected column reference after `:`".to_string())?;
2143                    let end_str = match end_tok {
2144                        EvalToken::Identifier(s) => s.clone(),
2145                        _ => {
2146                            return Err(format!(
2147                                "Expected column reference after `:`, got {:?}",
2148                                end_tok
2149                            ));
2150                        }
2151                    };
2152                    let (e_col, e_col_abs) = parse_column_ref(&end_str)
2153                        .ok_or_else(|| format!("Invalid end column: {}", end_str))?;
2154                    return Ok(Expr::RangeRef {
2155                        sheet: None,
2156                        start_row: 0,
2157                        start_col: col,
2158                        end_row: usize::MAX,
2159                        end_col: e_col,
2160                        start_row_abs: true,
2161                        start_col_abs: col_abs,
2162                        end_row_abs: true,
2163                        end_col_abs: e_col_abs,
2164                    });
2165                }
2166
2167                if let Some((row, row_abs)) = parse_row_ref(&id_name)
2168                    && self.peek() == Some(&EvalToken::Colon)
2169                {
2170                    self.next();
2171                    let end_tok = self
2172                        .next()
2173                        .ok_or_else(|| "Expected row reference after `:`".to_string())?;
2174                    let end_str = ref_token_text(end_tok).ok_or_else(|| {
2175                        format!("Expected row reference after `:`, got {:?}", end_tok)
2176                    })?;
2177                    let (e_row, e_row_abs) = parse_row_ref(&end_str)
2178                        .ok_or_else(|| format!("Invalid end row: {}", end_str))?;
2179                    return Ok(Expr::RangeRef {
2180                        sheet: None,
2181                        start_row: row,
2182                        start_col: 0,
2183                        end_row: e_row,
2184                        end_col: usize::MAX,
2185                        start_row_abs: row_abs,
2186                        start_col_abs: true,
2187                        end_row_abs: e_row_abs,
2188                        end_col_abs: true,
2189                    });
2190                }
2191
2192                Ok(Expr::Identifier(id_name.clone()))
2193            }
2194            EvalToken::StructuredRef {
2195                sheet,
2196                column,
2197                is_this_row,
2198                section,
2199            } => Ok(Expr::StructuredRef {
2200                sheet: sheet.clone(),
2201                column: column.clone(),
2202                is_this_row,
2203                section,
2204            }),
2205            _ => Err(format!("Unexpected token: {:?}", tok)),
2206        }
2207    }
2208}
2209
2210fn op_precedence(op: Op) -> u8 {
2211    match op {
2212        Op::Union => 40,
2213        Op::Intersect => 50,
2214        Op::Add | Op::Sub => 10,
2215        Op::Mul | Op::Div => 20,
2216        Op::Exp => 30,
2217        Op::Concat => 8,
2218        Op::Eq | Op::Ne | Op::Lt | Op::Gt | Op::Le | Op::Ge => 5,
2219        Op::Percent | Op::ImplicitIntersection | Op::Spill => 60,
2220    }
2221}
2222
2223pub fn parse_excel_formula(input: &str) -> Result<Expr, String> {
2224    let tokens = lex_eval(input)?;
2225    let mut parser = Parser::new(&tokens);
2226    let expr = parser.parse()?;
2227    if parser.peek().is_some() {
2228        return Err("Trailing tokens after expression".to_string());
2229    }
2230    Ok(expr)
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235    use super::*;
2236
2237    #[test]
2238    fn test_parse_cell_ref() {
2239        let (sheet, row, col) = parse_cell_ref("A1").unwrap();
2240        assert_eq!(sheet, None);
2241        assert_eq!(row, 0);
2242        assert_eq!(col, 0);
2243
2244        let (sheet, row, col) = parse_cell_ref("Sheet1!C5").unwrap();
2245        assert_eq!(sheet, Some("Sheet1".to_string()));
2246        assert_eq!(row, 4);
2247        assert_eq!(col, 2);
2248    }
2249
2250    #[test]
2251    fn test_parse_range_ref() {
2252        let (sheet, s_row, s_col, e_row, e_col) = parse_range_ref("A1:C10").unwrap();
2253        assert_eq!(sheet, None);
2254        assert_eq!((s_row, s_col), (0, 0));
2255        assert_eq!((e_row, e_col), (9, 2));
2256
2257        let (sheet, s_row, s_col, e_row, e_col) = parse_range_ref("'Data Sheet'!B2:D4").unwrap();
2258        assert_eq!(sheet, Some("Data Sheet".to_string()));
2259        assert_eq!((s_row, s_col), (1, 1));
2260        assert_eq!((e_row, e_col), (3, 3));
2261    }
2262
2263    #[test]
2264    fn test_col_conversions() {
2265        assert_eq!(col_idx_to_letters(0), "A");
2266        assert_eq!(col_idx_to_letters(25), "Z");
2267        assert_eq!(col_idx_to_letters(26), "AA");
2268        assert_eq!(col_idx_to_letters(27), "AB");
2269
2270        assert_eq!(col_letters_to_idx("A"), 0);
2271        assert_eq!(col_letters_to_idx("z"), 25);
2272        assert_eq!(col_letters_to_idx("AA"), 26);
2273        assert_eq!(col_letters_to_idx("AB"), 27);
2274    }
2275
2276    #[test]
2277    fn test_lex_scientific_notation_literals() {
2278        for (src, want) in [
2279            ("1E5", 1e5),
2280            ("1E+5", 1e5),
2281            ("1e5", 1e5),
2282            ("2.5E-3", 2.5e-3),
2283            (".5E3", 0.5e3),
2284            ("1E+300", 1e300),
2285        ] {
2286            match lex_eval(src).unwrap().as_slice() {
2287                [EvalToken::Number(got)] => {
2288                    assert_eq!(*got, want, "{src} lexed to the wrong value")
2289                }
2290                other => panic!("{src} did not lex as a single number: {other:?}"),
2291            }
2292        }
2293    }
2294
2295    #[test]
2296    fn test_lex_does_not_eat_e_that_starts_a_reference() {
2297        let tokens = lex_eval("A1:E5").unwrap();
2298        assert_eq!(
2299            tokens.len(),
2300            3,
2301            "A1:E5 must stay ident/colon/ident, got {tokens:?}"
2302        );
2303        assert!(matches!(
2304            lex_eval("1E").unwrap().as_slice(),
2305            [EvalToken::Number(n), _] if *n == 1.0
2306        ));
2307    }
2308
2309    #[test]
2310    fn test_compile_and_serialize() {
2311        let sheet1 = Sheet::new(crate::core::SheetInit {
2312            id: Some(123),
2313            name: Some("Sheet1".to_string()),
2314            rows: 5,
2315            cols: 5,
2316        });
2317        let sheet2 = Sheet::new(crate::core::SheetInit {
2318            id: Some(456),
2319            name: Some("Sheet2".to_string()),
2320            rows: 5,
2321            cols: 5,
2322        });
2323
2324        let sheets = vec![sheet1, sheet2];
2325
2326        let formula = compile_formula("=Sheet2!B1 + 10", &sheets);
2327        assert_eq!(formula.parts.len(), 3);
2328        match &formula.parts[1] {
2329            FormulaPart::SheetReference {
2330                sheet_id, row, col, ..
2331            } => {
2332                assert_eq!(*sheet_id, 456);
2333                assert_eq!(*row, 0);
2334                assert_eq!(*col, 1);
2335            }
2336            _ => panic!("Expected SheetReference"),
2337        }
2338
2339        let mut renamed_sheet2 = sheets[1].clone();
2340        renamed_sheet2.name = "Sheet3".to_string();
2341
2342        let serialized = serialize_formula(&formula, &[sheets[0].clone(), renamed_sheet2]);
2343        assert_eq!(serialized, "=Sheet3!B1 + 10");
2344    }
2345
2346    #[test]
2347    fn test_table_names_with_spaces() {
2348        let sheet1 = Sheet::new(crate::core::SheetInit {
2349            id: Some(123),
2350            name: Some("Sheet1".to_string()),
2351            rows: 5,
2352            cols: 5,
2353        });
2354        let sheet2 = Sheet::new(crate::core::SheetInit {
2355            id: Some(456),
2356            name: Some("My Sheet".to_string()),
2357            rows: 5,
2358            cols: 5,
2359        });
2360
2361        let sheets = vec![sheet1, sheet2];
2362
2363        let formula_quote = compile_formula("='My Sheet'!B1 + 10", &sheets);
2364        match &formula_quote.parts[1] {
2365            FormulaPart::SheetReference {
2366                sheet_id, row, col, ..
2367            } => {
2368                assert_eq!(*sheet_id, 456);
2369                assert_eq!(*row, 0);
2370                assert_eq!(*col, 1);
2371            }
2372            _ => panic!("Expected SheetReference"),
2373        }
2374
2375        let serialized_quote = serialize_formula(&formula_quote, &sheets);
2376        assert_eq!(serialized_quote, "='My Sheet'!B1 + 10");
2377
2378        let ast_quote = parse_excel_formula("'My Sheet'!B1 + 10").unwrap();
2379        match ast_quote {
2380            Expr::BinaryOp { left, .. } => match *left {
2381                Expr::CellRef {
2382                    sheet, row, col, ..
2383                } => {
2384                    assert_eq!(sheet, Some("My Sheet".to_string()));
2385                    assert_eq!(row, 0);
2386                    assert_eq!(col, 1);
2387                }
2388                _ => panic!("Expected CellRef"),
2389            },
2390            _ => panic!("Expected BinaryOp"),
2391        }
2392    }
2393
2394    #[test]
2395    fn test_row_ranges_compile_serialize_and_parse() {
2396        let sheet1 = Sheet::new(crate::core::SheetInit {
2397            id: Some(123),
2398            name: Some("Sheet1".to_string()),
2399            rows: 5,
2400            cols: 5,
2401        });
2402        let sheet2 = Sheet::new(crate::core::SheetInit {
2403            id: Some(456),
2404            name: Some("Sheet2".to_string()),
2405            rows: 5,
2406            cols: 5,
2407        });
2408        let sheets = vec![sheet1, sheet2];
2409
2410        let formula = compile_formula("=SUM(1:3)", &sheets);
2411        match &formula.parts[1] {
2412            FormulaPart::RangeReference {
2413                sheet_id,
2414                start_row,
2415                start_col,
2416                end_row,
2417                end_col,
2418                ..
2419            } => {
2420                assert_eq!(*sheet_id, 123);
2421                assert_eq!((*start_row, *start_col), (0, 0));
2422                assert_eq!((*end_row, *end_col), (2, usize::MAX));
2423            }
2424            _ => panic!("Expected whole-row RangeReference"),
2425        }
2426        assert_eq!(serialize_formula(&formula, &sheets), "=SUM(1:3)");
2427
2428        let cross = compile_formula("=SUM(Sheet2!$2:$4)", &sheets);
2429        assert_eq!(serialize_formula(&cross, &sheets), "=SUM(Sheet2!$2:$4)");
2430
2431        let ast = parse_excel_formula("SUM(Sheet2!1:3)").unwrap();
2432        match ast {
2433            Expr::FunctionCall { args, .. } => match &args[0] {
2434                Expr::RangeRef {
2435                    sheet,
2436                    start_row,
2437                    start_col,
2438                    end_row,
2439                    end_col,
2440                    ..
2441                } => {
2442                    assert_eq!(sheet.as_deref(), Some("Sheet2"));
2443                    assert_eq!((*start_row, *start_col), (0, 0));
2444                    assert_eq!((*end_row, *end_col), (2, usize::MAX));
2445                }
2446                _ => panic!("Expected whole-row RangeRef"),
2447            },
2448            _ => panic!("Expected FunctionCall"),
2449        }
2450    }
2451
2452    #[test]
2453    fn test_table_names_with_periods() {
2454        let sheet1 = Sheet::new(crate::core::SheetInit {
2455            id: Some(123),
2456            name: Some("Model_SL_5.5Yr".to_string()),
2457            rows: 5,
2458            cols: 5,
2459        });
2460
2461        let sheets = vec![sheet1];
2462
2463        let formula = compile_formula("=Model_SL_5.5Yr!B10 + 10", &sheets);
2464        assert_eq!(formula.parts.len(), 3);
2465        match &formula.parts[1] {
2466            FormulaPart::SheetReference {
2467                sheet_id, row, col, ..
2468            } => {
2469                assert_eq!(*sheet_id, 123);
2470                assert_eq!(*row, 9);
2471                assert_eq!(*col, 1);
2472            }
2473            _ => panic!("Expected SheetReference"),
2474        }
2475
2476        let ast = parse_excel_formula("Model_SL_5.5Yr!B10 + 10").unwrap();
2477        match ast {
2478            Expr::BinaryOp { left, .. } => match *left {
2479                Expr::CellRef {
2480                    sheet, row, col, ..
2481                } => {
2482                    assert_eq!(sheet, Some("Model_SL_5.5Yr".to_string()));
2483                    assert_eq!(row, 9);
2484                    assert_eq!(col, 1);
2485                }
2486                _ => panic!("Expected CellRef"),
2487            },
2488            _ => panic!("Expected BinaryOp"),
2489        }
2490    }
2491
2492    #[test]
2493    fn test_column_ranges() {
2494        let sheet1 = Sheet::new(crate::core::SheetInit {
2495            id: Some(123),
2496            name: Some("Sheet1".to_string()),
2497            rows: 5,
2498            cols: 5,
2499        });
2500        let sheets = vec![sheet1];
2501
2502        let formula = compile_formula("=SUM(A:B)", &sheets);
2503        assert_eq!(formula.parts.len(), 3);
2504        match &formula.parts[1] {
2505            FormulaPart::RangeReference {
2506                sheet_id,
2507                start_row,
2508                start_col,
2509                end_row,
2510                end_col,
2511                ..
2512            } => {
2513                assert_eq!(*sheet_id, 123);
2514                assert_eq!(*start_row, 0);
2515                assert_eq!(*start_col, 0);
2516                assert_eq!(*end_row, usize::MAX);
2517                assert_eq!(*end_col, 1);
2518            }
2519            _ => panic!("Expected RangeReference"),
2520        }
2521
2522        let serialized = serialize_formula(&formula, &sheets);
2523        assert_eq!(serialized, "=SUM(A:B)");
2524
2525        let sheet2 = Sheet::new(crate::core::SheetInit {
2526            id: Some(456),
2527            name: Some("Sheet2".to_string()),
2528            rows: 5,
2529            cols: 5,
2530        });
2531        let sheets_multi = vec![sheets[0].clone(), sheet2];
2532        let formula_cross = compile_formula("=SUM(Sheet2!$A:$C)", &sheets_multi);
2533        match &formula_cross.parts[1] {
2534            FormulaPart::RangeReference {
2535                sheet_id,
2536                start_row,
2537                start_col,
2538                end_row,
2539                end_col,
2540                start_col_ref_type,
2541                end_col_ref_type,
2542                ..
2543            } => {
2544                assert_eq!(*sheet_id, 456);
2545                assert_eq!(*start_row, 0);
2546                assert_eq!(*start_col, 0);
2547                assert_eq!(*end_row, usize::MAX);
2548                assert_eq!(*end_col, 2);
2549                assert_eq!(*start_col_ref_type, RefType::Absolute);
2550                assert_eq!(*end_col_ref_type, RefType::Absolute);
2551            }
2552            _ => panic!("Expected RangeReference for cross sheet"),
2553        }
2554
2555        let serialized_cross = serialize_formula(&formula_cross, &sheets_multi);
2556        assert_eq!(serialized_cross, "=SUM(Sheet2!$A:$C)");
2557
2558        let ast = parse_excel_formula("SUM(A:A)").unwrap();
2559        match ast {
2560            Expr::FunctionCall { name, args } => {
2561                assert_eq!(name, "SUM");
2562                assert_eq!(args.len(), 1);
2563                match &args[0] {
2564                    Expr::RangeRef {
2565                        sheet,
2566                        start_row,
2567                        start_col,
2568                        end_row,
2569                        end_col,
2570                        ..
2571                    } => {
2572                        assert_eq!(*sheet, None);
2573                        assert_eq!(*start_row, 0);
2574                        assert_eq!(*start_col, 0);
2575                        assert_eq!(*end_row, usize::MAX);
2576                        assert_eq!(*end_col, 0);
2577                    }
2578                    _ => panic!("Expected RangeRef inside SUM"),
2579                }
2580            }
2581            _ => panic!("Expected FunctionCall"),
2582        }
2583    }
2584
2585    #[test]
2586    fn test_excel_structured_references() {
2587        let mut sheet1 = Sheet::new(crate::core::SheetInit {
2588            id: Some(123),
2589            name: Some("Sheet1".to_string()),
2590            rows: 5,
2591            cols: 2,
2592        });
2593        sheet1.columns[0].name = "Sales".to_string();
2594        sheet1.columns[0].id = 1;
2595        sheet1.columns[1].name = "Cost".to_string();
2596        sheet1.columns[1].id = 2;
2597
2598        let sheets = vec![sheet1];
2599
2600        let f1 = compile_formula("=Sheet1[Sales]", &sheets);
2601        assert_eq!(f1.parts.len(), 2);
2602        match &f1.parts[1] {
2603            FormulaPart::StructuredReference {
2604                sheet_id,
2605                col_id,
2606                is_this_row,
2607                section,
2608            } => {
2609                assert_eq!(*sheet_id, 123);
2610                assert_eq!(*col_id, Some(1));
2611                assert!(!is_this_row);
2612                assert_eq!(*section, SheetSection::Data);
2613            }
2614            _ => panic!("Expected StructuredReference, got {:?}", f1.parts[1]),
2615        }
2616
2617        let s1 = serialize_formula(&f1, &sheets);
2618        assert_eq!(s1, "=[Sales]");
2619
2620        let f2 = compile_formula("=[@Cost]", &sheets);
2621        match &f2.parts[1] {
2622            FormulaPart::StructuredReference {
2623                sheet_id,
2624                col_id,
2625                is_this_row,
2626                section,
2627            } => {
2628                assert_eq!(*sheet_id, 123);
2629                assert_eq!(*col_id, Some(2));
2630                assert!(is_this_row);
2631                assert_eq!(*section, SheetSection::Data);
2632            }
2633            _ => panic!("Expected StructuredReference"),
2634        }
2635        let s2 = serialize_formula(&f2, &sheets);
2636        assert_eq!(s2, "=[@Cost]");
2637
2638        let f3 = compile_formula("=Sheet1[[#Headers],[Sales]]", &sheets);
2639        match &f3.parts[1] {
2640            FormulaPart::StructuredReference {
2641                sheet_id,
2642                col_id,
2643                section,
2644                ..
2645            } => {
2646                assert_eq!(*sheet_id, 123);
2647                assert_eq!(*col_id, Some(1));
2648                assert_eq!(*section, SheetSection::Headers);
2649            }
2650            _ => panic!("Expected StructuredReference"),
2651        }
2652        let s3 = serialize_formula(&f3, &sheets);
2653        assert_eq!(s3, "=[[#Headers], [Sales]]");
2654
2655        let mut sheet2 = Sheet::new(crate::core::SheetInit {
2656            id: Some(456),
2657            name: Some("Sheet2".to_string()),
2658            rows: 5,
2659            cols: 2,
2660        });
2661        sheet2.columns[0].name = "Revenue".to_string();
2662        sheet2.columns[0].id = 3;
2663        sheet2.columns[1].name = "Expenses".to_string();
2664        sheet2.columns[1].id = 4;
2665
2666        let multi_sheets = vec![sheets[0].clone(), sheet2];
2667        let f4 = compile_formula("=Sheet2[Revenue]", &multi_sheets);
2668        let s4 = serialize_formula(&f4, &multi_sheets);
2669        assert_eq!(s4, "=Sheet2[Revenue]");
2670
2671        let ast = parse_excel_formula("Sheet1[@Sales] + 10").unwrap();
2672        match ast {
2673            Expr::BinaryOp { op, left, right: _ } => {
2674                assert_eq!(op, Op::Add);
2675                match &*left {
2676                    Expr::StructuredRef {
2677                        sheet,
2678                        column,
2679                        is_this_row,
2680                        ..
2681                    } => {
2682                        assert_eq!(sheet.as_deref(), Some("Sheet1"));
2683                        assert_eq!(column.as_deref(), Some("Sales"));
2684                        assert!(is_this_row);
2685                    }
2686                    _ => panic!("Expected StructuredRef"),
2687                }
2688            }
2689            _ => panic!("Expected BinaryOp"),
2690        }
2691    }
2692
2693    #[test]
2694    fn test_structured_reference_whole_row_no_column() {
2695        let mut sheet1 = Sheet::new(crate::core::SheetInit {
2696            id: Some(123),
2697            name: Some("Sheet1".to_string()),
2698            rows: 5,
2699            cols: 2,
2700        });
2701        sheet1.columns[0].name = "Sales".to_string();
2702        sheet1.columns[0].id = 1;
2703        sheet1.columns[1].name = "Cost".to_string();
2704        sheet1.columns[1].id = 2;
2705        let sheets = vec![sheet1];
2706
2707        let f = compile_formula("=[@]", &sheets);
2708        match &f.parts[1] {
2709            FormulaPart::StructuredReference {
2710                col_id,
2711                is_this_row,
2712                section,
2713                ..
2714            } => {
2715                assert_eq!(*col_id, None);
2716                assert!(is_this_row);
2717                assert_eq!(*section, SheetSection::Data);
2718            }
2719            _ => panic!("Expected StructuredReference, got {:?}", f.parts[1]),
2720        }
2721        assert_eq!(serialize_formula(&f, &sheets), "=[@]");
2722
2723        let ast = parse_excel_formula("[@]").unwrap();
2724        match ast {
2725            Expr::StructuredRef {
2726                column,
2727                is_this_row,
2728                ..
2729            } => {
2730                assert_eq!(column, None);
2731                assert!(is_this_row);
2732            }
2733            _ => panic!("Expected StructuredRef"),
2734        }
2735    }
2736
2737    #[test]
2738    fn test_structured_reference_whole_table_sections_no_column() {
2739        let mut sheet1 = Sheet::new(crate::core::SheetInit {
2740            id: Some(123),
2741            name: Some("Sheet1".to_string()),
2742            rows: 5,
2743            cols: 2,
2744        });
2745        sheet1.columns[0].name = "Sales".to_string();
2746        sheet1.columns[1].name = "Cost".to_string();
2747        let sheets = vec![sheet1];
2748
2749        for (input, expected_section) in [
2750            ("=[#Data]", SheetSection::Data),
2751            ("=[#All]", SheetSection::All),
2752            ("=[#Headers]", SheetSection::Headers),
2753            ("=[#Totals]", SheetSection::Totals),
2754        ] {
2755            let f = compile_formula(input, &sheets);
2756            match &f.parts[1] {
2757                FormulaPart::StructuredReference {
2758                    col_id,
2759                    is_this_row,
2760                    section,
2761                    ..
2762                } => {
2763                    assert_eq!(*col_id, None, "input: {input}");
2764                    assert!(!is_this_row, "input: {input}");
2765                    assert_eq!(*section, expected_section, "input: {input}");
2766                }
2767                _ => panic!(
2768                    "Expected StructuredReference for {input}, got {:?}",
2769                    f.parts[1]
2770                ),
2771            }
2772            assert_eq!(serialize_formula(&f, &sheets), input);
2773        }
2774    }
2775
2776    #[test]
2777    fn test_rewrite_structured_table_reference_renames_table() {
2778        let rewritten = rewrite_structured_table_reference(
2779            "=SUM(Sales[Amount])",
2780            "Sales",
2781            Some("Revenue"),
2782            None,
2783        );
2784        assert_eq!(rewritten.as_deref(), Some("=SUM(Revenue[Amount])"));
2785    }
2786
2787    #[test]
2788    fn test_rewrite_structured_table_reference_renames_column() {
2789        let rewritten = rewrite_structured_table_reference(
2790            "=SUM(Sales[Amount])",
2791            "Sales",
2792            None,
2793            Some(("Amount", "Total")),
2794        );
2795        assert_eq!(rewritten.as_deref(), Some("=SUM(Sales[Total])"));
2796    }
2797
2798    #[test]
2799    fn test_rewrite_structured_table_reference_handles_multiple_refs_and_forms() {
2800        let rewritten = rewrite_structured_table_reference(
2801            "=Sales[@Amount] + SUM(Sales[Amount]) + Sales[[#Headers],[Amount]]",
2802            "Sales",
2803            Some("Revenue"),
2804            Some(("Amount", "Total")),
2805        );
2806        assert_eq!(
2807            rewritten.as_deref(),
2808            Some("=Revenue[@Total] + SUM(Revenue[Total]) + Revenue[[#Headers], [Total]]")
2809        );
2810    }
2811
2812    #[test]
2813    fn test_rewrite_structured_table_reference_ignores_other_tables_and_columns() {
2814        let rewritten = rewrite_structured_table_reference(
2815            "=SUM(Other[Amount]) + A1",
2816            "Sales",
2817            Some("Revenue"),
2818            None,
2819        );
2820        assert_eq!(rewritten, None);
2821
2822        let rewritten2 = rewrite_structured_table_reference(
2823            "=Sales[Quantity]",
2824            "Sales",
2825            None,
2826            Some(("Amount", "Total")),
2827        );
2828        assert_eq!(rewritten2, None);
2829    }
2830
2831    #[test]
2832    fn test_rewrite_structured_table_reference_ignores_non_formula_cells() {
2833        let rewritten = rewrite_structured_table_reference(
2834            "Sales[Amount] is a great product",
2835            "Sales",
2836            Some("Revenue"),
2837            None,
2838        );
2839        assert_eq!(rewritten, None);
2840    }
2841
2842    #[test]
2843    fn test_compile_formula_never_panics_on_unterminated_quote_ending_in_backslash() {
2844        let _ = compile_formula("=\"\\", &[]);
2845        let _ = compile_formula("=\"unterminated\\", &[]);
2846    }
2847
2848    #[test]
2849    fn test_rewrite_structured_table_reference_never_panics_on_unterminated_quote() {
2850        let _ = rewrite_structured_table_reference("=Sales[Amount]&\"\\", "Sales", None, None);
2851    }
2852
2853    #[test]
2854    fn an_error_value_lexes_as_a_literal_rather_than_as_punctuation() {
2855        assert_eq!(lex_eval("#REF!").unwrap(), vec![EvalToken::Error("#REF!")]);
2856        assert_eq!(
2857            lex_eval("#DIV/0!").unwrap(),
2858            vec![EvalToken::Error("#DIV/0!")]
2859        );
2860        assert_eq!(lex_eval("#N/A").unwrap(), vec![EvalToken::Error("#N/A")]);
2861        assert_eq!(lex_eval("#ref!").unwrap(), vec![EvalToken::Error("#REF!")]);
2862        assert_eq!(
2863            lex_eval("1+#REF!").unwrap(),
2864            vec![
2865                EvalToken::Number(1.0),
2866                EvalToken::Op(Op::Add),
2867                EvalToken::Error("#REF!"),
2868            ]
2869        );
2870    }
2871
2872    #[test]
2873    fn a_hash_that_starts_nothing_recognisable_is_left_alone() {
2874        assert!(match_error_code(&"#NOPE".chars().collect::<Vec<_>>(), 0).is_none());
2875        assert!(match_error_code(&"#RE".chars().collect::<Vec<_>>(), 0).is_none());
2876    }
2877
2878    #[test]
2879    fn ampersand_lexes_and_parses_as_concatenation() {
2880        assert_eq!(
2881            lex_eval("\"A\"&\"B\"").unwrap(),
2882            vec![
2883                EvalToken::String("A".to_string()),
2884                EvalToken::Op(Op::Concat),
2885                EvalToken::String("B".to_string()),
2886            ]
2887        );
2888
2889        assert_eq!(
2890            parse_excel_formula("\"x\" & 1 + 2").unwrap(),
2891            Expr::BinaryOp {
2892                op: Op::Concat,
2893                left: Box::new(Expr::String("x".to_string())),
2894                right: Box::new(Expr::BinaryOp {
2895                    op: Op::Add,
2896                    left: Box::new(Expr::Number(1.0)),
2897                    right: Box::new(Expr::Number(2.0)),
2898                }),
2899            }
2900        );
2901
2902        match parse_excel_formula("\"a\" & \"b\" = \"ab\"").unwrap() {
2903            Expr::BinaryOp { op, left, .. } => {
2904                assert_eq!(op, Op::Eq);
2905                assert!(matches!(*left, Expr::BinaryOp { op: Op::Concat, .. }));
2906            }
2907            other => panic!("Expected equality, got {other:?}"),
2908        }
2909    }
2910
2911    #[test]
2912    fn test_bracket_slice_parsing() {
2913        let ast = parse_excel_formula("arr[1:3]").unwrap();
2914        assert_eq!(
2915            ast,
2916            Expr::Slice {
2917                expr: Box::new(Expr::Identifier("arr".to_string())),
2918                start: Some(Box::new(Expr::Number(1.0))),
2919                end: Some(Box::new(Expr::Number(3.0))),
2920            }
2921        );
2922
2923        let ast_no_end = parse_excel_formula("arr[2:]").unwrap();
2924        assert_eq!(
2925            ast_no_end,
2926            Expr::Slice {
2927                expr: Box::new(Expr::Identifier("arr".to_string())),
2928                start: Some(Box::new(Expr::Number(2.0))),
2929                end: None,
2930            }
2931        );
2932
2933        let ast_no_start = parse_excel_formula("arr[:2]").unwrap();
2934        assert_eq!(
2935            ast_no_start,
2936            Expr::Slice {
2937                expr: Box::new(Expr::Identifier("arr".to_string())),
2938                start: None,
2939                end: Some(Box::new(Expr::Number(2.0))),
2940            }
2941        );
2942    }
2943
2944    #[test]
2945    fn test_sheet_column_bracket_indexing() {
2946        let ast = parse_excel_formula("Sheet1[\"Sales\"]").unwrap();
2947        assert_eq!(
2948            ast,
2949            Expr::StructuredRef {
2950                sheet: Some("Sheet1".to_string()),
2951                column: Some("Sales".to_string()),
2952                is_this_row: false,
2953                section: SheetSection::Data,
2954            }
2955        );
2956    }
2957
2958    #[test]
2959    fn at_lexes_and_parses_as_implicit_intersection() {
2960        assert_eq!(
2961            lex_eval("@A1:A3").unwrap(),
2962            vec![
2963                EvalToken::Op(Op::ImplicitIntersection),
2964                EvalToken::Identifier("A1".to_string()),
2965                EvalToken::Colon,
2966                EvalToken::Identifier("A3".to_string()),
2967            ]
2968        );
2969
2970        assert_eq!(
2971            parse_excel_formula("@A1:A3").unwrap(),
2972            Expr::UnaryOp {
2973                op: Op::ImplicitIntersection,
2974                expr: Box::new(Expr::RangeRef {
2975                    sheet: None,
2976                    start_row: 0,
2977                    start_col: 0,
2978                    end_row: 2,
2979                    end_col: 0,
2980                    start_row_abs: false,
2981                    start_col_abs: false,
2982                    end_row_abs: false,
2983                    end_col_abs: false,
2984                }),
2985            }
2986        );
2987    }
2988
2989    #[test]
2990    fn xlsx_spill_encoding_preserves_strings_errors_and_table_headers() {
2991        for (source, expected) in [
2992            ("SUM($A$1#)", "SUM(_xlfn.ANCHORARRAY($A$1))"),
2993            (
2994                "SUM('Data Sheet'!A1#,Sheet2!B2#)",
2995                "SUM(_xlfn.ANCHORARRAY('Data Sheet'!A1),_xlfn.ANCHORARRAY(Sheet2!B2))",
2996            ),
2997            (
2998                "IFERROR(A1#,\"A1#\"\"B2#\")",
2999                "IFERROR(_xlfn.ANCHORARRAY(A1),\"A1#\"\"B2#\")",
3000            ),
3001            (
3002                "SUM(Table1[[#Headers],[A1#]])",
3003                "SUM(Table1[[#Headers],[A1#]])",
3004            ),
3005            ("IFERROR(#REF!,\"#VALUE!\")", "IFERROR(#REF!,\"#VALUE!\")"),
3006            ("SUM(_xlfn.ANCHORARRAY(A1))", "SUM(_xlfn.ANCHORARRAY(A1))"),
3007        ] {
3008            assert_eq!(encode_xlsx_spill_references(source), expected, "{source}");
3009        }
3010    }
3011
3012    #[test]
3013    fn hash_lexes_and_parses_as_spill_operator_after_references() {
3014        assert_eq!(
3015            lex_eval("A1#").unwrap(),
3016            vec![
3017                EvalToken::Identifier("A1".to_string()),
3018                EvalToken::Op(Op::Spill),
3019            ]
3020        );
3021
3022        assert_eq!(
3023            parse_excel_formula("A1#").unwrap(),
3024            Expr::UnaryOp {
3025                op: Op::Spill,
3026                expr: Box::new(Expr::CellRef {
3027                    sheet: None,
3028                    row: 0,
3029                    col: 0,
3030                    row_abs: false,
3031                    col_abs: false,
3032                }),
3033            }
3034        );
3035    }
3036}