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 if range.cells() == 1 {
352 self.set(
353 range.top,
354 range.left,
355 Cell {
356 content: Content::Formula(text.to_string()),
357 style,
358 },
359 );
360 return;
361 }
362 let si = self.shared_formulas;
363 self.shared_formulas += 1;
364 for row in range.top..=range.bottom {
365 for col in range.left..=range.right {
366 let content = if row == range.top && col == range.left {
367 Content::SharedMaster {
368 text: text.to_string(),
369 range: range.text(),
370 si,
371 }
372 } else {
373 Content::SharedChild { si }
374 };
375 self.set(row, col, Cell { content, style });
376 }
377 }
378 }
379
380 pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
383 let mut it = self.cells.keys();
384 let &(r0, c0) = it.next()?;
385 let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
386 for &(r, c) in it {
387 top = top.min(r);
388 left = left.min(c);
389 bottom = bottom.max(r);
390 right = right.max(c);
391 }
392 Some(((top, left), (bottom, right)))
393 }
394}
395
396pub fn shared_masters(sheet: &Sheet) -> HashMap<u32, (&str, u32, u32)> {
401 let mut masters = HashMap::new();
402 for ((row, col), cell) in &sheet.cells {
403 if let Content::SharedMaster { text, si, .. } = &cell.content {
404 masters.insert(*si, (text.as_str(), *row, *col));
405 }
406 }
407 masters
408}
409
410pub fn formula_text(
416 content: &Content,
417 row: u32,
418 col: u32,
419 masters: &HashMap<u32, (&str, u32, u32)>,
420) -> Option<String> {
421 let raw = match content {
422 Content::Formula(t) | Content::DynamicFormula(t) => t.clone(),
423 Content::SharedMaster { text, .. } => text.clone(),
424 Content::SharedChild { si } => {
425 let (text, m_row, m_col) = masters.get(si)?;
426 crate::refers::shift_a1_references(
427 text,
428 i64::from(row) - i64::from(*m_row),
429 i64::from(col) - i64::from(*m_col),
430 )
431 }
432 Content::Text(_) | Content::Number(_) | Content::Bool(_) | Content::Error(_) => {
433 return None
434 }
435 };
436 Some(strip_future_prefixes(&raw))
437}
438
439#[derive(Clone, Debug)]
442pub struct Workbook {
443 pub sheets: Vec<Sheet>,
444 pub styles: Styles,
445 pub active_sheet: usize,
446 pub defined_names: Vec<(String, String)>,
449}
450
451impl Default for Workbook {
452 fn default() -> Self {
453 Self::new()
454 }
455}
456
457impl Workbook {
458 pub fn new() -> Workbook {
459 Workbook {
460 sheets: Vec::new(),
461 styles: Styles::new(),
462 active_sheet: 0,
463 defined_names: Vec::new(),
464 }
465 }
466
467 pub fn has_dynamic_formulas(&self) -> bool {
470 self.sheets.iter().any(|s| {
471 s.cells
472 .values()
473 .any(|c| matches!(c.content, Content::DynamicFormula(_)))
474 })
475 }
476
477 pub fn find_sheet(&self, name: &str) -> Option<usize> {
483 self.sheets
484 .iter()
485 .position(|s| s.name.eq_ignore_ascii_case(name))
486 }
487
488 pub fn ensure_sheet(&mut self, name: &str) -> usize {
491 if let Some(i) = self.find_sheet(name) {
492 return i;
493 }
494 self.sheets.push(Sheet::new(name));
495 self.sheets.len() - 1
496 }
497}
498
499pub fn column_letters(col: u32) -> String {
501 let mut n = col;
502 let mut letters = Vec::new();
503 while n > 0 {
504 let rem = (n - 1) % 26;
505 letters.push((b'A' + rem as u8) as char);
506 n = (n - 1) / 26;
507 }
508 letters.iter().rev().collect()
509}
510
511pub fn cell_ref(row: u32, col: u32) -> String {
513 format!("{}{}", column_letters(col), row)
514}
515
516pub fn column_width_attr(chars: f64) -> f64 {
522 const MAX_DIGIT_WIDTH: f64 = 7.0;
523 ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
524}
525
526pub fn number_text(v: f64) -> Option<String> {
531 v.is_finite().then(|| format!("{v}"))
532}
533
534pub fn is_name_start(c: char) -> bool {
536 c.is_ascii_alphabetic() || c == '_'
537}
538
539pub fn is_name_char(c: char) -> bool {
542 c.is_ascii_alphanumeric() || c == '_' || c == '.'
543}
544
545pub fn strip_future_prefixes(formula: &str) -> String {
553 let chars: Vec<char> = formula.chars().collect();
554 let mut out = String::with_capacity(formula.len());
555 let mut i = 0;
556 while i < chars.len() {
557 let c = chars[i];
558 if c == '"' {
559 out.push(c);
560 i += 1;
561 while i < chars.len() {
562 out.push(chars[i]);
563 if chars[i] == '"' {
564 if i + 1 < chars.len() && chars[i + 1] == '"' {
565 out.push('"');
566 i += 2;
567 continue;
568 }
569 i += 1;
570 break;
571 }
572 i += 1;
573 }
574 continue;
575 }
576 if is_name_start(c) && (i == 0 || !is_name_char(chars[i - 1])) {
577 let start = i;
578 while i < chars.len() && is_name_char(chars[i]) {
579 i += 1;
580 }
581 let name: String = chars[start..i].iter().collect();
582 let mut bare = name.as_str();
583 loop {
584 let before = bare.len();
585 for prefix in ["_xlfn.", "_xlws.", "_xlpm."] {
586 if bare.len() > prefix.len()
587 && bare[..prefix.len()].eq_ignore_ascii_case(prefix)
588 {
589 bare = &bare[prefix.len()..];
590 }
591 }
592 if bare.len() == before {
593 break;
594 }
595 }
596 out.push_str(bare);
597 continue;
598 }
599 out.push(c);
600 i += 1;
601 }
602 out
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn references_and_letters() {
611 assert_eq!(column_letters(1), "A");
612 assert_eq!(column_letters(26), "Z");
613 assert_eq!(column_letters(27), "AA");
614 assert_eq!(column_letters(702), "ZZ");
615 assert_eq!(column_letters(703), "AAA");
616 assert_eq!(cell_ref(1, 2), "B1");
617 assert_eq!(cell_ref(12, 3), "C12");
618 }
619
620 #[test]
621 fn a_sheet_is_found_as_the_fold_finds_it() {
622 use crate::intrinsics::fold;
626 let mut book = Workbook::new();
627 for name in ["É", "é", "İSTANBUL", "Straße", "Data", "Screen.last"] {
628 book.sheets.push(Sheet::new(name));
629 }
630 let by_fold = |name: &str| book.sheets.iter().position(|s| fold(&s.name) == fold(name));
631 let asked = [
632 ("É", Some(0)),
633 ("é", Some(1)),
634 ("İstanbul", Some(2)),
635 ("istanbul", None),
636 ("straße", Some(3)),
637 ("STRASSE", None),
638 ("DATA", Some(4)),
639 ("SCREEN.LAST", Some(5)),
640 ("Screen", None),
641 ("", None),
642 ];
643 for (name, want) in asked {
644 assert_eq!(book.find_sheet(name), want, "{name}");
645 assert_eq!(book.find_sheet(name), by_fold(name), "{name}");
646 }
647 }
648
649 #[test]
650 fn the_room_widths_as_excel_writes_them() {
651 assert_eq!(column_width_attr(72.0), 72.7109375);
652 assert_eq!(column_width_attr(60.0), 60.7109375);
653 assert_eq!(column_width_attr(8.43), 9.140625);
654 }
655
656 #[test]
657 fn styles_dedup_and_fills_keep_first_use_order() {
658 let mut s = Styles::new();
659 let a = s.id(Style {
660 fill: Some(Rgb(1, 2, 3)),
661 ..Style::default()
662 });
663 let b = s.id(Style {
664 fill: Some(Rgb(4, 5, 6)),
665 wrap: true,
666 ..Style::default()
667 });
668 let a2 = s.id(Style {
669 fill: Some(Rgb(1, 2, 3)),
670 ..Style::default()
671 });
672 assert_eq!((a, b, a2), (1, 2, 1));
673 assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
674 assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
675 let wrap = Style {
678 wrap: true,
679 ..Style::default()
680 };
681 s.put(5, wrap.clone());
682 assert_eq!(s.xfs().len(), 6);
683 assert_eq!(s.get(5), Some(&wrap));
684 assert_eq!(s.get(4), Some(&Style::default()));
685 assert_eq!(s.get(6), None);
686 }
687
688 #[test]
689 fn a1_references_as_a_program_writes_them() {
690 let r = parse_a1_range("b2").unwrap();
691 assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
692 assert_eq!(r.text(), "B2");
693 let r = parse_a1_range("c4:A1").unwrap();
694 assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
695 assert_eq!(r.text(), "A1:C4");
696 assert_eq!(r.cells(), 12);
697 assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
698 assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
699 for bad in [
700 "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
701 ] {
702 assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
703 }
704 }
705
706 #[test]
707 fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
708 let mut sh = Sheet::new("S");
709 sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
710 sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
711 sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
712 assert_eq!(
713 sh.cells[&(2, 3)].content,
714 Content::SharedMaster {
715 text: "B2+B3".to_string(),
716 range: "C2:C3".to_string(),
717 si: 0
718 }
719 );
720 assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
721 assert_eq!(
722 sh.cells[&(2, 4)].content,
723 Content::Formula("B2*2".to_string())
724 );
725 assert_eq!(
726 sh.cells[&(2, 5)].content,
727 Content::SharedMaster {
728 text: "1".to_string(),
729 range: "E2:E3".to_string(),
730 si: 1
731 }
732 );
733 assert_eq!(sh.shared_formulas, 2);
734 }
735
736 #[test]
737 fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
738 let mut sh = Sheet::new("S");
739 sh.set_formula_dynamic(
740 parse_a1_range("c2:d3").unwrap(),
741 "IFS(B2>3,$A$1,TRUE,B$1)",
742 0,
743 );
744 assert_eq!(
745 sh.cells[&(2, 3)].content,
746 Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
747 );
748 assert_eq!(
749 sh.cells[&(3, 3)].content,
750 Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
751 );
752 assert_eq!(
753 sh.cells[&(2, 4)].content,
754 Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
755 );
756 assert_eq!(
757 sh.cells[&(3, 4)].content,
758 Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
759 );
760 let mut wb = Workbook::new();
761 assert!(!wb.has_dynamic_formulas());
762 wb.sheets.push(sh);
763 assert!(wb.has_dynamic_formulas());
764 }
765
766 #[test]
767 fn sheets_are_found_without_case_and_made_once() {
768 let mut wb = Workbook::new();
769 assert_eq!(wb.find_sheet("Output"), None);
770 let i = wb.ensure_sheet("Output");
771 assert_eq!(wb.ensure_sheet("output"), i);
772 assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
773 assert_eq!(wb.sheets.len(), 1);
774 assert_eq!(wb.sheets[0].name, "Output");
775 }
776
777 #[test]
778 fn a_sheet_knows_its_extent() {
779 let mut sh = Sheet::new("S");
780 assert_eq!(sh.extent(), None);
781 sh.set(
782 3,
783 2,
784 Cell {
785 content: Content::Number(1.0),
786 style: 0,
787 },
788 );
789 sh.set(
790 1,
791 3,
792 Cell {
793 content: Content::Bool(true),
794 style: 0,
795 },
796 );
797 assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
798 }
799
800 #[test]
801 fn numbers_as_the_file_holds_them() {
802 assert_eq!(number_text(5.0).as_deref(), Some("5"));
803 assert_eq!(number_text(0.1).as_deref(), Some("0.1"));
804 assert_eq!(number_text(-2.5).as_deref(), Some("-2.5"));
805 assert_eq!(number_text(f64::NAN), None);
806 assert_eq!(number_text(f64::INFINITY), None);
807 }
808
809 #[test]
810 fn the_prefixes_come_off_a_formula_read_back() {
811 assert_eq!(
812 strip_future_prefixes("_xlfn.IFS(B2>3,\"big\",TRUE,\"small\")"),
813 "IFS(B2>3,\"big\",TRUE,\"small\")"
814 );
815 assert_eq!(
816 strip_future_prefixes("_xlfn._xlws.FILTER(A:A,B:B>1)"),
817 "FILTER(A:A,B:B>1)"
818 );
819 assert_eq!(
820 strip_future_prefixes("_xlfn.LET(_xlpm.x,1,_xlpm.x*2)"),
821 "LET(x,1,x*2)"
822 );
823 assert_eq!(
824 strip_future_prefixes("SUM(A1:A3)&\"_xlfn.\""),
825 "SUM(A1:A3)&\"_xlfn.\""
826 );
827 assert_eq!(strip_future_prefixes("Rate*B1"), "Rate*B1");
828 assert_eq!(strip_future_prefixes("_xlfn."), "_xlfn.");
829 }
830}