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