1use crate::intrinsics::fold;
27use crate::sheet::{column_letters, MAX_COLUMN, MAX_ROW};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum Kind {
32 Cell,
33 Range,
34 Column,
35 Row,
36 Name,
37 Structured,
38 External,
39 ThreeD,
41 Spill,
42 Unreadable,
44 Broken,
46}
47
48impl Kind {
49 pub fn word(self) -> &'static str {
51 match self {
52 Kind::Cell => "cell",
53 Kind::Range => "range",
54 Kind::Column => "column",
55 Kind::Row => "row",
56 Kind::Name => "name",
57 Kind::Structured => "structured",
58 Kind::External => "external",
59 Kind::ThreeD => "3d",
60 Kind::Spill => "spill",
61 Kind::Unreadable => "unreadable",
62 Kind::Broken => "broken",
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum Shape {
70 None,
71 Cell,
72 Range,
73 Column,
74 Row,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct FormulaRef {
81 pub kind: Kind,
82 pub shape: Shape,
83 pub book: String,
85 pub sheet_name: String,
88 pub written: String,
90 pub part: String,
93 pub row1: u32,
94 pub col1: u32,
95 pub row2: u32,
96 pub col2: u32,
97 pub row_abs1: bool,
98 pub col_abs1: bool,
99 pub row_abs2: bool,
100 pub col_abs2: bool,
101 pub pos: usize,
103 pub span: usize,
105 pub part_pos: usize,
107}
108
109impl FormulaRef {
110 fn at(pos: usize) -> FormulaRef {
111 FormulaRef {
112 kind: Kind::Name,
113 shape: Shape::None,
114 book: String::new(),
115 sheet_name: String::new(),
116 written: String::new(),
117 part: String::new(),
118 row1: 0,
119 col1: 0,
120 row2: 0,
121 col2: 0,
122 row_abs1: false,
123 col_abs1: false,
124 row_abs2: false,
125 col_abs2: false,
126 pos,
127 span: 0,
128 part_pos: pos,
129 }
130 }
131
132 pub fn unreadable_name(&self) -> &'static str {
134 if fold(&self.part) == "indirect" {
135 "INDIRECT"
136 } else {
137 "OFFSET"
138 }
139 }
140}
141
142pub fn scan(formula_text: &str) -> Vec<FormulaRef> {
149 let s: Vec<char> = formula_text.chars().collect();
150 let n = s.len();
151 let mut refs = Vec::new();
152 let mut i = 0;
153 if n >= 1 && s[0] == '=' {
154 i = 1;
155 }
156 while i < n {
157 if s[i] == '"' {
158 i = skip_delimited(&s, i, '"');
159 } else if !read_reference(&s, &mut i, &mut refs) {
160 i += 1;
161 }
162 }
163 refs
164}
165
166fn read_reference(s: &[char], i: &mut usize, refs: &mut Vec<FormulaRef>) -> bool {
171 let ch = s[*i];
172 let mut r = FormulaRef::at(*i);
173 let mut j = *i;
174 if read_qualifier(s, &mut j, &mut r) {
175 r.part_pos = j;
176 if read_part(s, &mut j, &mut r) {
177 finish_ref(s, j, r, refs);
178 }
179 *i = j;
180 return true;
181 }
182 if ch == '\'' {
183 *i = skip_delimited(s, *i, '\'');
184 true
185 } else if ch == '[' {
186 let end = bracket_group_end(s, *i);
187 r.kind = Kind::Structured;
188 r.part_pos = *i;
189 r.part = s[*i..end].iter().collect();
190 finish_ref(s, end, r, refs);
191 *i = end;
192 true
193 } else if ch == '#' || ch == '$' || is_ident_char(ch) {
194 r.part_pos = *i;
195 if read_part(s, &mut j, &mut r) {
196 finish_ref(s, j, r, refs);
197 }
198 if j == *i {
199 j = *i + 1;
200 }
201 *i = j;
202 true
203 } else {
204 false
205 }
206}
207
208fn read_qualifier(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
212 let n = s.len();
213 let ch = s[*j];
214 if ch == '\'' {
215 let Some(k) = delimited_end(s, *j, '\'') else {
216 return false;
217 };
218 if s.get(k) != Some(&'!') {
219 return false;
220 }
221 let inner: String = s[*j + 1..k - 1].iter().collect();
222 let q = inner.replace("''", "'");
223 match (q.find('['), q.find(']')) {
224 (Some(b), Some(e)) if e > b => {
225 r.book = q[b + 1..e].to_string();
226 r.sheet_name = q[e + 1..].to_string();
227 }
228 _ => r.sheet_name = q,
229 }
230 *j = k + 1;
231 return true;
232 }
233 let mut k = *j;
234 let mut e = 0usize;
235 if ch == '[' {
236 let Some(e_rel) = s[*j + 1..].iter().position(|&c| c == ']') else {
237 return false;
238 };
239 e = *j + 1 + e_rel;
240 if let Some(b_rel) = s[*j + 1..].iter().position(|&c| c == '[') {
241 if *j + 1 + b_rel < e {
242 return false; }
244 }
245 k = e + 1;
246 if s.get(k) == Some(&'!') {
247 r.book = s[*j + 1..e].iter().collect();
248 r.sheet_name.clear();
249 *j = k + 1;
250 return true;
251 }
252 }
253 let mut p = k;
254 while p < n && is_ident_char(s[p]) {
255 p += 1;
256 }
257 if p == k {
258 return false;
259 }
260 let mut q: String = s[k..p].iter().collect();
261 if s.get(p) == Some(&':') {
262 let mut p2 = p + 1;
263 while p2 < n && is_ident_char(s[p2]) {
264 p2 += 1;
265 }
266 if p2 > p + 1 && s.get(p2) == Some(&'!') {
267 q.push(':');
268 q.extend(s[p + 1..p2].iter());
269 p = p2;
270 }
271 }
272 if s.get(p) != Some(&'!') {
273 return false;
274 }
275 if ch == '[' {
276 r.book = s[*j + 1..e].iter().collect();
277 }
278 r.sheet_name = q;
279 *j = p + 1;
280 true
281}
282
283fn read_part(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
290 if s.get(*j) == Some(&'#') {
291 let k = error_literal_end(s, *j);
292 let tok: String = s[*j..k].iter().collect();
293 *j = k;
294 if fold(&tok) != "#ref!" {
295 return false;
296 }
297 r.kind = Kind::Broken;
298 r.part = tok;
299 if read_cell_like(s, j, r) {
300 r.part_pos = *j - r.part.chars().count();
301 }
302 return true;
303 }
304 let k = token_end(s, *j);
305 if k == *j {
306 return false;
307 }
308 let tok: String = s[*j..k].iter().collect();
309 let nxt = s.get(k).copied();
310 if nxt == Some('(') {
311 *j = k;
312 let f = fold(&tok);
313 if f == "indirect" || f == "offset" {
314 r.kind = Kind::Unreadable;
315 r.part = tok;
316 return true;
317 }
318 return false;
319 }
320 if nxt == Some('[') {
321 let end = bracket_group_end(s, k);
322 r.kind = Kind::Structured;
323 r.part = s[*j..end].iter().collect();
324 *j = end;
325 return true;
326 }
327 if read_cell_like(s, j, r) {
328 r.kind = match r.shape {
329 Shape::Cell => {
330 if s.get(*j) == Some(&'#') {
331 r.part.push('#');
332 *j += 1;
333 Kind::Spill
334 } else {
335 Kind::Cell
336 }
337 }
338 Shape::Range => Kind::Range,
339 Shape::Column => Kind::Column,
340 Shape::Row | Shape::None => Kind::Row,
341 };
342 return true;
343 }
344 let mut first = tok.chars().next();
345 if first == Some('$') {
346 first = tok.chars().nth(1);
347 }
348 if first == Some('.') || first.is_some_and(|c| c.is_ascii_digit()) {
349 *j = k; return false;
351 }
352 let f = fold(&tok);
353 if f == "true" || f == "false" {
354 *j = k;
355 return false;
356 }
357 r.kind = Kind::Name;
358 r.part = tok;
359 *j = k;
360 if s.get(*j) == Some(&'#') {
361 r.kind = Kind::Spill;
362 r.part.push('#');
363 *j += 1;
364 }
365 true
366}
367
368fn read_cell_like(s: &[char], j: &mut usize, r: &mut FormulaRef) -> bool {
373 let k = token_end(s, *j);
374 if k == *j {
375 return false;
376 }
377 let tok: String = s[*j..k].iter().collect();
378 let mut tok2 = String::new();
379 let mut k2 = k;
380 if s.get(k) == Some(&':') {
381 k2 = token_end(s, k + 1);
382 if k2 > k + 1 {
383 tok2 = s[k + 1..k2].iter().collect();
384 }
385 }
386 let mut shape = Shape::None;
387 let (mut r1, mut c1, mut r2, mut c2) = (0u32, 0u32, 0u32, 0u32);
388 let (mut ra1, mut ca1, mut ra2, mut ca2) = (false, false, false, false);
389 if let Some((row, col, ra, ca)) = parse_cell_part(&tok) {
390 r1 = row;
391 c1 = col;
392 ra1 = ra;
393 ca1 = ca;
394 shape = Shape::Cell;
395 if !tok2.is_empty() {
396 if let Some((row, col, ra, ca)) = parse_cell_part(&tok2) {
397 r2 = row;
398 c2 = col;
399 ra2 = ra;
400 ca2 = ca;
401 shape = Shape::Range;
402 }
403 }
404 } else if !tok2.is_empty() {
405 if let (Some((col, ca)), Some((col2, ca_two))) =
406 (parse_col_part(&tok), parse_col_part(&tok2))
407 {
408 c1 = col;
409 ca1 = ca;
410 c2 = col2;
411 ca2 = ca_two;
412 shape = Shape::Column;
413 } else if let (Some((row, ra)), Some((row2, ra_two))) =
414 (parse_row_part(&tok), parse_row_part(&tok2))
415 {
416 r1 = row;
417 ra1 = ra;
418 r2 = row2;
419 ra2 = ra_two;
420 shape = Shape::Row;
421 }
422 }
423 if shape == Shape::None {
424 return false;
425 }
426 r.shape = shape;
427 r.row1 = r1;
428 r.col1 = c1;
429 r.row_abs1 = ra1;
430 r.col_abs1 = ca1;
431 if shape == Shape::Cell {
432 r.part = tok;
433 *j = k;
434 } else {
435 r.row2 = r2;
436 r.col2 = c2;
437 r.row_abs2 = ra2;
438 r.col_abs2 = ca2;
439 r.part = s[*j..k2].iter().collect();
440 *j = k2;
441 }
442 true
443}
444
445fn finish_ref(s: &[char], j: usize, mut r: FormulaRef, refs: &mut Vec<FormulaRef>) {
447 r.span = j - r.pos;
448 r.written = s[r.pos..j].iter().collect();
449 if !r.book.is_empty() {
450 r.kind = Kind::External;
451 } else if r.sheet_name.contains(':') {
452 r.kind = Kind::ThreeD;
453 }
454 refs.push(r);
455}
456
457fn parse_cell_part(tok: &str) -> Option<(u32, u32, bool, bool)> {
464 let c: Vec<char> = tok.chars().collect();
465 let n = c.len();
466 let mut p = 0;
467 let mut col_abs = false;
468 if c.first() == Some(&'$') {
469 col_abs = true;
470 p = 1;
471 }
472 let mut k = p;
473 while k < n && c[k].is_ascii_alphabetic() {
474 k += 1;
475 }
476 let letters: String = c[p..k].iter().collect();
477 if letters.is_empty() || letters.len() > 3 {
478 return None;
479 }
480 let mut row_abs = false;
481 if c.get(k) == Some(&'$') {
482 row_abs = true;
483 k += 1;
484 }
485 let p2 = k;
486 while k < n && c[k].is_ascii_digit() {
487 k += 1;
488 }
489 let digits: String = c[p2..k].iter().collect();
490 if digits.is_empty() || digits.len() > 7 {
491 return None;
492 }
493 if k < n {
494 return None; }
496 let col = letters_to_col(&letters)?;
497 let row: u32 = digits.parse().ok()?;
498 if row == 0 || row > MAX_ROW {
499 return None;
500 }
501 Some((row, col, row_abs, col_abs))
502}
503
504fn parse_col_part(tok: &str) -> Option<(u32, bool)> {
506 let (letters, abs) = match tok.strip_prefix('$') {
507 Some(rest) => (rest, true),
508 None => (tok, false),
509 };
510 let count = letters.chars().count();
511 if count == 0 || count > 3 || !letters.chars().all(|c| c.is_ascii_alphabetic()) {
512 return None;
513 }
514 Some((letters_to_col(letters)?, abs))
515}
516
517fn parse_row_part(tok: &str) -> Option<(u32, bool)> {
519 let (digits, abs) = match tok.strip_prefix('$') {
520 Some(rest) => (rest, true),
521 None => (tok, false),
522 };
523 let count = digits.chars().count();
524 if count == 0 || count > 7 || !digits.chars().all(|c| c.is_ascii_digit()) {
525 return None;
526 }
527 let row: u32 = digits.parse().ok()?;
528 if row == 0 || row > MAX_ROW {
529 return None;
530 }
531 Some((row, abs))
532}
533
534pub(crate) fn letters_to_col(letters: &str) -> Option<u32> {
538 let mut v: u32 = 0;
539 for ch in fold(letters).chars() {
540 let c = ch as u32;
541 if !(97..=122).contains(&c) {
542 return None;
543 }
544 v = v * 26 + (c - 96);
545 }
546 if v == 0 || v > MAX_COLUMN {
547 return None;
548 }
549 Some(v)
550}
551
552fn token_end(s: &[char], j: usize) -> usize {
558 let mut k = j;
559 while k < s.len() && (s[k] == '$' || is_ident_char(s[k])) {
560 k += 1;
561 }
562 k
563}
564
565fn error_literal_end(s: &[char], j: usize) -> usize {
568 let mut k = j + 1;
569 while k < s.len() && (s[k].is_ascii_alphanumeric() || s[k] == '/' || s[k] == '_') {
570 k += 1;
571 }
572 if matches!(s.get(k), Some('!') | Some('?')) {
573 k += 1;
574 }
575 k
576}
577
578fn delimited_end(s: &[char], j: usize, d: char) -> Option<usize> {
581 let n = s.len();
582 let mut k = j + 1;
583 while k < n {
584 if s[k] != d {
585 k += 1;
586 } else if s.get(k + 1) == Some(&d) {
587 k += 2;
588 } else {
589 return Some(k + 1);
590 }
591 }
592 None
593}
594
595pub fn skip_delimited(s: &[char], j: usize, d: char) -> usize {
599 delimited_end(s, j, d).unwrap_or(s.len())
600}
601
602pub fn bracket_group_end(s: &[char], i: usize) -> usize {
606 let n = s.len();
607 let mut k = i;
608 let mut depth: i32 = 0;
609 while k < n {
610 let ch = s[k];
611 if ch == '\'' {
612 k += 2;
613 } else {
614 if ch == '[' {
615 depth += 1;
616 }
617 if ch == ']' {
618 depth -= 1;
619 }
620 k += 1;
621 if depth == 0 {
622 return k;
623 }
624 }
625 }
626 n
627}
628
629pub fn is_ident_char(ch: char) -> bool {
632 ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || (ch as u32) > 127
633}
634
635pub fn spell(r: &FormulaRef, home_sheet: &str) -> String {
642 let q = if r.sheet_name.is_empty() {
643 format!("{}!", quote_sheet(home_sheet))
644 } else {
645 format!("{}!", quote_sheet(&r.sheet_name))
646 };
647 match r.kind {
648 Kind::Cell | Kind::Range | Kind::Column | Kind::Row => format!("{q}{}", spelled_part(r)),
649 Kind::Spill => {
650 if r.shape == Shape::Cell {
651 format!("{q}{}#", spelled_part(r))
652 } else {
653 r.written.clone()
654 }
655 }
656 Kind::Broken => {
657 if r.shape != Shape::None {
658 r.written.clone()
659 } else {
660 format!("{q}#REF!")
661 }
662 }
663 Kind::Unreadable => format!("(unreadable \"{}\")", r.unreadable_name()),
664 Kind::Name | Kind::Structured | Kind::External | Kind::ThreeD => r.written.clone(),
665 }
666}
667
668fn spelled_part(r: &FormulaRef) -> String {
670 match r.shape {
671 Shape::Cell => format!("{}{}", column_letters(r.col1), r.row1),
672 Shape::Range => format!(
673 "{}{}:{}{}",
674 column_letters(r.col1),
675 r.row1,
676 column_letters(r.col2),
677 r.row2
678 ),
679 Shape::Column => format!("{}:{}", column_letters(r.col1), column_letters(r.col2)),
680 Shape::Row => format!("{}:{}", r.row1, r.row2),
681 Shape::None => String::new(),
682 }
683}
684
685pub fn quote_sheet(sheet_name: &str) -> String {
688 if sheet_needs_quotes(sheet_name) {
689 format!("'{}'", sheet_name.replace('\'', "''"))
690 } else {
691 sheet_name.to_string()
692 }
693}
694
695fn sheet_needs_quotes(sheet_name: &str) -> bool {
699 let Some(first) = sheet_name.chars().next() else {
700 return true;
701 };
702 if first.is_ascii_digit() {
703 return true;
704 }
705 if !sheet_name.chars().all(is_ident_char) {
706 return true;
707 }
708 if parse_cell_part(sheet_name).is_some() {
709 return true;
710 }
711 is_r1c1_shaped(sheet_name)
712}
713
714fn is_r1c1_shaped(sheet_name: &str) -> bool {
716 let f = fold(sheet_name);
717 let mut chars = f.chars();
718 let mut seen_c = match chars.next() {
719 Some('c') => true,
720 Some('r') => false,
721 _ => return false,
722 };
723 for ch in chars {
724 if ch == 'c' && !seen_c {
725 seen_c = true;
726 } else if !ch.is_ascii_digit() {
727 return false;
728 }
729 }
730 true
731}
732
733fn rewrite(formula_text: &str, render: impl Fn(&FormulaRef) -> String) -> String {
740 let chars: Vec<char> = formula_text.chars().collect();
741 let mut out = String::with_capacity(formula_text.len() + 8);
742 let mut p = 0;
743 for r in &scan(formula_text) {
744 if r.shape != Shape::None {
745 out.extend(chars[p..r.part_pos].iter());
746 out.push_str(&render(r));
747 if r.kind == Kind::Spill {
748 out.push('#');
749 }
750 p = r.pos + r.span;
751 }
752 }
753 out.extend(chars[p..].iter());
754 out
755}
756
757pub fn r1c1(formula_text: &str, home_row: u32, home_col: u32) -> String {
760 rewrite(formula_text, |r| r1c1_part(r, home_row, home_col))
761}
762
763fn r1c1_part(r: &FormulaRef, home_row: u32, home_col: u32) -> String {
766 let cell = |row: u32, ra: bool, col: u32, ca: bool| {
767 format!(
768 "R{}C{}",
769 rel_spell(row, ra, home_row),
770 rel_spell(col, ca, home_col)
771 )
772 };
773 match r.shape {
774 Shape::Cell => cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
775 Shape::Range => format!(
776 "{}:{}",
777 cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
778 cell(r.row2, r.row_abs2, r.col2, r.col_abs2)
779 ),
780 Shape::Column => {
781 let a = format!("C{}", rel_spell(r.col1, r.col_abs1, home_col));
782 let b = format!("C{}", rel_spell(r.col2, r.col_abs2, home_col));
783 if a == b {
784 a
785 } else {
786 format!("{a}:{b}")
787 }
788 }
789 Shape::Row => {
790 let a = format!("R{}", rel_spell(r.row1, r.row_abs1, home_row));
791 let b = format!("R{}", rel_spell(r.row2, r.row_abs2, home_row));
792 if a == b {
793 a
794 } else {
795 format!("{a}:{b}")
796 }
797 }
798 Shape::None => String::new(),
799 }
800}
801
802fn rel_spell(n: u32, is_abs: bool, home: u32) -> String {
804 if is_abs {
805 n.to_string()
806 } else if n == home {
807 String::new()
808 } else {
809 format!("[{}]", i64::from(n) - i64::from(home))
810 }
811}
812
813pub fn shift_a1_references(formula: &str, d_row: i64, d_col: i64) -> String {
819 if d_row == 0 && d_col == 0 {
820 return formula.to_string();
821 }
822 #[cfg(test)]
823 crate::sheet::work::moved();
824 rewrite(formula, |r| shifted_part(r, d_row, d_col))
825}
826
827pub(crate) fn moves_whole(formula: &str, low: (i64, i64), high: (i64, i64)) -> bool {
837 let on = |n: u32, is_abs: bool, (lo, hi): (i64, i64), max: u32| {
838 shifted(n, is_abs, lo, max).is_some() && shifted(n, is_abs, hi, max).is_some()
839 };
840 let rows = (low.0, high.0);
841 let cols = (low.1, high.1);
842 scan(formula).iter().all(|r| {
843 if r.kind == Kind::ThreeD || (r.kind == Kind::Broken && r.shape != Shape::None) {
844 return false;
845 }
846 match r.shape {
847 Shape::None => true,
848 Shape::Cell => {
849 on(r.row1, r.row_abs1, rows, MAX_ROW) && on(r.col1, r.col_abs1, cols, MAX_COLUMN)
850 }
851 Shape::Range => {
852 on(r.row1, r.row_abs1, rows, MAX_ROW)
853 && on(r.col1, r.col_abs1, cols, MAX_COLUMN)
854 && on(r.row2, r.row_abs2, rows, MAX_ROW)
855 && on(r.col2, r.col_abs2, cols, MAX_COLUMN)
856 }
857 Shape::Column => {
858 on(r.col1, r.col_abs1, cols, MAX_COLUMN) && on(r.col2, r.col_abs2, cols, MAX_COLUMN)
859 }
860 Shape::Row => {
861 on(r.row1, r.row_abs1, rows, MAX_ROW) && on(r.row2, r.row_abs2, rows, MAX_ROW)
862 }
863 }
864 })
865}
866
867fn shifted(n: u32, is_abs: bool, d: i64, max: u32) -> Option<u32> {
868 if is_abs {
869 return Some(n);
870 }
871 let m = i64::from(n) + d;
872 (m >= 1 && m <= i64::from(max)).then_some(m as u32)
873}
874
875fn mark(is_abs: bool) -> &'static str {
876 if is_abs {
877 "$"
878 } else {
879 ""
880 }
881}
882
883fn shifted_part(r: &FormulaRef, d_row: i64, d_col: i64) -> String {
884 let cell = |row: u32, ra: bool, col: u32, ca: bool| -> Option<String> {
885 let col = shifted(col, ca, d_col, MAX_COLUMN)?;
886 let row = shifted(row, ra, d_row, MAX_ROW)?;
887 Some(format!(
888 "{}{}{}{}",
889 mark(ca),
890 column_letters(col),
891 mark(ra),
892 row
893 ))
894 };
895 let rendered = match r.shape {
896 Shape::Cell => cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
897 Shape::Range => match (
898 cell(r.row1, r.row_abs1, r.col1, r.col_abs1),
899 cell(r.row2, r.row_abs2, r.col2, r.col_abs2),
900 ) {
901 (Some(a), Some(b)) => Some(format!("{a}:{b}")),
902 _ => None,
903 },
904 Shape::Column => match (
905 shifted(r.col1, r.col_abs1, d_col, MAX_COLUMN),
906 shifted(r.col2, r.col_abs2, d_col, MAX_COLUMN),
907 ) {
908 (Some(a), Some(b)) => Some(format!(
909 "{}{}:{}{}",
910 mark(r.col_abs1),
911 column_letters(a),
912 mark(r.col_abs2),
913 column_letters(b)
914 )),
915 _ => None,
916 },
917 Shape::Row => match (
918 shifted(r.row1, r.row_abs1, d_row, MAX_ROW),
919 shifted(r.row2, r.row_abs2, d_row, MAX_ROW),
920 ) {
921 (Some(a), Some(b)) => Some(format!(
922 "{}{}:{}{}",
923 mark(r.row_abs1),
924 a,
925 mark(r.row_abs2),
926 b
927 )),
928 _ => None,
929 },
930 Shape::None => None,
931 };
932 rendered.unwrap_or_else(|| "#REF!".to_string())
933}
934
935pub fn resolve_books(formula_text: &str, books: &[String]) -> String {
940 let chars: Vec<char> = formula_text.chars().collect();
941 let mut out = String::with_capacity(formula_text.len() + 16);
942 let mut p = 0;
943 for r in &scan(formula_text) {
944 if r.kind != Kind::External || r.book.is_empty() {
945 continue;
946 }
947 if !r.book.chars().all(|c| c.is_ascii_digit()) {
948 continue;
949 }
950 let Some(name) = r
951 .book
952 .parse::<usize>()
953 .ok()
954 .and_then(|n| n.checked_sub(1))
955 .and_then(|i| books.get(i))
956 .filter(|name| !name.is_empty())
957 else {
958 continue;
959 };
960 let Some(open) = r.written.find('[') else {
962 continue;
963 };
964 let book_len = r.book.len();
965 let mut token = String::with_capacity(r.written.len() + name.len());
966 token.push_str(&r.written[..open + 1]);
967 token.push_str(name);
968 token.push_str(&r.written[open + 1 + book_len..]);
969 out.extend(chars[p..r.pos].iter());
970 out.push_str(&token);
971 p = r.pos + r.span;
972 }
973 out.extend(chars[p..].iter());
974 out
975}
976
977pub fn parse_home(home_text: &str) -> Option<(String, u32, u32)> {
982 let refs = scan(home_text);
983 if refs.len() != 1 {
984 return None;
985 }
986 let r = &refs[0];
987 if r.kind != Kind::Cell || r.sheet_name.is_empty() {
988 return None;
989 }
990 if r.pos != 0 || r.span != home_text.chars().count() {
991 return None;
992 }
993 Some((r.sheet_name.clone(), r.row1, r.col1))
994}
995
996pub fn report_for(body: &[&str]) -> String {
1005 let home_text = body.first().copied().unwrap_or("");
1006 let mut formula: String = if body.len() > 1 {
1007 body[1..].join("\n")
1008 } else {
1009 String::new()
1010 };
1011 while formula.ends_with('\n') {
1012 formula.pop();
1013 }
1014 let Some((home_sheet, home_row, home_col)) = parse_home(home_text) else {
1015 return format!("BAD HOME\t{home_text}\r\n");
1016 };
1017 let refs = scan(&formula);
1018 let mut out = String::new();
1019 if refs.is_empty() {
1020 out.push_str("NONE\r\n");
1021 } else {
1022 for r in &refs {
1023 out.push_str(&format!(
1024 "{}\t{}\t{}\r\n",
1025 r.kind.word(),
1026 r.written,
1027 spell(r, &home_sheet)
1028 ));
1029 }
1030 }
1031 out.push_str(&format!("R1C1\t{}\r\n", r1c1(&formula, home_row, home_col)));
1032 out
1033}
1034
1035pub fn fixture_report(fixture: &str) -> String {
1038 let text = fixture.replace("\r\n", "\n");
1039 let mut out = String::new();
1040 let mut header: Option<&str> = None;
1041 let mut body: Vec<&str> = Vec::new();
1042 for line in text.split('\n') {
1043 if line.starts_with("=== ") {
1044 if let Some(h) = header {
1045 out.push_str(h);
1046 out.push_str("\r\n");
1047 out.push_str(&report_for(&body));
1048 }
1049 header = Some(line);
1050 body.clear();
1051 } else if header.is_some() {
1052 body.push(line);
1053 }
1054 }
1055 if let Some(h) = header {
1056 out.push_str(h);
1057 out.push_str("\r\n");
1058 out.push_str(&report_for(&body));
1059 }
1060 out
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066
1067 const FIXTURE: &str = include_str!("../testdata/refers.txt");
1068 const GOLDEN: &str = include_str!("../testdata/refers_golden.txt");
1069
1070 #[test]
1071 fn the_refers_golden_is_reproduced() {
1072 let norm = |s: &str| s.replace("\r\n", "\n").trim_end_matches('\n').to_string();
1077 let got = norm(&fixture_report(FIXTURE));
1078 let want = norm(GOLDEN);
1079 if got != want {
1080 let at = got
1081 .chars()
1082 .zip(want.chars())
1083 .position(|(a, b)| a != b)
1084 .unwrap_or(got.len().min(want.len()));
1085 let line = want[..at].matches('\n').count() + 1;
1086 let context = |s: &str| {
1087 let start = s[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
1088 let end = s[at..].find('\n').map(|i| at + i).unwrap_or(s.len());
1089 s[start..end].to_string()
1090 };
1091 panic!(
1092 "the refers golden differs at char {at} (line {line}):\n want: {}\n got: {}",
1093 context(&want),
1094 context(&got)
1095 );
1096 }
1097 }
1098
1099 #[test]
1102 #[ignore]
1103 fn print_refers_report() {
1104 print!("{}", fixture_report(FIXTURE));
1105 }
1106
1107 #[test]
1108 fn the_record_carries_its_fields() {
1109 let refs = scan("='Q1 Data'!$A$1:B2");
1110 assert_eq!(refs.len(), 1);
1111 let r = &refs[0];
1112 assert_eq!((r.kind, r.shape), (Kind::Range, Shape::Range));
1113 assert_eq!((r.sheet_name.as_str(), r.book.as_str()), ("Q1 Data", ""));
1114 assert_eq!(r.part, "$A$1:B2");
1115 assert_eq!((r.row1, r.col1, r.row2, r.col2), (1, 1, 2, 2));
1116 assert_eq!(
1117 (r.row_abs1, r.col_abs1, r.row_abs2, r.col_abs2),
1118 (true, true, false, false)
1119 );
1120 assert_eq!((r.pos, r.span, r.part_pos), (1, 17, 11));
1121 let refs = scan("=#REF!A1");
1122 assert_eq!((refs[0].kind, refs[0].shape), (Kind::Broken, Shape::Cell));
1123 assert_eq!((refs[0].part.as_str(), refs[0].part_pos), ("A1", 6));
1124 let refs = scan("=INDIRECT(A1)");
1125 assert_eq!(refs.len(), 2);
1126 assert_eq!(refs[0].kind, Kind::Unreadable);
1127 assert_eq!(refs[0].unreadable_name(), "INDIRECT");
1128 assert_eq!(refs[1].kind, Kind::Cell);
1129 assert_eq!(scan("A1+B1").len(), 2);
1130 assert!(scan("=1+1").is_empty());
1131 }
1132
1133 #[test]
1134 fn the_home_cell_parses() {
1135 assert_eq!(parse_home("Model!B3"), Some(("Model".to_string(), 3, 2)));
1136 assert_eq!(
1137 parse_home("'Q1 Data'!C5"),
1138 Some(("Q1 Data".to_string(), 5, 3))
1139 );
1140 assert_eq!(parse_home("B3"), None);
1141 assert_eq!(parse_home("Model!A:A"), None);
1142 assert_eq!(parse_home("Model!B3+1"), None);
1143 }
1144
1145 #[test]
1146 fn a_sheet_is_quoted_as_a_reference_quotes_it() {
1147 assert_eq!(quote_sheet("Data"), "Data");
1148 assert_eq!(quote_sheet("Q1 Data"), "'Q1 Data'");
1149 assert_eq!(quote_sheet("It's"), "'It''s'");
1150 assert_eq!(quote_sheet("A1"), "'A1'");
1151 assert_eq!(quote_sheet("2024"), "'2024'");
1152 assert_eq!(quote_sheet("Q1.Data"), "Q1.Data");
1153 assert_eq!(quote_sheet("R1C1"), "'R1C1'");
1154 assert_eq!(quote_sheet("RC"), "'RC'");
1155 assert_eq!(quote_sheet("Tax_Rate"), "Tax_Rate");
1156 assert_eq!(quote_sheet("Umsatz\u{d6}"), "Umsatz\u{d6}");
1157 assert_eq!(quote_sheet(""), "''");
1158 }
1159
1160 #[test]
1161 fn a_name_past_ascii_is_a_name_and_a_sheet_past_ascii_is_a_sheet() {
1162 let refs = scan("=Umsatz_\u{d6}*2");
1163 assert_eq!((refs.len(), refs[0].kind), (1, Kind::Name));
1164 assert_eq!(refs[0].written, "Umsatz_\u{d6}");
1165 let refs = scan("=\u{dc}bersicht!A1");
1166 assert_eq!(
1167 (refs[0].kind, refs[0].sheet_name.as_str()),
1168 (Kind::Cell, "\u{dc}bersicht")
1169 );
1170 assert_eq!(spell(&refs[0], "Model"), "\u{dc}bersicht!A1");
1171 assert_eq!(
1172 r1c1("=\u{dc}bersicht!A1", 3, 2),
1173 "=\u{dc}bersicht!R[-2]C[-1]"
1174 );
1175 }
1176
1177 #[test]
1178 fn books_resolve_to_their_file_names() {
1179 let books = vec!["Rates.xlsx".to_string(), String::new()];
1180 assert_eq!(
1181 resolve_books("=[1]Sheet1!A1+[1]!Rate", &books),
1182 "=[Rates.xlsx]Sheet1!A1+[Rates.xlsx]!Rate"
1183 );
1184 assert_eq!(
1185 resolve_books("='[1]Q1 Data'!A1", &books),
1186 "='[Rates.xlsx]Q1 Data'!A1"
1187 );
1188 assert_eq!(resolve_books("=[2]Sheet1!A1", &books), "=[2]Sheet1!A1");
1189 assert_eq!(resolve_books("=[3]Sheet1!A1", &books), "=[3]Sheet1!A1");
1190 assert_eq!(
1191 resolve_books("=[Book.xlsx]Sheet1!A1", &books),
1192 "=[Book.xlsx]Sheet1!A1"
1193 );
1194 assert_eq!(resolve_books("=A1+[Amount]", &books), "=A1+[Amount]");
1195 }
1196
1197 fn s(f: &str, r: i64, c: i64) -> String {
1199 shift_a1_references(f, r, c)
1200 }
1201
1202 #[test]
1203 fn relative_parts_move_and_absolute_parts_stay() {
1204 assert_eq!(s("B2+B3", 1, 0), "B3+B4");
1205 assert_eq!(s("$B$2+B$2+$B2", 1, 1), "$B$2+C$2+$B3");
1206 assert_eq!(s("SUM(A1:A3)", 0, 1), "SUM(B1:B3)");
1207 assert_eq!(
1208 s("IFS(B2>3,\"big\",TRUE,\"small\")", 2, 0),
1209 "IFS(B4>3,\"big\",TRUE,\"small\")"
1210 );
1211 assert_eq!(s("b2*2", 1, 0), "B3*2");
1212 assert_eq!(s("Z1+AA1", 0, 1), "AA1+AB1");
1213 }
1214
1215 #[test]
1216 fn whole_columns_rows_and_sheets() {
1217 assert_eq!(s("SUM(A:A)", 0, 1), "SUM(B:B)");
1218 assert_eq!(s("SUM($A:A)", 0, 1), "SUM($A:B)");
1219 assert_eq!(s("SUM(1:3)", 1, 0), "SUM(2:4)");
1220 assert_eq!(s("Data!A1+'Q1 Data'!B2", 1, 0), "Data!A2+'Q1 Data'!B3");
1221 assert_eq!(s("'It''s'!A1", 1, 0), "'It''s'!A2");
1222 assert_eq!(
1223 s("SUM(Jan:Dec!A1)+[1]Sheet1!A1+A1#", 1, 0),
1224 "SUM(Jan:Dec!A2)+[1]Sheet1!A2+A2#"
1225 );
1226 }
1227
1228 #[test]
1229 fn what_is_not_a_reference_is_left_alone() {
1230 assert_eq!(s("\"A1\"&A1", 1, 0), "\"A1\"&A2");
1231 assert_eq!(s("\"say \"\"A1\"\"\"&A1", 1, 0), "\"say \"\"A1\"\"\"&A2");
1232 assert_eq!(s("LOG10(A1)", 1, 0), "LOG10(A2)");
1233 assert_eq!(s("Table1[Col]+A1", 1, 0), "Table1[Col]+A2");
1234 assert_eq!(
1235 s("Table1[[#This Row],[A1]]", 1, 0),
1236 "Table1[[#This Row],[A1]]"
1237 );
1238 assert_eq!(s("TRUE+A1", 1, 0), "TRUE+A2");
1239 assert_eq!(s("ABC123X+A1", 1, 0), "ABC123X+A2");
1240 assert_eq!(s("Rate*A1", 1, 0), "Rate*A2");
1241 assert_eq!(s("_xlfn.IFS(A1,1)", 1, 0), "_xlfn.IFS(A2,1)");
1242 assert_eq!(s("1.5+A1", 1, 0), "1.5+A2");
1243 assert_eq!(s("A1", 0, 0), "A1");
1244 assert_eq!(s("INDIRECT(\"A1\")+A1", 1, 0), "INDIRECT(\"A1\")+A2");
1245 }
1246
1247 #[test]
1248 fn off_the_sheet_is_ref_error() {
1249 assert_eq!(s("A1", -1, 0), "#REF!");
1250 assert_eq!(s("A1+B1", 0, -1), "#REF!+A1");
1251 assert_eq!(s("XFD1", 0, 1), "#REF!");
1252 assert_eq!(s("A1:B2", -1, 0), "#REF!");
1253 assert_eq!(s("$A$1", -5, -5), "$A$1");
1254 assert_eq!(s("SUM(A:A)", 0, -1), "SUM(#REF!)");
1255 }
1256}