1use std::collections::{BTreeMap, HashMap};
22
23use crate::numfmt::Code;
24use crate::rows::Visibility;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub struct Rgb(pub u8, pub u8, pub u8);
29
30impl Rgb {
31 pub fn argb_hex(self) -> String {
33 format!("FF{:02X}{:02X}{:02X}", self.0, self.1, self.2)
34 }
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
42pub enum NumFmt {
43 #[default]
44 General,
45 Text,
46 Code(Code),
47}
48
49impl NumFmt {
50 pub fn builtin(&self) -> Option<u32> {
55 match self {
56 NumFmt::General => Some(0),
57 NumFmt::Text => Some(49),
58 NumFmt::Code(c) => c.builtin(),
59 }
60 }
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
66pub struct Style {
67 pub fill: Option<Rgb>,
68 pub num_fmt: NumFmt,
69 pub wrap: bool,
70}
71
72#[derive(Clone, Debug)]
75pub struct Styles {
76 xfs: Vec<Style>,
77}
78
79impl Default for Styles {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl Styles {
86 pub fn new() -> Styles {
88 Styles {
89 xfs: vec![Style::default()],
90 }
91 }
92
93 pub fn id(&mut self, style: Style) -> u32 {
95 if let Some(i) = self.xfs.iter().position(|s| *s == style) {
96 return i as u32;
97 }
98 self.xfs.push(style);
99 (self.xfs.len() - 1) as u32
100 }
101
102 pub fn put(&mut self, index: u32, style: Style) {
106 let i = index as usize;
107 if self.xfs.len() <= i {
108 self.xfs.resize(i + 1, Style::default());
109 }
110 self.xfs[i] = style;
111 }
112
113 pub fn get(&self, index: u32) -> Option<&Style> {
115 self.xfs.get(index as usize)
116 }
117
118 pub fn xfs(&self) -> &[Style] {
120 &self.xfs
121 }
122
123 pub fn fills(&self) -> Vec<Rgb> {
125 let mut fills: Vec<Rgb> = Vec::new();
126 for s in &self.xfs {
127 if let Some(f) = s.fill {
128 if !fills.contains(&f) {
129 fills.push(f);
130 }
131 }
132 }
133 fills
134 }
135
136 pub fn has_code(&self) -> bool {
139 self.xfs
140 .iter()
141 .any(|s| matches!(s.num_fmt, NumFmt::Code(_)))
142 }
143}
144
145#[derive(Clone, Debug, PartialEq)]
152pub enum Content {
153 Text(String),
154 Number(f64),
155 Bool(bool),
156 Error(String),
161 Formula(String),
162 SharedMaster {
163 text: String,
164 range: String,
165 si: u32,
166 },
167 SharedChild {
168 si: u32,
169 },
170 DynamicFormula(String),
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub struct Cell {
179 pub content: Content,
180 pub style: u32,
181}
182
183#[derive(Clone, Debug, PartialEq)]
187pub struct Column {
188 pub index: u32,
189 pub width: Option<f64>,
190 pub hidden: bool,
191 pub style: Option<u32>,
192}
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
196pub struct A1Range {
197 pub top: u32,
198 pub left: u32,
199 pub bottom: u32,
200 pub right: u32,
201}
202
203impl A1Range {
204 pub fn cells(&self) -> u64 {
205 u64::from(self.bottom - self.top + 1) * u64::from(self.right - self.left + 1)
206 }
207
208 pub fn text(&self) -> String {
210 if self.top == self.bottom && self.left == self.right {
211 cell_ref(self.top, self.left)
212 } else {
213 format!(
214 "{}:{}",
215 cell_ref(self.top, self.left),
216 cell_ref(self.bottom, self.right)
217 )
218 }
219 }
220}
221
222pub const MAX_COLUMN: u32 = 16_384;
224pub const MAX_ROW: u32 = 1_048_576;
225
226fn parse_a1_cell(s: &str) -> Option<(u32, u32)> {
227 let s = s.replace('$', "");
228 let letters: String = s.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
229 let digits = &s[letters.len()..];
230 if letters.is_empty() || letters.len() > 3 || digits.is_empty() {
231 return None;
232 }
233 if !digits.chars().all(|c| c.is_ascii_digit()) {
234 return None;
235 }
236 let mut col: u32 = 0;
237 for c in letters.chars() {
238 col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
239 }
240 let row: u32 = digits.parse().ok()?;
241 if col == 0 || col > MAX_COLUMN || row == 0 || row > MAX_ROW {
242 return None;
243 }
244 Some((row, col))
245}
246
247pub fn parse_a1_range(s: &str) -> Option<A1Range> {
251 let s = s.trim();
252 let (a, b) = match s.split_once(':') {
253 Some((a, b)) => (a, b),
254 None => (s, s),
255 };
256 let (r1, c1) = parse_a1_cell(a)?;
257 let (r2, c2) = parse_a1_cell(b)?;
258 Some(A1Range {
259 top: r1.min(r2),
260 left: c1.min(c2),
261 bottom: r1.max(r2),
262 right: c1.max(c2),
263 })
264}
265
266#[derive(Clone, Debug, PartialEq)]
270pub struct RowSettings {
271 pub height: Option<f64>,
272 pub hidden: bool,
273}
274
275#[derive(Clone, Debug, PartialEq)]
277pub struct Sheet {
278 pub name: String,
279 pub cells: BTreeMap<(u32, u32), Cell>,
280 pub columns: Vec<Column>,
281 pub rows: BTreeMap<u32, RowSettings>,
284 pub looks: Vec<(String, u32)>,
289 pub visibility: Visibility,
293 pub gridlines: bool,
294 pub active_cell: (u32, u32),
296 pub shared_formulas: u32,
298 pub sentences: BTreeMap<(u32, u32), u32>,
303}
304
305impl Sheet {
306 pub fn new(name: &str) -> Sheet {
307 Sheet {
308 name: name.to_string(),
309 cells: BTreeMap::new(),
310 columns: Vec::new(),
311 rows: BTreeMap::new(),
312 looks: Vec::new(),
313 visibility: Visibility::Visible,
314 gridlines: true,
315 active_cell: (1, 1),
316 shared_formulas: 0,
317 sentences: BTreeMap::new(),
318 }
319 }
320
321 pub fn wrote(&mut self, range: A1Range, row_of_sentence: u32) {
324 if row_of_sentence == 0 {
325 return;
326 }
327 for row in range.top..=range.bottom {
328 for col in range.left..=range.right {
329 self.sentences.insert((row, col), row_of_sentence);
330 }
331 }
332 }
333
334 pub fn set(&mut self, row: u32, col: u32, cell: Cell) {
335 self.cells.insert((row, col), cell);
336 }
337
338 pub fn set_formula_dynamic(&mut self, range: A1Range, text: &str, style: u32) {
347 for row in range.top..=range.bottom {
348 for col in range.left..=range.right {
349 let moved = crate::refers::shift_a1_references(
350 text,
351 i64::from(row) - i64::from(range.top),
352 i64::from(col) - i64::from(range.left),
353 );
354 self.set(
355 row,
356 col,
357 Cell {
358 content: Content::DynamicFormula(moved),
359 style,
360 },
361 );
362 }
363 }
364 }
365
366 pub fn set_formula(&mut self, range: A1Range, text: &str, style: u32) {
367 let si = self.next_shared(range);
368 for row in range.top..=range.bottom {
369 for col in range.left..=range.right {
370 let content = filled(range, text, si, row, col);
371 self.set(row, col, Cell { content, style });
372 }
373 }
374 }
375
376 pub(crate) fn next_shared(&mut self, range: A1Range) -> Option<u32> {
379 (range.cells() > 1).then(|| {
380 self.shared_formulas += 1;
381 self.shared_formulas - 1
382 })
383 }
384
385 pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
388 #[cfg(test)]
389 work::extents(self.cells.len());
390 let mut it = self.cells.keys();
391 let &(r0, c0) = it.next()?;
392 let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
393 for &(r, c) in it {
394 top = top.min(r);
395 left = left.min(c);
396 bottom = bottom.max(r);
397 right = right.max(c);
398 }
399 Some(((top, left), (bottom, right)))
400 }
401}
402
403pub fn shared_masters(sheet: &Sheet) -> HashMap<u32, (&str, u32, u32)> {
408 #[cfg(test)]
409 work::walked(sheet.cells.len());
410 let mut masters = HashMap::new();
411 for ((row, col), cell) in &sheet.cells {
412 if let Content::SharedMaster { text, si, .. } = &cell.content {
413 masters.insert(*si, (text.as_str(), *row, *col));
414 }
415 }
416 masters
417}
418
419pub fn formula_text(
425 content: &Content,
426 row: u32,
427 col: u32,
428 masters: &HashMap<u32, (&str, u32, u32)>,
429) -> Option<String> {
430 let raw = match content {
431 Content::Formula(t) | Content::DynamicFormula(t) => t.clone(),
432 Content::SharedMaster { text, .. } => text.clone(),
433 Content::SharedChild { si } => {
434 let (text, m_row, m_col) = masters.get(si)?;
435 crate::refers::shift_a1_references(
436 text,
437 i64::from(row) - i64::from(*m_row),
438 i64::from(col) - i64::from(*m_col),
439 )
440 }
441 Content::Text(_) | Content::Number(_) | Content::Bool(_) | Content::Error(_) => {
442 return None
443 }
444 };
445 Some(strip_future_prefixes(&raw))
446}
447
448pub(crate) fn filled(range: A1Range, text: &str, si: Option<u32>, row: u32, col: u32) -> Content {
454 match si {
455 None => Content::Formula(text.to_string()),
456 Some(si) if row == range.top && col == range.left => Content::SharedMaster {
457 text: text.to_string(),
458 range: range.text(),
459 si,
460 },
461 Some(si) => Content::SharedChild { si },
462 }
463}
464
465#[cfg(test)]
479pub(crate) mod work {
480 use std::cell::Cell;
481
482 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
484 pub(crate) struct Work {
485 pub(crate) walked: u64,
487 pub(crate) moved: u64,
489 pub(crate) whole: u64,
491 pub(crate) split: u64,
493 pub(crate) extents: u64,
495 pub(crate) copied: u64,
497 pub(crate) swept: u64,
500 pub(crate) viewed: u64,
503 }
504
505 thread_local! {
506 static ON: Cell<bool> = const { Cell::new(false) };
507 static DONE: Cell<Work> = const {
508 Cell::new(Work {
509 walked: 0,
510 moved: 0,
511 whole: 0,
512 split: 0,
513 extents: 0,
514 copied: 0,
515 swept: 0,
516 viewed: 0,
517 })
518 };
519 }
520
521 pub(crate) fn begin() {
523 ON.with(|on| on.set(true));
524 DONE.with(|done| done.set(Work::default()));
525 }
526
527 pub(crate) fn end() -> Work {
529 ON.with(|on| on.set(false));
530 DONE.with(Cell::get)
531 }
532
533 fn add(f: impl FnOnce(&mut Work)) {
534 if ON.with(Cell::get) {
535 DONE.with(|done| {
536 let mut work = done.get();
537 f(&mut work);
538 done.set(work);
539 });
540 }
541 }
542
543 pub(crate) fn walked(cells: usize) {
545 add(|w| w.walked += cells as u64);
546 }
547
548 pub(crate) fn extents(cells: usize) {
550 add(|w| w.extents += cells as u64);
551 }
552
553 pub(crate) fn moved() {
555 add(|w| w.moved += 1);
556 }
557
558 pub(crate) fn released(whole: bool) {
560 add(|w| {
561 if whole {
562 w.whole += 1;
563 } else {
564 w.split += 1;
565 }
566 });
567 }
568
569 pub(crate) fn copied() {
571 add(|w| w.copied += 1);
572 }
573
574 pub(crate) fn swept(cells: usize) {
576 add(|w| w.swept += cells as u64);
577 }
578
579 pub(crate) fn viewed(cells: usize) {
581 add(|w| w.viewed += cells as u64);
582 }
583}
584
585#[derive(Clone, Debug)]
588pub struct Workbook {
589 pub sheets: Vec<Sheet>,
590 pub styles: Styles,
591 pub active_sheet: usize,
592 pub defined_names: Vec<(String, String)>,
595}
596
597impl Default for Workbook {
598 fn default() -> Self {
599 Self::new()
600 }
601}
602
603impl Workbook {
604 pub fn new() -> Workbook {
605 Workbook {
606 sheets: Vec::new(),
607 styles: Styles::new(),
608 active_sheet: 0,
609 defined_names: Vec::new(),
610 }
611 }
612
613 pub fn has_dynamic_formulas(&self) -> bool {
616 self.sheets.iter().any(|s| {
617 s.cells
618 .values()
619 .any(|c| matches!(c.content, Content::DynamicFormula(_)))
620 })
621 }
622
623 pub fn find_sheet(&self, name: &str) -> Option<usize> {
629 self.sheets
630 .iter()
631 .position(|s| s.name.eq_ignore_ascii_case(name))
632 }
633
634 pub fn ensure_sheet(&mut self, name: &str) -> usize {
637 if let Some(i) = self.find_sheet(name) {
638 return i;
639 }
640 self.sheets.push(Sheet::new(name));
641 self.sheets.len() - 1
642 }
643}
644
645pub fn column_letters(col: u32) -> String {
647 let mut n = col;
648 let mut letters = Vec::new();
649 while n > 0 {
650 let rem = (n - 1) % 26;
651 letters.push((b'A' + rem as u8) as char);
652 n = (n - 1) / 26;
653 }
654 letters.iter().rev().collect()
655}
656
657pub fn cell_ref(row: u32, col: u32) -> String {
659 format!("{}{}", column_letters(col), row)
660}
661
662pub fn column_width_attr(chars: f64) -> f64 {
668 const MAX_DIGIT_WIDTH: f64 = 7.0;
669 ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
670}
671
672pub fn number_text(v: f64) -> Option<String> {
677 v.is_finite().then(|| format!("{v}"))
678}
679
680pub fn is_name_start(c: char) -> bool {
682 c.is_ascii_alphabetic() || c == '_'
683}
684
685pub fn is_name_char(c: char) -> bool {
688 c.is_ascii_alphanumeric() || c == '_' || c == '.'
689}
690
691pub fn strip_future_prefixes(formula: &str) -> String {
699 let chars: Vec<char> = formula.chars().collect();
700 let mut out = String::with_capacity(formula.len());
701 let mut i = 0;
702 while i < chars.len() {
703 let c = chars[i];
704 if c == '"' {
705 out.push(c);
706 i += 1;
707 while i < chars.len() {
708 out.push(chars[i]);
709 if chars[i] == '"' {
710 if i + 1 < chars.len() && chars[i + 1] == '"' {
711 out.push('"');
712 i += 2;
713 continue;
714 }
715 i += 1;
716 break;
717 }
718 i += 1;
719 }
720 continue;
721 }
722 if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
723 let start = i;
724 while i < chars.len() && is_name_char(chars[i]) {
725 i += 1;
726 }
727 let name: String = chars[start..i].iter().collect();
728 let mut bare = name.as_str();
729 loop {
730 let before = bare.len();
731 for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
732 if bare.len() > prefix.len()
733 && bare[..prefix.len()].eq_ignore_ascii_case(prefix)
734 {
735 bare = &bare[prefix.len()..];
736 }
737 }
738 if bare.len() == before {
739 break;
740 }
741 }
742 out.push_str(bare);
743 continue;
744 }
745 out.push(c);
746 i += 1;
747 }
748 out
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 #[test]
756 fn references_and_letters() {
757 assert_eq!(column_letters(1), "A");
758 assert_eq!(column_letters(26), "Z");
759 assert_eq!(column_letters(27), "AA");
760 assert_eq!(column_letters(702), "ZZ");
761 assert_eq!(column_letters(703), "AAA");
762 assert_eq!(cell_ref(1, 2), "B1");
763 assert_eq!(cell_ref(12, 3), "C12");
764 }
765
766 #[test]
767 fn a_sheet_is_found_as_the_fold_finds_it() {
768 use crate::intrinsics::fold;
772 let mut book = Workbook::new();
773 for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
774 book.sheets.push(Sheet::new(name));
775 }
776 let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
777 let asked = [
778 ("É", Some(0)),
779 ("é", Some(1)),
780 ("İstanbul", Some(2)),
781 ("istanbul", None),
782 ("straße", Some(3)),
783 ("STRASSE", None),
784 ("DATA", Some(4)),
785 ("SCREEN.LAST", Some(5)),
786 ("Screen", None),
787 ("", None),
788 ];
789 for (name, want) in asked {
790 assert_eq!(book.find_sheet(name), want, "{name}");
791 assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
792 }
793 }
794
795 #[test]
796 fn the_room_widths_as_excel_writes_them() {
797 assert_eq!(column_width_attr(72.0), 72.7109375);
798 assert_eq!(column_width_attr(60.0), 60.7109375);
799 assert_eq!(column_width_attr(8.43), 9.140625);
800 }
801
802 #[test]
803 fn styles_dedup_and_fills_keep_first_use_order() {
804 let mut s = Styles::new();
805 let a = s.id(Style {
806 fill: Some(Rgb(1, 2, 3)),
807 ..Style::default()
808 });
809 let b = s.id(Style {
810 fill: Some(Rgb(4, 5, 6)),
811 wrap: true,
812 ..Style::default()
813 });
814 let a2 = s.id(Style {
815 fill: Some(Rgb(1, 2, 3)),
816 ..Style::default()
817 });
818 assert_eq!((a, b, a2), (1, 2, 1));
819 assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
820 assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
821 let wrap = Style {
824 wrap: true,
825 ..Style::default()
826 };
827 s.put(5, wrap.clone());
828 assert_eq!(s.xfs().len(), 6);
829 assert_eq!(s.get(5), Some(&wrap));
830 assert_eq!(s.get(4), Some(&Style::default()));
831 assert_eq!(s.get(6), None);
832 }
833
834 #[test]
835 fn a1_references_as_a_program_writes_them() {
836 let r = parse_a1_range("b2").unwrap();
837 assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
838 assert_eq!(r.text(), "B2");
839 let r = parse_a1_range("c4:A1").unwrap();
840 assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
841 assert_eq!(r.text(), "A1:C4");
842 assert_eq!(r.cells(), 12);
843 assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
844 assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
845 for bad in [
846 "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
847 ] {
848 assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
849 }
850 }
851
852 #[test]
853 fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
854 let mut sh = Sheet::new("S");
855 sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
856 sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
857 sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
858 assert_eq!(
859 sh.cells[&(2, 3)].content,
860 Content::SharedMaster {
861 text: "B2+B3".to_string(),
862 range: "C2:C3".to_string(),
863 si: 0
864 }
865 );
866 assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
867 assert_eq!(
868 sh.cells[&(2, 4)].content,
869 Content::Formula("B2*2".to_string())
870 );
871 assert_eq!(
872 sh.cells[&(2, 5)].content,
873 Content::SharedMaster {
874 text: "1".to_string(),
875 range: "E2:E3".to_string(),
876 si: 1
877 }
878 );
879 assert_eq!(sh.shared_formulas, 2);
880 }
881
882 #[test]
883 fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
884 let mut sh = Sheet::new("S");
885 sh.set_formula_dynamic(
886 parse_a1_range("c2:d3").unwrap(),
887 "IFS(B2>3,$A$1,TRUE,B$1)",
888 0,
889 );
890 assert_eq!(
891 sh.cells[&(2, 3)].content,
892 Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
893 );
894 assert_eq!(
895 sh.cells[&(3, 3)].content,
896 Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
897 );
898 assert_eq!(
899 sh.cells[&(2, 4)].content,
900 Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
901 );
902 assert_eq!(
903 sh.cells[&(3, 4)].content,
904 Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
905 );
906 let mut wb = Workbook::new();
907 assert!(!wb.has_dynamic_formulas());
908 wb.sheets.push(sh);
909 assert!(wb.has_dynamic_formulas());
910 }
911
912 #[test]
913 fn sheets_are_found_without_case_and_made_once() {
914 let mut wb = Workbook::new();
915 assert_eq!(wb.find_sheet("Output"), None);
916 let i = wb.ensure_sheet("Output");
917 assert_eq!(wb.ensure_sheet("output"), i);
918 assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
919 assert_eq!(wb.sheets.len(), 1);
920 assert_eq!(wb.sheets[0].name, "Output");
921 }
922
923 #[test]
924 fn a_sheet_knows_its_extent() {
925 let mut sh = Sheet::new("S");
926 assert_eq!(sh.extent(), None);
927 sh.set(
928 3,
929 2,
930 Cell {
931 content: Content::Number(1.0),
932 style: 0,
933 },
934 );
935 sh.set(
936 1,
937 3,
938 Cell {
939 content: Content::Bool(true),
940 style: 0,
941 },
942 );
943 assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
944 }
945
946 #[test]
947 fn numbers_as_the_file_holds_them() {
948 assert_eq!(number_text(5.0).as_deref(), Some("5"));
949 assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
950 assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
951 assert_eq!(number_text(f64::NAN), None);
952 assert_eq!(number_text(f64::INFINITY), None);
953 }
954
955 #[test]
956 fn the_prefixes_come_off_a_formula_read_back() {
957 assert_eq!(
958 strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
959 "IFS(B2>3,\"big\",TRUE,\"small\")"
960 );
961 assert_eq!(
962 strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
963 "FILTER(A:A,B:B>1)"
964 );
965 assert_eq!(
966 strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
967 "LET(x,1,x*2)"
968 );
969 assert_eq!(
970 strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
971 "SUM(A1:A3)&\"_xlfn.\""
972 );
973 assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
974 assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
975 }
976}