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)]
461pub(crate) mod work {
462 use std::cell::Cell;
463
464 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
466 pub(crate) struct Work {
467 pub(crate) walked: u64,
469 pub(crate) moved: u64,
471 pub(crate) whole: u64,
473 pub(crate) split: u64,
475 pub(crate) extents: u64,
477 pub(crate) copied: u64,
479 pub(crate) swept: u64,
482 }
483
484 thread_local! {
485 static ON: Cell<bool> = const { Cell::new(false) };
486 static DONE: Cell<Work> = const {
487 Cell::new(Work {
488 walked: 0,
489 moved: 0,
490 whole: 0,
491 split: 0,
492 extents: 0,
493 copied: 0,
494 swept: 0,
495 })
496 };
497 }
498
499 pub(crate) fn begin() {
501 ON.with(|on| on.set(true));
502 DONE.with(|done| done.set(Work::default()));
503 }
504
505 pub(crate) fn end() -> Work {
507 ON.with(|on| on.set(false));
508 DONE.with(Cell::get)
509 }
510
511 fn add(f: impl FnOnce(&mut Work)) {
512 if ON.with(Cell::get) {
513 DONE.with(|done| {
514 let mut work = done.get();
515 f(&mut work);
516 done.set(work);
517 });
518 }
519 }
520
521 pub(crate) fn walked(cells: usize) {
523 add(|w| w.walked += cells as u64);
524 }
525
526 pub(crate) fn extents(cells: usize) {
528 add(|w| w.extents += cells as u64);
529 }
530
531 pub(crate) fn moved() {
533 add(|w| w.moved += 1);
534 }
535
536 pub(crate) fn released(whole: bool) {
538 add(|w| {
539 if whole {
540 w.whole += 1;
541 } else {
542 w.split += 1;
543 }
544 });
545 }
546
547 pub(crate) fn copied() {
549 add(|w| w.copied += 1);
550 }
551
552 pub(crate) fn swept(cells: usize) {
554 add(|w| w.swept += cells as u64);
555 }
556}
557
558#[derive(Clone, Debug)]
561pub struct Workbook {
562 pub sheets: Vec<Sheet>,
563 pub styles: Styles,
564 pub active_sheet: usize,
565 pub defined_names: Vec<(String, String)>,
568}
569
570impl Default for Workbook {
571 fn default() -> Self {
572 Self::new()
573 }
574}
575
576impl Workbook {
577 pub fn new() -> Workbook {
578 Workbook {
579 sheets: Vec::new(),
580 styles: Styles::new(),
581 active_sheet: 0,
582 defined_names: Vec::new(),
583 }
584 }
585
586 pub fn has_dynamic_formulas(&self) -> bool {
589 self.sheets.iter().any(|s| {
590 s.cells
591 .values()
592 .any(|c| matches!(c.content, Content::DynamicFormula(_)))
593 })
594 }
595
596 pub fn find_sheet(&self, name: &str) -> Option<usize> {
602 self.sheets
603 .iter()
604 .position(|s| s.name.eq_ignore_ascii_case(name))
605 }
606
607 pub fn ensure_sheet(&mut self, name: &str) -> usize {
610 if let Some(i) = self.find_sheet(name) {
611 return i;
612 }
613 self.sheets.push(Sheet::new(name));
614 self.sheets.len() - 1
615 }
616}
617
618pub fn column_letters(col: u32) -> String {
620 let mut n = col;
621 let mut letters = Vec::new();
622 while n > 0 {
623 let rem = (n - 1) % 26;
624 letters.push((b'A' + rem as u8) as char);
625 n = (n - 1) / 26;
626 }
627 letters.iter().rev().collect()
628}
629
630pub fn cell_ref(row: u32, col: u32) -> String {
632 format!("{}{}", column_letters(col), row)
633}
634
635pub fn column_width_attr(chars: f64) -> f64 {
641 const MAX_DIGIT_WIDTH: f64 = 7.0;
642 ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
643}
644
645pub fn number_text(v: f64) -> Option<String> {
650 v.is_finite().then(|| format!("{v}"))
651}
652
653pub fn is_name_start(c: char) -> bool {
655 c.is_ascii_alphabetic() || c == '_'
656}
657
658pub fn is_name_char(c: char) -> bool {
661 c.is_ascii_alphanumeric() || c == '_' || c == '.'
662}
663
664pub fn strip_future_prefixes(formula: &str) -> String {
672 let chars: Vec<char> = formula.chars().collect();
673 let mut out = String::with_capacity(formula.len());
674 let mut i = 0;
675 while i < chars.len() {
676 let c = chars[i];
677 if c == '"' {
678 out.push(c);
679 i += 1;
680 while i < chars.len() {
681 out.push(chars[i]);
682 if chars[i] == '"' {
683 if i + 1 < chars.len() && chars[i + 1] == '"' {
684 out.push('"');
685 i += 2;
686 continue;
687 }
688 i += 1;
689 break;
690 }
691 i += 1;
692 }
693 continue;
694 }
695 if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
696 let start = i;
697 while i < chars.len() && is_name_char(chars[i]) {
698 i += 1;
699 }
700 let name: String = chars[start..i].iter().collect();
701 let mut bare = name.as_str();
702 loop {
703 let before = bare.len();
704 for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
705 if bare.len() > prefix.len()
706 && bare[..prefix.len()].eq_ignore_ascii_case(prefix)
707 {
708 bare = &bare[prefix.len()..];
709 }
710 }
711 if bare.len() == before {
712 break;
713 }
714 }
715 out.push_str(bare);
716 continue;
717 }
718 out.push(c);
719 i += 1;
720 }
721 out
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727
728 #[test]
729 fn references_and_letters() {
730 assert_eq!(column_letters(1), "A");
731 assert_eq!(column_letters(26), "Z");
732 assert_eq!(column_letters(27), "AA");
733 assert_eq!(column_letters(702), "ZZ");
734 assert_eq!(column_letters(703), "AAA");
735 assert_eq!(cell_ref(1, 2), "B1");
736 assert_eq!(cell_ref(12, 3), "C12");
737 }
738
739 #[test]
740 fn a_sheet_is_found_as_the_fold_finds_it() {
741 use crate::intrinsics::fold;
745 let mut book = Workbook::new();
746 for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
747 book.sheets.push(Sheet::new(name));
748 }
749 let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
750 let asked = [
751 ("É", Some(0)),
752 ("é", Some(1)),
753 ("İstanbul", Some(2)),
754 ("istanbul", None),
755 ("straße", Some(3)),
756 ("STRASSE", None),
757 ("DATA", Some(4)),
758 ("SCREEN.LAST", Some(5)),
759 ("Screen", None),
760 ("", None),
761 ];
762 for (name, want) in asked {
763 assert_eq!(book.find_sheet(name), want, "{name}");
764 assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
765 }
766 }
767
768 #[test]
769 fn the_room_widths_as_excel_writes_them() {
770 assert_eq!(column_width_attr(72.0), 72.7109375);
771 assert_eq!(column_width_attr(60.0), 60.7109375);
772 assert_eq!(column_width_attr(8.43), 9.140625);
773 }
774
775 #[test]
776 fn styles_dedup_and_fills_keep_first_use_order() {
777 let mut s = Styles::new();
778 let a = s.id(Style {
779 fill: Some(Rgb(1, 2, 3)),
780 ..Style::default()
781 });
782 let b = s.id(Style {
783 fill: Some(Rgb(4, 5, 6)),
784 wrap: true,
785 ..Style::default()
786 });
787 let a2 = s.id(Style {
788 fill: Some(Rgb(1, 2, 3)),
789 ..Style::default()
790 });
791 assert_eq!((a, b, a2), (1, 2, 1));
792 assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
793 assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
794 let wrap = Style {
797 wrap: true,
798 ..Style::default()
799 };
800 s.put(5, wrap.clone());
801 assert_eq!(s.xfs().len(), 6);
802 assert_eq!(s.get(5), Some(&wrap));
803 assert_eq!(s.get(4), Some(&Style::default()));
804 assert_eq!(s.get(6), None);
805 }
806
807 #[test]
808 fn a1_references_as_a_program_writes_them() {
809 let r = parse_a1_range("b2").unwrap();
810 assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
811 assert_eq!(r.text(), "B2");
812 let r = parse_a1_range("c4:A1").unwrap();
813 assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
814 assert_eq!(r.text(), "A1:C4");
815 assert_eq!(r.cells(), 12);
816 assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
817 assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
818 for bad in [
819 "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
820 ] {
821 assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
822 }
823 }
824
825 #[test]
826 fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
827 let mut sh = Sheet::new("S");
828 sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
829 sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
830 sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
831 assert_eq!(
832 sh.cells[&(2, 3)].content,
833 Content::SharedMaster {
834 text: "B2+B3".to_string(),
835 range: "C2:C3".to_string(),
836 si: 0
837 }
838 );
839 assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
840 assert_eq!(
841 sh.cells[&(2, 4)].content,
842 Content::Formula("B2*2".to_string())
843 );
844 assert_eq!(
845 sh.cells[&(2, 5)].content,
846 Content::SharedMaster {
847 text: "1".to_string(),
848 range: "E2:E3".to_string(),
849 si: 1
850 }
851 );
852 assert_eq!(sh.shared_formulas, 2);
853 }
854
855 #[test]
856 fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
857 let mut sh = Sheet::new("S");
858 sh.set_formula_dynamic(
859 parse_a1_range("c2:d3").unwrap(),
860 "IFS(B2>3,$A$1,TRUE,B$1)",
861 0,
862 );
863 assert_eq!(
864 sh.cells[&(2, 3)].content,
865 Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
866 );
867 assert_eq!(
868 sh.cells[&(3, 3)].content,
869 Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
870 );
871 assert_eq!(
872 sh.cells[&(2, 4)].content,
873 Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
874 );
875 assert_eq!(
876 sh.cells[&(3, 4)].content,
877 Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
878 );
879 let mut wb = Workbook::new();
880 assert!(!wb.has_dynamic_formulas());
881 wb.sheets.push(sh);
882 assert!(wb.has_dynamic_formulas());
883 }
884
885 #[test]
886 fn sheets_are_found_without_case_and_made_once() {
887 let mut wb = Workbook::new();
888 assert_eq!(wb.find_sheet("Output"), None);
889 let i = wb.ensure_sheet("Output");
890 assert_eq!(wb.ensure_sheet("output"), i);
891 assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
892 assert_eq!(wb.sheets.len(), 1);
893 assert_eq!(wb.sheets[0].name, "Output");
894 }
895
896 #[test]
897 fn a_sheet_knows_its_extent() {
898 let mut sh = Sheet::new("S");
899 assert_eq!(sh.extent(), None);
900 sh.set(
901 3,
902 2,
903 Cell {
904 content: Content::Number(1.0),
905 style: 0,
906 },
907 );
908 sh.set(
909 1,
910 3,
911 Cell {
912 content: Content::Bool(true),
913 style: 0,
914 },
915 );
916 assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
917 }
918
919 #[test]
920 fn numbers_as_the_file_holds_them() {
921 assert_eq!(number_text(5.0).as_deref(), Some("5"));
922 assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
923 assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
924 assert_eq!(number_text(f64::NAN), None);
925 assert_eq!(number_text(f64::INFINITY), None);
926 }
927
928 #[test]
929 fn the_prefixes_come_off_a_formula_read_back() {
930 assert_eq!(
931 strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
932 "IFS(B2>3,\"big\",TRUE,\"small\")"
933 );
934 assert_eq!(
935 strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
936 "FILTER(A:A,B:B>1)"
937 );
938 assert_eq!(
939 strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
940 "LET(x,1,x*2)"
941 );
942 assert_eq!(
943 strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
944 "SUM(A1:A3)&\"_xlfn.\""
945 );
946 assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
947 assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
948 }
949}