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