1pub mod inflate;
14pub mod merge;
15pub mod ooxml;
16pub mod xlfn;
17pub mod xml;
18pub mod zip;
19
20use std::collections::BTreeMap;
21
22use crate::intrinsics::fold;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct Rgb(pub u8, pub u8, pub u8);
27
28impl Rgb {
29 pub fn argb_hex(self) -> String {
31 format!("FF{:02X}{:02X}{:02X}", self.0, self.1, self.2)
32 }
33}
34
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum NumFmt {
39 #[default]
40 General,
41 Text,
42}
43
44impl NumFmt {
45 pub fn id(self) -> u32 {
47 match self {
48 NumFmt::General => 0,
49 NumFmt::Text => 49,
50 }
51 }
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Eq)]
57pub struct Style {
58 pub fill: Option<Rgb>,
59 pub num_fmt: NumFmt,
60 pub wrap: bool,
61}
62
63#[derive(Clone, Debug)]
66pub struct Styles {
67 xfs: Vec<Style>,
68}
69
70impl Default for Styles {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl Styles {
77 pub fn new() -> Styles {
79 Styles {
80 xfs: vec![Style::default()],
81 }
82 }
83
84 pub fn id(&mut self, style: Style) -> u32 {
86 if let Some(i) = self.xfs.iter().position(|s| *s == style) {
87 return i as u32;
88 }
89 self.xfs.push(style);
90 (self.xfs.len() - 1) as u32
91 }
92
93 pub fn xfs(&self) -> &[Style] {
95 &self.xfs
96 }
97
98 pub fn fills(&self) -> Vec<Rgb> {
100 let mut fills: Vec<Rgb> = Vec::new();
101 for s in &self.xfs {
102 if let Some(f) = s.fill {
103 if !fills.contains(&f) {
104 fills.push(f);
105 }
106 }
107 }
108 fills
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
119pub enum Content {
120 Text(String),
121 Number(f64),
122 Bool(bool),
123 Formula(String),
124 SharedMaster {
125 text: String,
126 range: String,
127 si: u32,
128 },
129 SharedChild {
130 si: u32,
131 },
132 DynamicFormula(String),
137}
138
139#[derive(Clone, Debug, PartialEq)]
140pub struct Cell {
141 pub content: Content,
142 pub style: u32,
143}
144
145#[derive(Clone, Debug, PartialEq)]
149pub struct Column {
150 pub index: u32,
151 pub width: Option<f64>,
152 pub hidden: bool,
153 pub style: Option<u32>,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct A1Range {
159 pub top: u32,
160 pub left: u32,
161 pub bottom: u32,
162 pub right: u32,
163}
164
165impl A1Range {
166 pub fn cells(&self) -> u64 {
167 u64::from(self.bottom - self.top + 1) * u64::from(self.right - self.left + 1)
168 }
169
170 pub fn text(&self) -> String {
172 if self.top == self.bottom && self.left == self.right {
173 cell_ref(self.top, self.left)
174 } else {
175 format!(
176 "{}:{}",
177 cell_ref(self.top, self.left),
178 cell_ref(self.bottom, self.right)
179 )
180 }
181 }
182}
183
184pub const MAX_COLUMN: u32 = 16_384;
186pub const MAX_ROW: u32 = 1_048_576;
187
188fn parse_a1_cell(s: &str) -> Option<(u32, u32)> {
189 let s = s.replace('$', "");
190 let letters: String = s.chars().take_while(|c| c.is_ascii_alphabetic()).collect();
191 let digits = &s[letters.len()..];
192 if letters.is_empty() || letters.len() > 3 || digits.is_empty() {
193 return None;
194 }
195 if !digits.chars().all(|c| c.is_ascii_digit()) {
196 return None;
197 }
198 let mut col: u32 = 0;
199 for c in letters.chars() {
200 col = col * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
201 }
202 let row: u32 = digits.parse().ok()?;
203 if col == 0 || col > MAX_COLUMN || row == 0 || row > MAX_ROW {
204 return None;
205 }
206 Some((row, col))
207}
208
209pub fn parse_a1_range(s: &str) -> Option<A1Range> {
213 let s = s.trim();
214 let (a, b) = match s.split_once(':') {
215 Some((a, b)) => (a, b),
216 None => (s, s),
217 };
218 let (r1, c1) = parse_a1_cell(a)?;
219 let (r2, c2) = parse_a1_cell(b)?;
220 Some(A1Range {
221 top: r1.min(r2),
222 left: c1.min(c2),
223 bottom: r1.max(r2),
224 right: c1.max(c2),
225 })
226}
227
228#[derive(Clone, Debug, PartialEq)]
230pub struct Sheet {
231 pub name: String,
232 pub cells: BTreeMap<(u32, u32), Cell>,
233 pub columns: Vec<Column>,
234 pub gridlines: bool,
235 pub active_cell: (u32, u32),
237 pub shared_formulas: u32,
239}
240
241impl Sheet {
242 pub fn new(name: &str) -> Sheet {
243 Sheet {
244 name: name.to_string(),
245 cells: BTreeMap::new(),
246 columns: Vec::new(),
247 gridlines: true,
248 active_cell: (1, 1),
249 shared_formulas: 0,
250 }
251 }
252
253 pub fn set(&mut self, row: u32, col: u32, cell: Cell) {
254 self.cells.insert((row, col), cell);
255 }
256
257 pub fn set_formula_dynamic(&mut self, range: A1Range, text: &str, style: u32) {
266 for row in range.top..=range.bottom {
267 for col in range.left..=range.right {
268 let moved = crate::refers::shift_a1_references(
269 text,
270 i64::from(row) - i64::from(range.top),
271 i64::from(col) - i64::from(range.left),
272 );
273 self.set(
274 row,
275 col,
276 Cell {
277 content: Content::DynamicFormula(moved),
278 style,
279 },
280 );
281 }
282 }
283 }
284
285 pub fn set_formula(&mut self, range: A1Range, text: &str, style: u32) {
286 if range.cells() == 1 {
287 self.set(
288 range.top,
289 range.left,
290 Cell {
291 content: Content::Formula(text.to_string()),
292 style,
293 },
294 );
295 return;
296 }
297 let si = self.shared_formulas;
298 self.shared_formulas += 1;
299 for row in range.top..=range.bottom {
300 for col in range.left..=range.right {
301 let content = if row == range.top && col == range.left {
302 Content::SharedMaster {
303 text: text.to_string(),
304 range: range.text(),
305 si,
306 }
307 } else {
308 Content::SharedChild { si }
309 };
310 self.set(row, col, Cell { content, style });
311 }
312 }
313 }
314
315 pub fn extent(&self) -> Option<((u32, u32), (u32, u32))> {
318 let mut it = self.cells.keys();
319 let &(r0, c0) = it.next()?;
320 let (mut top, mut left, mut bottom, mut right) = (r0, c0, r0, c0);
321 for &(r, c) in it {
322 top = top.min(r);
323 left = left.min(c);
324 bottom = bottom.max(r);
325 right = right.max(c);
326 }
327 Some(((top, left), (bottom, right)))
328 }
329}
330
331#[derive(Clone, Debug)]
334pub struct Workbook {
335 pub sheets: Vec<Sheet>,
336 pub styles: Styles,
337 pub active_sheet: usize,
338 pub defined_names: Vec<(String, String)>,
341}
342
343impl Default for Workbook {
344 fn default() -> Self {
345 Self::new()
346 }
347}
348
349impl Workbook {
350 pub fn new() -> Workbook {
351 Workbook {
352 sheets: Vec::new(),
353 styles: Styles::new(),
354 active_sheet: 0,
355 defined_names: Vec::new(),
356 }
357 }
358
359 pub fn has_dynamic_formulas(&self) -> bool {
362 self.sheets.iter().any(|s| {
363 s.cells
364 .values()
365 .any(|c| matches!(c.content, Content::DynamicFormula(_)))
366 })
367 }
368
369 pub fn find_sheet(&self, name: &str) -> Option<usize> {
372 let want = fold(name);
373 self.sheets.iter().position(|s| fold(&s.name) == want)
374 }
375
376 pub fn ensure_sheet(&mut self, name: &str) -> usize {
379 if let Some(i) = self.find_sheet(name) {
380 return i;
381 }
382 self.sheets.push(Sheet::new(name));
383 self.sheets.len() - 1
384 }
385}
386
387pub fn column_letters(col: u32) -> String {
389 let mut n = col;
390 let mut letters = Vec::new();
391 while n > 0 {
392 let rem = (n - 1) % 26;
393 letters.push((b'A' + rem as u8) as char);
394 n = (n - 1) / 26;
395 }
396 letters.iter().rev().collect()
397}
398
399pub fn cell_ref(row: u32, col: u32) -> String {
401 format!("{}{}", column_letters(col), row)
402}
403
404pub fn column_width_attr(chars: f64) -> f64 {
410 const MAX_DIGIT_WIDTH: f64 = 7.0;
411 ((chars * MAX_DIGIT_WIDTH + 5.0) / MAX_DIGIT_WIDTH * 256.0).trunc() / 256.0
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn references_and_letters() {
420 assert_eq!(column_letters(1), "A");
421 assert_eq!(column_letters(26), "Z");
422 assert_eq!(column_letters(27), "AA");
423 assert_eq!(column_letters(702), "ZZ");
424 assert_eq!(column_letters(703), "AAA");
425 assert_eq!(cell_ref(1, 2), "B1");
426 assert_eq!(cell_ref(12, 3), "C12");
427 }
428
429 #[test]
430 fn the_room_widths_as_excel_writes_them() {
431 assert_eq!(column_width_attr(72.0), 72.7109375);
432 assert_eq!(column_width_attr(60.0), 60.7109375);
433 assert_eq!(column_width_attr(8.43), 9.140625);
434 }
435
436 #[test]
437 fn styles_dedup_and_fills_keep_first_use_order() {
438 let mut s = Styles::new();
439 let a = s.id(Style {
440 fill: Some(Rgb(1, 2, 3)),
441 ..Style::default()
442 });
443 let b = s.id(Style {
444 fill: Some(Rgb(4, 5, 6)),
445 wrap: true,
446 ..Style::default()
447 });
448 let a2 = s.id(Style {
449 fill: Some(Rgb(1, 2, 3)),
450 ..Style::default()
451 });
452 assert_eq!((a, b, a2), (1, 2, 1));
453 assert_eq!(s.fills(), vec![Rgb(1, 2, 3), Rgb(4, 5, 6)]);
454 assert_eq!(Rgb(247, 244, 252).argb_hex(), "FFF7F4FC");
455 }
456
457 #[test]
458 fn a1_references_as_a_program_writes_them() {
459 let r = parse_a1_range("b2").unwrap();
460 assert_eq!((r.top, r.left, r.bottom, r.right), (2, 2, 2, 2));
461 assert_eq!(r.text(), "B2");
462 let r = parse_a1_range("c4:A1").unwrap();
463 assert_eq!((r.top, r.left, r.bottom, r.right), (1, 1, 4, 3));
464 assert_eq!(r.text(), "A1:C4");
465 assert_eq!(r.cells(), 12);
466 assert_eq!(parse_a1_range("$B$2:$B$4").unwrap().text(), "B2:B4");
467 assert_eq!(parse_a1_range("XFD1048576").unwrap().text(), "XFD1048576");
468 for bad in [
469 "c:c", "1:3", "XFE1", "A0", "A1048577", "AAAA1", "b", "2", "", "b2:",
470 ] {
471 assert!(parse_a1_range(bad).is_none(), "{bad} should not parse");
472 }
473 }
474
475 #[test]
476 fn a_formula_over_a_range_is_shared_from_its_top_left_cell() {
477 let mut sh = Sheet::new("S");
478 sh.set_formula(parse_a1_range("c2:c3").unwrap(), "B2+B3", 0);
479 sh.set_formula(parse_a1_range("d2").unwrap(), "B2*2", 0);
480 sh.set_formula(parse_a1_range("e2:e3").unwrap(), "1", 0);
481 assert_eq!(
482 sh.cells[&(2, 3)].content,
483 Content::SharedMaster {
484 text: "B2+B3".to_string(),
485 range: "C2:C3".to_string(),
486 si: 0
487 }
488 );
489 assert_eq!(sh.cells[&(3, 3)].content, Content::SharedChild { si: 0 });
490 assert_eq!(
491 sh.cells[&(2, 4)].content,
492 Content::Formula("B2*2".to_string())
493 );
494 assert_eq!(
495 sh.cells[&(2, 5)].content,
496 Content::SharedMaster {
497 text: "1".to_string(),
498 range: "E2:E3".to_string(),
499 si: 1
500 }
501 );
502 assert_eq!(sh.shared_formulas, 2);
503 }
504
505 #[test]
506 fn a_dynamic_formula_over_a_range_is_one_per_cell_with_its_references_moved() {
507 let mut sh = Sheet::new("S");
508 sh.set_formula_dynamic(
509 parse_a1_range("c2:d3").unwrap(),
510 "IFS(B2>3,$A$1,TRUE,B$1)",
511 0,
512 );
513 assert_eq!(
514 sh.cells[&(2, 3)].content,
515 Content::DynamicFormula("IFS(B2>3,$A$1,TRUE,B$1)".to_string())
516 );
517 assert_eq!(
518 sh.cells[&(3, 3)].content,
519 Content::DynamicFormula("IFS(B3>3,$A$1,TRUE,B$1)".to_string())
520 );
521 assert_eq!(
522 sh.cells[&(2, 4)].content,
523 Content::DynamicFormula("IFS(C2>3,$A$1,TRUE,C$1)".to_string())
524 );
525 assert_eq!(
526 sh.cells[&(3, 4)].content,
527 Content::DynamicFormula("IFS(C3>3,$A$1,TRUE,C$1)".to_string())
528 );
529 let mut wb = Workbook::new();
530 assert!(!wb.has_dynamic_formulas());
531 wb.sheets.push(sh);
532 assert!(wb.has_dynamic_formulas());
533 }
534
535 #[test]
536 fn sheets_are_found_without_case_and_made_once() {
537 let mut wb = Workbook::new();
538 assert_eq!(wb.find_sheet("Output"), None);
539 let i = wb.ensure_sheet("Output");
540 assert_eq!(wb.ensure_sheet("output"), i);
541 assert_eq!(wb.find_sheet("OUTPUT"), Some(i));
542 assert_eq!(wb.sheets.len(), 1);
543 assert_eq!(wb.sheets[0].name, "Output");
544 }
545
546 #[test]
547 fn a_sheet_knows_its_extent() {
548 let mut sh = Sheet::new("S");
549 assert_eq!(sh.extent(), None);
550 sh.set(
551 3,
552 2,
553 Cell {
554 content: Content::Number(1.0),
555 style: 0,
556 },
557 );
558 sh.set(
559 1,
560 3,
561 Cell {
562 content: Content::Bool(true),
563 style: 0,
564 },
565 );
566 assert_eq!(sh.extent(), Some(((1, 2), (3, 3))));
567 }
568}