1use std::collections::{BTreeMap, HashMap};
22
23use crate::rows::Visibility;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Rgb(pub u8, pub u8, pub u8);
28
29impl Rgb {
30 pub fn argb_hex(self) -> String {
32 format!("FF{:02X}{:02X}{:02X}", self.0, self.1, self.2)
33 }
34}
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub enum NumFmt {
40 #[default]
41 General,
42 Text,
43}
44
45impl NumFmt {
46 pub fn id(self) -> u32 {
48 match self {
49 NumFmt::General => 0,
50 NumFmt::Text => 49,
51 }
52 }
53}
54
55#[derive(Clone, Debug, Default, PartialEq, Eq)]
58pub struct Style {
59 pub fill: Option<Rgb>,
60 pub num_fmt: NumFmt,
61 pub wrap: bool,
62}
63
64#[derive(Clone, Debug)]
67pub struct Styles {
68 xfs: Vec<Style>,
69}
70
71impl Default for Styles {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77impl Styles {
78 pub fn new() -> Styles {
80 Styles {
81 xfs: vec![Style::default()],
82 }
83 }
84
85 pub fn id(&mut self, style: Style) -> u32 {
87 if let Some(i) = self.xfs.iter().position(|s| *s == style) {
88 return i as u32;
89 }
90 self.xfs.push(style);
91 (self.xfs.len() - 1) as u32
92 }
93
94 pub fn put(&mut self, index: u32, style: Style) {
98 let i = index as usize;
99 if self.xfs.len() <= i {
100 self.xfs.resize(i + 1, Style::default());
101 }
102 self.xfs[i] = style;
103 }
104
105 pub fn get(&self, index: u32) -> Option<&Style> {
107 self.xfs.get(index as usize)
108 }
109
110 pub fn xfs(&self) -> &[Style] {
112 &self.xfs
113 }
114
115 pub fn fills(&self) -> Vec<Rgb> {
117 let mut fills: Vec<Rgb> = Vec::new();
118 for s in &self.xfs {
119 if let Some(f) = s.fill {
120 if !fills.contains(&f) {
121 fills.push(f);
122 }
123 }
124 }
125 fills
126 }
127}
128
129#[derive(Clone, Debug, PartialEq)]
136pub enum Content {
137 Text(String),
138 Number(f64),
139 Bool(bool),
140 Error(String),
145 Formula(String),
146 SharedMaster {
147 text: String,
148 range: String,
149 si: u32,
150 },
151 SharedChild {
152 si: u32,
153 },
154 DynamicFormula(String),
159}
160
161#[derive(Clone, Debug, PartialEq)]
162pub struct Cell {
163 pub content: Content,
164 pub style: u32,
165}
166
167#[derive(Clone, Debug, PartialEq)]
171pub struct Column {
172 pub index: u32,
173 pub width: Option<f64>,
174 pub hidden: bool,
175 pub style: Option<u32>,
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct A1Range {
181 pub top: u32,
182 pub left: u32,
183 pub bottom: u32,
184 pub right: u32,
185}
186
187impl A1Range {
188 pub fn cells(&self) -> u64 {
189 u64::from(self.bottom - self.top + 1) * u64::from(self.right - self.left + 1)
190 }
191
192 pub fn text(&self) -> String {
194 if self.top == self.bottom && self.left == self.right {
195 cell_ref(self.top, self.left)
196 } else {
197 format!(
198 "{}:{}",
199 cell_ref(self.top, self.left),
200 cell_ref(self.bottom, self.right)
201 )
202 }
203 }
204}
205
206pub const MAX_COLUMN: u32 = 16_384;
208pub const MAX_ROW: u32 = 1_048_576;
209
210fn parse_a1_cell(s: &str) -> Option<(u32, u32)> {
211 let s = s.replace('$', "");
212 let letters: String = s.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
213 let digits = &s[letters.len()..];
214 if letters.is_empty() || letters.len() > 3 || digits.is_empty() {
215 return None;
216 }
217 if !digits.chars().all(|c| c.is_ascii_digit()) {
218 return None;
219 }
220 let mut col: u32 = 0;
221 for c in letters.chars() {
222 col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
223 }
224 let row: u32 = digits.parse().ok()?;
225 if col == 0 || col > MAX_COLUMN || row == 0 || row > MAX_ROW {
226 return None;
227 }
228 Some((row, col))
229}
230
231pub fn parse_a1_range(s: &str) -> Option<A1Range> {
235 let s = s.trim();
236 let (a, b) = match s.split_once(':') {
237 Some((a, b)) => (a, b),
238 None => (s, s),
239 };
240 let (r1, c1) = parse_a1_cell(a)?;
241 let (r2, c2) = parse_a1_cell(b)?;
242 Some(A1Range {
243 top: r1.min(r2),
244 left: c1.min(c2),
245 bottom: r1.max(r2),
246 right: c1.max(c2),
247 })
248}
249
250#[derive(Clone, Debug, PartialEq)]
254pub struct RowSettings {
255 pub height: Option<f64>,
256 pub hidden: bool,
257}
258
259#[derive(Clone, Debug, PartialEq)]
261pub struct Sheet {
262 pub name: String,
263 pub cells: BTreeMap<(u32, u32), Cell>,
264 pub columns: Vec<Column>,
265 pub rows: BTreeMap<u32, RowSettings>,
268 pub looks: Vec<(String, u32)>,
273 pub visibility: Visibility,
277 pub gridlines: bool,
278 pub active_cell: (u32, u32),
280 pub shared_formulas: u32,
282 pub sentences: BTreeMap<(u32, u32), u32>,
287}
288
289impl Sheet {
290 pub fn new(name: &str) -> Sheet {
291 Sheet {
292 name: name.to_string(),
293 cells: BTreeMap::new(),
294 columns: Vec::new(),
295 rows: BTreeMap::new(),
296 looks: Vec::new(),
297 visibility: Visibility::Visible,
298 gridlines: true,
299 active_cell: (1, 1),
300 shared_formulas: 0,
301 sentences: BTreeMap::new(),
302 }
303 }
304
305 pub fn wrote(&mut self, range: A1Range, row_of_sentence: u32) {
308 if row_of_sentence == 0 {
309 return;
310 }
311 for row in range.top..=range.bottom {
312 for col in range.left..=range.right {
313 self.sentences.insert((row, col), row_of_sentence);
314 }
315 }
316 }
317
318 pub fn set(&mut self, row: u32, col: u32, cell: Cell) {
319 self.cells.insert((row, col), cell);
320 }
321
322 pub fn set_formula_dynamic(&mut self, range: A1Range, text: &str, style: u32) {
331 for row in range.top..=range.bottom {
332 for col in range.left..=range.right {
333 let moved = crate::refers::shift_a1_references(
334 text,
335 i64::from(row) - i64::from(range.top),
336 i64::from(col) - i64::from(range.left),
337 );
338 self.set(
339 row,
340 col,
341 Cell {
342 content: Content::DynamicFormula(moved),
343 style,
344 },
345 );
346 }
347 }
348 }
349
350 pub fn set_formula(&mut self, range: A1Range, text: &str, style: u32) {
351 let si = self.next_shared(range);
352 for row in range.top..=range.bottom {
353 for col in range.left..=range.right {
354 let content = filled(range, text, si, row, col);
355 self.set(row, col, Cell { content, style });
356 }
357 }
358 }
359
360 pub(crate) fn next_shared(&mut self, range: A1Range) -> Option<u32> {
363 (range.cells() > 1).then(|| {
364 self.shared_formulas += 1;
365 self.shared_formulas - 1
366 })
367 }
368
369 pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
372 #[cfg(test)]
373 work::extents(self.cells.len());
374 let mut it = self.cells.keys();
375 let &(r0, c0) = it.next()?;
376 let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
377 for &(r, c) in it {
378 top = top.min(r);
379 left = left.min(c);
380 bottom = bottom.max(r);
381 right = right.max(c);
382 }
383 Some(((top, left), (bottom, right)))
384 }
385}
386
387pub fn shared_masters(sheet: &Sheet) -> HashMap<u32, (&str, u32, u32)> {
392 #[cfg(test)]
393 work::walked(sheet.cells.len());
394 let mut masters = HashMap::new();
395 for ((row, col), cell) in &sheet.cells {
396 if let Content::SharedMaster { text, si, .. } = &cell.content {
397 masters.insert(*si, (text.as_str(), *row, *col));
398 }
399 }
400 masters
401}
402
403pub fn formula_text(
409 content: &Content,
410 row: u32,
411 col: u32,
412 masters: &HashMap<u32, (&str, u32, u32)>,
413) -> Option<String> {
414 let raw = match content {
415 Content::Formula(t) | Content::DynamicFormula(t) => t.clone(),
416 Content::SharedMaster { text, .. } => text.clone(),
417 Content::SharedChild { si } => {
418 let (text, m_row, m_col) = masters.get(si)?;
419 crate::refers::shift_a1_references(
420 text,
421 i64::from(row) - i64::from(*m_row),
422 i64::from(col) - i64::from(*m_col),
423 )
424 }
425 Content::Text(_) | Content::Number(_) | Content::Bool(_) | Content::Error(_) => {
426 return None
427 }
428 };
429 Some(strip_future_prefixes(&raw))
430}
431
432pub(crate) fn filled(range: A1Range, text: &str, si: Option<u32>, row: u32, col: u32) -> Content {
438 match si {
439 None => Content::Formula(text.to_string()),
440 Some(si) if row == range.top && col == range.left => Content::SharedMaster {
441 text: text.to_string(),
442 range: range.text(),
443 si,
444 },
445 Some(si) => Content::SharedChild { si },
446 }
447}
448
449#[cfg(test)]
463pub(crate) mod work {
464 use std::cell::Cell;
465
466 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
468 pub(crate) struct Work {
469 pub(crate) walked: u64,
471 pub(crate) moved: u64,
473 pub(crate) whole: u64,
475 pub(crate) split: u64,
477 pub(crate) extents: u64,
479 pub(crate) copied: u64,
481 pub(crate) swept: u64,
484 pub(crate) viewed: u64,
487 }
488
489 thread_local! {
490 static ON: Cell<bool> = const { Cell::new(false) };
491 static DONE: Cell<Work> = const {
492 Cell::new(Work {
493 walked: 0,
494 moved: 0,
495 whole: 0,
496 split: 0,
497 extents: 0,
498 copied: 0,
499 swept: 0,
500 viewed: 0,
501 })
502 };
503 }
504
505 pub(crate) fn begin() {
507 ON.with(|on| on.set(true));
508 DONE.with(|done| done.set(Work::default()));
509 }
510
511 pub(crate) fn end() -> Work {
513 ON.with(|on| on.set(false));
514 DONE.with(Cell::get)
515 }
516
517 fn add(f: impl FnOnce(&mut Work)) {
518 if ON.with(Cell::get) {
519 DONE.with(|done| {
520 let mut work = done.get();
521 f(&mut work);
522 done.set(work);
523 });
524 }
525 }
526
527 pub(crate) fn walked(cells: usize) {
529 add(|w| w.walked += cells as u64);
530 }
531
532 pub(crate) fn extents(cells: usize) {
534 add(|w| w.extents += cells as u64);
535 }
536
537 pub(crate) fn moved() {
539 add(|w| w.moved += 1);
540 }
541
542 pub(crate) fn released(whole: bool) {
544 add(|w| {
545 if whole {
546 w.whole += 1;
547 } else {
548 w.split += 1;
549 }
550 });
551 }
552
553 pub(crate) fn copied() {
555 add(|w| w.copied += 1);
556 }
557
558 pub(crate) fn swept(cells: usize) {
560 add(|w| w.swept += cells as u64);
561 }
562
563 pub(crate) fn viewed(cells: usize) {
565 add(|w| w.viewed += cells as u64);
566 }
567}
568
569#[derive(Clone, Debug)]
572pub struct Workbook {
573 pub sheets: Vec<Sheet>,
574 pub styles: Styles,
575 pub active_sheet: usize,
576 pub defined_names: Vec<(String, String)>,
579}
580
581impl Default for Workbook {
582 fn default() -> Self {
583 Self::new()
584 }
585}
586
587impl Workbook {
588 pub fn new() -> Workbook {
589 Workbook {
590 sheets: Vec::new(),
591 styles: Styles::new(),
592 active_sheet: 0,
593 defined_names: Vec::new(),
594 }
595 }
596
597 pub fn has_dynamic_formulas(&self) -> bool {
600 self.sheets.iter().any(|s| {
601 s.cells
602 .values()
603 .any(|c| matches!(c.content, Content::DynamicFormula(_)))
604 })
605 }
606
607 pub fn find_sheet(&self, name: &str) -> Option<usize> {
613 self.sheets
614 .iter()
615 .position(|s| s.name.eq_ignore_ascii_case(name))
616 }
617
618 pub fn ensure_sheet(&mut self, name: &str) -> usize {
621 if let Some(i) = self.find_sheet(name) {
622 return i;
623 }
624 self.sheets.push(Sheet::new(name));
625 self.sheets.len() - 1
626 }
627}
628
629pub fn column_letters(col: u32) -> String {
631 let mut n = col;
632 let mut letters = Vec::new();
633 while n > 0 {
634 let rem = (n - 1) % 26;
635 letters.push((b'A' + rem as u8) as char);
636 n = (n - 1) / 26;
637 }
638 letters.iter().rev().collect()
639}
640
641pub fn cell_ref(row: u32, col: u32) -> String {
643 format!("{}{}", column_letters(col), row)
644}
645
646pub fn column_width_attr(chars: f64) -> f64 {
652 const MAX_DIGIT_WIDTH: f64 = 7.0;
653 ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
654}
655
656pub fn number_text(v: f64) -> Option<String> {
661 v.is_finite().then(|| format!("{v}"))
662}
663
664pub fn is_name_start(c: char) -> bool {
666 c.is_ascii_alphabetic() || c == '_'
667}
668
669pub fn is_name_char(c: char) -> bool {
672 c.is_ascii_alphanumeric() || c == '_' || c == '.'
673}
674
675pub fn strip_future_prefixes(formula: &str) -> String {
683 let chars: Vec<char> = formula.chars().collect();
684 let mut out = String::with_capacity(formula.len());
685 let mut i = 0;
686 while i < chars.len() {
687 let c = chars[i];
688 if c == '"' {
689 out.push(c);
690 i += 1;
691 while i < chars.len() {
692 out.push(chars[i]);
693 if chars[i] == '"' {
694 if i + 1 < chars.len() && chars[i + 1] == '"' {
695 out.push('"');
696 i += 2;
697 continue;
698 }
699 i += 1;
700 break;
701 }
702 i += 1;
703 }
704 continue;
705 }
706 if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
707 let start = i;
708 while i < chars.len() && is_name_char(chars[i]) {
709 i += 1;
710 }
711 let name: String = chars[start..i].iter().collect();
712 let mut bare = name.as_str();
713 loop {
714 let before = bare.len();
715 for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
716 if bare.len() > prefix.len()
717 && bare[..prefix.len()].eq_ignore_ascii_case(prefix)
718 {
719 bare = &bare[prefix.len()..];
720 }
721 }
722 if bare.len() == before {
723 break;
724 }
725 }
726 out.push_str(bare);
727 continue;
728 }
729 out.push(c);
730 i += 1;
731 }
732 out
733}
734
735#[cfg(test)]
736mod tests {
737 use super::*;
738
739 #[test]
740 fn references_and_letters() {
741 assert_eq!(column_letters(1), "A");
742 assert_eq!(column_letters(26), "Z");
743 assert_eq!(column_letters(27), "AA");
744 assert_eq!(column_letters(702), "ZZ");
745 assert_eq!(column_letters(703), "AAA");
746 assert_eq!(cell_ref(1, 2), "B1");
747 assert_eq!(cell_ref(12, 3), "C12");
748 }
749
750 #[test]
751 fn a_sheet_is_found_as_the_fold_finds_it() {
752 use crate::intrinsics::fold;
756 let mut book = Workbook::new();
757 for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
758 book.sheets.push(Sheet::new(name));
759 }
760 let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
761 let asked = [
762 ("É", Some(0)),
763 ("é", Some(1)),
764 ("İstanbul", Some(2)),
765 ("istanbul", None),
766 ("straße", Some(3)),
767 ("STRASSE", None),
768 ("DATA", Some(4)),
769 ("SCREEN.LAST", Some(5)),
770 ("Screen", None),
771 ("", None),
772 ];
773 for (name, want) in asked {
774 assert_eq!(book.find_sheet(name), want, "{name}");
775 assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
776 }
777 }
778
779 #[test]
780 fn the_room_widths_as_excel_writes_them() {
781 assert_eq!(column_width_attr(72.0), 72.7109375);
782 assert_eq!(column_width_attr(60.0), 60.7109375);
783 assert_eq!(column_width_attr(8.43), 9.140625);
784 }
785
786 #[test]
787 fn styles_dedup_and_fills_keep_first_use_order() {
788 let mut s = Styles::new();
789 let a = s.id(Style {
790 fill: Some(Rgb(1, 2, 3)),
791 ..Style::default()
792 });
793 let b = s.id(Style {
794 fill: Some(Rgb(4, 5, 6)),
795 wrap: true,
796 ..Style::default()
797 });
798 let a2 = s.id(Style {
799 fill: Some(Rgb(1, 2, 3)),
800 ..Style::default()
801 });
802 assert_eq!((a, b, a2), (1, 2, 1));
803 assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
804 assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
805 let wrap = Style {
808 wrap: true,
809 ..Style::default()
810 };
811 s.put(5, wrap.clone());
812 assert_eq!(s.xfs().len(), 6);
813 assert_eq!(s.get(5), Some(&wrap));
814 assert_eq!(s.get(4), Some(&Style::default()));
815 assert_eq!(s.get(6), None);
816 }
817
818 #[test]
819 fn a1_references_as_a_program_writes_them() {
820 let r = parse_a1_range("b2").unwrap();
821 assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
822 assert_eq!(r.text(), "B2");
823 let r = parse_a1_range("c4:A1").unwrap();
824 assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
825 assert_eq!(r.text(), "A1:C4");
826 assert_eq!(r.cells(), 12);
827 assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
828 assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
829 for bad in [
830 "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
831 ] {
832 assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
833 }
834 }
835
836 #[test]
837 fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
838 let mut sh = Sheet::new("S");
839 sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
840 sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
841 sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
842 assert_eq!(
843 sh.cells[&(2, 3)].content,
844 Content::SharedMaster {
845 text: "B2+B3".to_string(),
846 range: "C2:C3".to_string(),
847 si: 0
848 }
849 );
850 assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
851 assert_eq!(
852 sh.cells[&(2, 4)].content,
853 Content::Formula("B2*2".to_string())
854 );
855 assert_eq!(
856 sh.cells[&(2, 5)].content,
857 Content::SharedMaster {
858 text: "1".to_string(),
859 range: "E2:E3".to_string(),
860 si: 1
861 }
862 );
863 assert_eq!(sh.shared_formulas, 2);
864 }
865
866 #[test]
867 fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
868 let mut sh = Sheet::new("S");
869 sh.set_formula_dynamic(
870 parse_a1_range("c2:d3").unwrap(),
871 "IFS(B2>3,$A$1,TRUE,B$1)",
872 0,
873 );
874 assert_eq!(
875 sh.cells[&(2, 3)].content,
876 Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
877 );
878 assert_eq!(
879 sh.cells[&(3, 3)].content,
880 Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
881 );
882 assert_eq!(
883 sh.cells[&(2, 4)].content,
884 Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
885 );
886 assert_eq!(
887 sh.cells[&(3, 4)].content,
888 Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
889 );
890 let mut wb = Workbook::new();
891 assert!(!wb.has_dynamic_formulas());
892 wb.sheets.push(sh);
893 assert!(wb.has_dynamic_formulas());
894 }
895
896 #[test]
897 fn sheets_are_found_without_case_and_made_once() {
898 let mut wb = Workbook::new();
899 assert_eq!(wb.find_sheet("Output"), None);
900 let i = wb.ensure_sheet("Output");
901 assert_eq!(wb.ensure_sheet("output"), i);
902 assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
903 assert_eq!(wb.sheets.len(), 1);
904 assert_eq!(wb.sheets[0].name, "Output");
905 }
906
907 #[test]
908 fn a_sheet_knows_its_extent() {
909 let mut sh = Sheet::new("S");
910 assert_eq!(sh.extent(), None);
911 sh.set(
912 3,
913 2,
914 Cell {
915 content: Content::Number(1.0),
916 style: 0,
917 },
918 );
919 sh.set(
920 1,
921 3,
922 Cell {
923 content: Content::Bool(true),
924 style: 0,
925 },
926 );
927 assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
928 }
929
930 #[test]
931 fn numbers_as_the_file_holds_them() {
932 assert_eq!(number_text(5.0).as_deref(), Some("5"));
933 assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
934 assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
935 assert_eq!(number_text(f64::NAN), None);
936 assert_eq!(number_text(f64::INFINITY), None);
937 }
938
939 #[test]
940 fn the_prefixes_come_off_a_formula_read_back() {
941 assert_eq!(
942 strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
943 "IFS(B2>3,\"big\",TRUE,\"small\")"
944 );
945 assert_eq!(
946 strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
947 "FILTER(A:A,B:B>1)"
948 );
949 assert_eq!(
950 strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
951 "LET(x,1,x*2)"
952 );
953 assert_eq!(
954 strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
955 "SUM(A1:A3)&\"_xlfn.\""
956 );
957 assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
958 assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
959 }
960}