1use crate::error::{Error, Result};
6use crate::ext::{
7 Chart as ExtChart, ChartType as ExtChartType, Comment as ExtComment, ResolvedSheet,
8 parse_worksheet,
9};
10use crate::parsers::FromXml;
11use ooxml_opc::{Package, Relationships};
12use quick_xml::Reader;
13use quick_xml::events::Event;
14use std::fs::File;
15use std::io::{BufReader, Cursor, Read, Seek};
16use std::path::Path;
17
18const REL_OFFICE_DOCUMENT: &str =
20 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
21const REL_SHARED_STRINGS: &str =
22 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
23const REL_STYLES: &str =
24 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
25const REL_COMMENTS: &str =
26 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
27const REL_DRAWING: &str =
28 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
29const REL_CHART: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
30const REL_CHARTSHEET: &str =
31 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet";
32const REL_PIVOT_TABLE: &str =
33 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable";
34const REL_PIVOT_CACHE_DEFINITION: &str =
35 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition";
36
37pub struct Workbook<R: Read + Seek> {
41 package: Package<R>,
42 workbook_path: String,
44 workbook_rels: Relationships,
46 sheet_info: Vec<SheetInfo>,
48 shared_strings: Vec<String>,
50 styles: crate::types::Stylesheet,
52 defined_names: Vec<crate::types::DefinedName>,
54 #[cfg(feature = "sml-protection")]
56 workbook_protection: Option<crate::types::WorkbookProtection>,
57}
58
59#[derive(Debug, Clone)]
61struct SheetInfo {
62 name: String,
63 #[allow(dead_code)]
64 sheet_id: u32,
65 rel_id: String,
66}
67
68impl Workbook<BufReader<File>> {
69 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
71 let file = File::open(path)?;
72 Self::from_reader(BufReader::new(file))
73 }
74}
75
76impl<R: Read + Seek> Workbook<R> {
77 pub fn from_reader(reader: R) -> Result<Self> {
79 let mut package = Package::open(reader)?;
80
81 let root_rels = package.read_relationships()?;
83 let workbook_rel = root_rels
84 .get_by_type(REL_OFFICE_DOCUMENT)
85 .ok_or_else(|| Error::Invalid("Missing workbook relationship".into()))?;
86 let workbook_path = workbook_rel.target.clone();
87
88 let workbook_rels = package
90 .read_part_relationships(&workbook_path)
91 .unwrap_or_default();
92
93 let workbook_xml = package.read_part(&workbook_path)?;
95 let wb: crate::types::Workbook = bootstrap(&workbook_xml)?;
96 let sheet_info = wb
97 .sheets
98 .sheet
99 .iter()
100 .map(|s| SheetInfo {
101 name: s.name.to_string(),
102 sheet_id: s.sheet_id,
103 rel_id: s.id.to_string(),
104 })
105 .collect();
106 let defined_names = wb
107 .defined_names
108 .map(|dn| {
109 let inner = *dn;
110 inner.defined_name
111 })
112 .unwrap_or_default();
113 #[cfg(feature = "sml-protection")]
114 let workbook_protection = wb.workbook_protection.map(|b| *b);
115
116 let shared_strings = if let Some(rel) = workbook_rels.get_by_type(REL_SHARED_STRINGS) {
118 let path = resolve_path(&workbook_path, &rel.target);
119 if let Ok(data) = package.read_part(&path) {
120 let sst: crate::types::SharedStrings = bootstrap(&data)?;
121 extract_shared_strings(&sst)
122 } else {
123 Vec::new()
124 }
125 } else {
126 Vec::new()
127 };
128
129 let styles = if let Some(rel) = workbook_rels.get_by_type(REL_STYLES) {
131 let path = resolve_path(&workbook_path, &rel.target);
132 if let Ok(data) = package.read_part(&path) {
133 bootstrap(&data)?
134 } else {
135 crate::types::Stylesheet::default()
136 }
137 } else {
138 crate::types::Stylesheet::default()
139 };
140
141 Ok(Self {
142 package,
143 workbook_path,
144 workbook_rels,
145 sheet_info,
146 shared_strings,
147 styles,
148 defined_names,
149 #[cfg(feature = "sml-protection")]
150 workbook_protection,
151 })
152 }
153
154 pub fn sheet_count(&self) -> usize {
156 self.sheet_info.len()
157 }
158
159 pub fn sheet_names(&self) -> Vec<&str> {
161 self.sheet_info.iter().map(|s| s.name.as_str()).collect()
162 }
163
164 pub fn styles(&self) -> &crate::types::Stylesheet {
166 &self.styles
167 }
168
169 pub fn stylesheet(&self) -> Option<&crate::types::Stylesheet> {
171 Some(&self.styles)
172 }
173
174 #[cfg(feature = "sml-protection")]
180 pub fn workbook_protection(&self) -> Option<&crate::types::WorkbookProtection> {
181 self.workbook_protection.as_ref()
182 }
183
184 #[cfg(feature = "sml-protection")]
188 pub fn raw_workbook_protection(&self) -> Option<&crate::types::WorkbookProtection> {
189 self.workbook_protection.as_ref()
190 }
191
192 pub fn defined_names(&self) -> &[crate::types::DefinedName] {
194 &self.defined_names
195 }
196
197 pub fn defined_name(&self, name: &str) -> Option<&crate::types::DefinedName> {
201 self.defined_names
202 .iter()
203 .find(|d| d.name.eq_ignore_ascii_case(name) && d.local_sheet_id.is_none())
204 }
205
206 pub fn defined_name_in_sheet(
208 &self,
209 name: &str,
210 sheet_index: u32,
211 ) -> Option<&crate::types::DefinedName> {
212 self.defined_names
213 .iter()
214 .find(|d| d.name.eq_ignore_ascii_case(name) && d.local_sheet_id == Some(sheet_index))
215 }
216
217 pub fn global_defined_names(&self) -> impl Iterator<Item = &crate::types::DefinedName> {
219 self.defined_names
220 .iter()
221 .filter(|d| d.local_sheet_id.is_none())
222 }
223
224 pub fn sheet_defined_names(
226 &self,
227 sheet_index: u32,
228 ) -> impl Iterator<Item = &crate::types::DefinedName> {
229 self.defined_names
230 .iter()
231 .filter(move |d| d.local_sheet_id == Some(sheet_index))
232 }
233
234 pub fn pivot_caches(&mut self) -> Result<Vec<crate::types::PivotCacheDefinition>> {
242 let mut caches = Vec::new();
243 let rel_targets: Vec<String> = self
244 .workbook_rels
245 .get_all_by_type(REL_PIVOT_CACHE_DEFINITION)
246 .map(|r| r.target.clone())
247 .collect();
248 for target in rel_targets {
249 let path = resolve_path(&self.workbook_path, &target);
250 if let Ok(data) = self.package.read_part(&path)
251 && let Ok(cache) = bootstrap::<crate::types::PivotCacheDefinition>(&data)
252 {
253 caches.push(cache);
254 }
255 }
256 Ok(caches)
257 }
258
259 pub fn resolved_sheet(&mut self, index: usize) -> Result<ResolvedSheet> {
270 let info = self
271 .sheet_info
272 .get(index)
273 .ok_or_else(|| Error::Invalid(format!("Sheet index {} out of range", index)))?
274 .clone();
275
276 self.load_resolved_sheet(&info)
277 }
278
279 pub fn resolved_sheet_by_name(&mut self, name: &str) -> Result<ResolvedSheet> {
284 let info = self
285 .sheet_info
286 .iter()
287 .find(|s| s.name == name)
288 .ok_or_else(|| Error::Invalid(format!("Sheet '{}' not found", name)))?
289 .clone();
290
291 self.load_resolved_sheet(&info)
292 }
293
294 pub fn resolved_sheets(&mut self) -> Result<Vec<ResolvedSheet>> {
296 let infos: Vec<_> = self.sheet_info.clone();
297 infos
298 .iter()
299 .map(|info| self.load_resolved_sheet(info))
300 .collect()
301 }
302
303 pub fn sheet_xml(&mut self, index: usize) -> Result<Vec<u8>> {
325 let info = self
326 .sheet_info
327 .get(index)
328 .ok_or_else(|| Error::Invalid(format!("Sheet index {} out of range", index)))?
329 .clone();
330
331 let rel = self.workbook_rels.get(&info.rel_id).ok_or_else(|| {
332 Error::Invalid(format!("Missing relationship for sheet '{}'", info.name))
333 })?;
334
335 let path = resolve_path(&self.workbook_path, &rel.target);
336 Ok(self.package.read_part(&path)?)
337 }
338
339 pub fn sheet_xml_by_name(&mut self, name: &str) -> Result<Vec<u8>> {
343 let info = self
344 .sheet_info
345 .iter()
346 .find(|s| s.name == name)
347 .ok_or_else(|| Error::Invalid(format!("Sheet '{}' not found", name)))?
348 .clone();
349
350 let rel = self.workbook_rels.get(&info.rel_id).ok_or_else(|| {
351 Error::Invalid(format!("Missing relationship for sheet '{}'", info.name))
352 })?;
353
354 let path = resolve_path(&self.workbook_path, &rel.target);
355 Ok(self.package.read_part(&path)?)
356 }
357
358 fn load_resolved_sheet(&mut self, info: &SheetInfo) -> Result<ResolvedSheet> {
360 let rel = self.workbook_rels.get(&info.rel_id).ok_or_else(|| {
362 Error::Invalid(format!("Missing relationship for sheet '{}'", info.name))
363 })?;
364
365 let path = resolve_path(&self.workbook_path, &rel.target);
366 let data = self.package.read_part(&path)?;
367
368 let is_chartsheet = rel.relationship_type == REL_CHARTSHEET;
370
371 let worksheet = if is_chartsheet {
373 let minimal_xml = br#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>"#;
376 parse_worksheet(minimal_xml)
377 .map_err(|e| Error::Invalid(format!("Chartsheet parse error: {:?}", e)))?
378 } else {
379 parse_worksheet(&data).map_err(|e| Error::Invalid(format!("Parse error: {:?}", e)))?
380 };
381
382 let mut comments = Vec::new();
384 let mut charts = Vec::new();
385 #[cfg(feature = "sml-pivot")]
386 let mut pivot_tables: Vec<crate::types::CTPivotTableDefinition> = Vec::new();
387
388 if let Ok(sheet_rels) = self.package.read_part_relationships(&path) {
389 if !is_chartsheet && let Some(comments_rel) = sheet_rels.get_by_type(REL_COMMENTS) {
391 let comments_path = resolve_path(&path, &comments_rel.target);
392 if let Ok(comments_data) = self.package.read_part(&comments_path) {
393 comments = parse_comments_xml(&comments_data)?;
394 }
395 }
396
397 if let Some(drawing_rel) = sheet_rels.get_by_type(REL_DRAWING) {
399 let drawing_path = resolve_path(&path, &drawing_rel.target);
400 if let Ok(drawing_rels) = self.package.read_part_relationships(&drawing_path) {
401 for rel in drawing_rels.iter() {
402 let chart_path = resolve_path(&drawing_path, &rel.target);
403 if rel.relationship_type == REL_CHART
404 && let Ok(chart_data) = self.package.read_part(&chart_path)
405 && let Ok(chart) = parse_chart_ext(&chart_data)
406 {
407 charts.push(chart);
408 }
409 }
410 }
411 }
412
413 #[cfg(feature = "sml-pivot")]
415 for rel in sheet_rels.get_all_by_type(REL_PIVOT_TABLE) {
416 let pt_path = resolve_path(&path, &rel.target);
417 if let Ok(pt_data) = self.package.read_part(&pt_path)
418 && let Ok(pt) = bootstrap::<crate::types::CTPivotTableDefinition>(&pt_data)
419 {
420 pivot_tables.push(pt);
421 }
422 }
423 }
424
425 Ok(ResolvedSheet::with_extras(
426 info.name.clone(),
427 worksheet,
428 self.shared_strings.clone(),
429 comments,
430 charts,
431 #[cfg(feature = "sml-pivot")]
432 pivot_tables,
433 ))
434 }
435}
436
437fn parse_comments_xml(xml: &[u8]) -> Result<Vec<ExtComment>> {
439 let comments: crate::types::Comments = bootstrap(xml)?;
440 let authors = comments.authors.author.clone();
441 #[cfg(not(feature = "sml-comments"))]
442 {
443 let _ = (authors, comments.comment_list);
444 return Ok(Vec::new());
445 }
446 #[cfg(feature = "sml-comments")]
447 Ok(comments
448 .comment_list
449 .comment
450 .iter()
451 .map(|c| {
452 let author = authors.get(c.author_id as usize).cloned();
453 let text = rich_string_to_plain(&c.text);
454 ExtComment {
455 reference: c.reference.clone(),
456 author,
457 text,
458 }
459 })
460 .collect())
461}
462
463fn parse_chart_ext(xml: &[u8]) -> Result<ExtChart> {
465 let old_chart = parse_chart(xml)?;
466 Ok(ExtChart {
467 title: old_chart.title,
468 chart_type: match old_chart.chart_type {
469 ChartType::Bar | ChartType::Bar3D => ExtChartType::Bar,
470 ChartType::Line | ChartType::Line3D => ExtChartType::Line,
471 ChartType::Pie | ChartType::Pie3D => ExtChartType::Pie,
472 ChartType::Area | ChartType::Area3D => ExtChartType::Area,
473 ChartType::Surface | ChartType::Surface3D => ExtChartType::Surface,
474 ChartType::Scatter => ExtChartType::Scatter,
475 ChartType::Doughnut => ExtChartType::Doughnut,
476 ChartType::Radar => ExtChartType::Radar,
477 ChartType::Bubble => ExtChartType::Bubble,
478 ChartType::Stock => ExtChartType::Stock,
479 ChartType::Unknown => ExtChartType::Unknown,
480 },
481 })
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
490pub enum ConditionalRuleType {
491 Expression,
493 CellIs,
495 ColorScale,
497 DataBar,
499 IconSet,
501 Top10,
503 UniqueValues,
505 DuplicateValues,
507 ContainsText,
509 NotContainsText,
511 BeginsWith,
513 EndsWith,
515 ContainsBlanks,
517 NotContainsBlanks,
519 ContainsErrors,
521 NotContainsErrors,
523 TimePeriod,
525 AboveAverage,
527}
528
529impl ConditionalRuleType {
530 pub fn parse(s: &str) -> Option<Self> {
532 match s {
533 "expression" => Some(Self::Expression),
534 "cellIs" => Some(Self::CellIs),
535 "colorScale" => Some(Self::ColorScale),
536 "dataBar" => Some(Self::DataBar),
537 "iconSet" => Some(Self::IconSet),
538 "top10" => Some(Self::Top10),
539 "uniqueValues" => Some(Self::UniqueValues),
540 "duplicateValues" => Some(Self::DuplicateValues),
541 "containsText" => Some(Self::ContainsText),
542 "notContainsText" => Some(Self::NotContainsText),
543 "beginsWith" => Some(Self::BeginsWith),
544 "endsWith" => Some(Self::EndsWith),
545 "containsBlanks" => Some(Self::ContainsBlanks),
546 "notContainsBlanks" => Some(Self::NotContainsBlanks),
547 "containsErrors" => Some(Self::ContainsErrors),
548 "notContainsErrors" => Some(Self::NotContainsErrors),
549 "timePeriod" => Some(Self::TimePeriod),
550 "aboveAverage" => Some(Self::AboveAverage),
551 _ => None,
552 }
553 }
554
555 pub fn to_xml_value(self) -> &'static str {
557 match self {
558 Self::Expression => "expression",
559 Self::CellIs => "cellIs",
560 Self::ColorScale => "colorScale",
561 Self::DataBar => "dataBar",
562 Self::IconSet => "iconSet",
563 Self::Top10 => "top10",
564 Self::UniqueValues => "uniqueValues",
565 Self::DuplicateValues => "duplicateValues",
566 Self::ContainsText => "containsText",
567 Self::NotContainsText => "notContainsText",
568 Self::BeginsWith => "beginsWith",
569 Self::EndsWith => "endsWith",
570 Self::ContainsBlanks => "containsBlanks",
571 Self::NotContainsBlanks => "notContainsBlanks",
572 Self::ContainsErrors => "containsErrors",
573 Self::NotContainsErrors => "notContainsErrors",
574 Self::TimePeriod => "timePeriod",
575 Self::AboveAverage => "aboveAverage",
576 }
577 }
578}
579
580#[derive(Debug, Clone)]
585pub struct Chart {
586 title: Option<String>,
587 chart_type: ChartType,
588 series: Vec<ChartSeries>,
589}
590
591impl Chart {
592 pub fn title(&self) -> Option<&str> {
594 self.title.as_deref()
595 }
596
597 pub fn chart_type(&self) -> ChartType {
599 self.chart_type
600 }
601
602 pub fn series(&self) -> &[ChartSeries] {
604 &self.series
605 }
606}
607
608#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
612pub enum ChartType {
613 Area,
614 Area3D,
615 Bar,
616 Bar3D,
617 Bubble,
618 Doughnut,
619 #[default]
620 Line,
621 Line3D,
622 Pie,
623 Pie3D,
624 Radar,
625 Scatter,
626 Stock,
627 Surface,
628 Surface3D,
629 Unknown,
630}
631
632#[derive(Debug, Clone)]
634pub struct ChartSeries {
635 index: u32,
636 name: Option<String>,
637 category_ref: Option<String>,
638 value_ref: Option<String>,
639 categories: Vec<String>,
640 values: Vec<f64>,
641}
642
643impl ChartSeries {
644 pub fn index(&self) -> u32 {
646 self.index
647 }
648
649 pub fn name(&self) -> Option<&str> {
651 self.name.as_deref()
652 }
653
654 pub fn category_ref(&self) -> Option<&str> {
656 self.category_ref.as_deref()
657 }
658
659 pub fn value_ref(&self) -> Option<&str> {
661 self.value_ref.as_deref()
662 }
663
664 pub fn categories(&self) -> &[String] {
666 &self.categories
667 }
668
669 pub fn values(&self) -> &[f64] {
671 &self.values
672 }
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
679pub enum DataValidationType {
680 #[default]
682 None,
683 Whole,
685 Decimal,
687 List,
689 Date,
691 Time,
693 TextLength,
695 Custom,
697}
698
699impl DataValidationType {
700 #[allow(dead_code)]
702 fn parse(s: &str) -> Self {
703 match s {
704 "none" => Self::None,
705 "whole" => Self::Whole,
706 "decimal" => Self::Decimal,
707 "list" => Self::List,
708 "date" => Self::Date,
709 "time" => Self::Time,
710 "textLength" => Self::TextLength,
711 "custom" => Self::Custom,
712 _ => Self::None,
713 }
714 }
715
716 pub fn to_xml_value(self) -> &'static str {
718 match self {
719 Self::None => "none",
720 Self::Whole => "whole",
721 Self::Decimal => "decimal",
722 Self::List => "list",
723 Self::Date => "date",
724 Self::Time => "time",
725 Self::TextLength => "textLength",
726 Self::Custom => "custom",
727 }
728 }
729}
730
731#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
735pub enum DataValidationOperator {
736 #[default]
738 Between,
739 NotBetween,
741 Equal,
743 NotEqual,
745 LessThan,
747 LessThanOrEqual,
749 GreaterThan,
751 GreaterThanOrEqual,
753}
754
755impl DataValidationOperator {
756 #[allow(dead_code)]
758 fn parse(s: &str) -> Self {
759 match s {
760 "between" => Self::Between,
761 "notBetween" => Self::NotBetween,
762 "equal" => Self::Equal,
763 "notEqual" => Self::NotEqual,
764 "lessThan" => Self::LessThan,
765 "lessThanOrEqual" => Self::LessThanOrEqual,
766 "greaterThan" => Self::GreaterThan,
767 "greaterThanOrEqual" => Self::GreaterThanOrEqual,
768 _ => Self::Between,
769 }
770 }
771
772 pub fn to_xml_value(self) -> &'static str {
774 match self {
775 Self::Between => "between",
776 Self::NotBetween => "notBetween",
777 Self::Equal => "equal",
778 Self::NotEqual => "notEqual",
779 Self::LessThan => "lessThan",
780 Self::LessThanOrEqual => "lessThanOrEqual",
781 Self::GreaterThan => "greaterThan",
782 Self::GreaterThanOrEqual => "greaterThanOrEqual",
783 }
784 }
785}
786
787#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
791pub enum DataValidationErrorStyle {
792 #[default]
794 Stop,
795 Warning,
797 Information,
799}
800
801impl DataValidationErrorStyle {
802 #[allow(dead_code)]
804 fn parse(s: &str) -> Self {
805 match s {
806 "stop" => Self::Stop,
807 "warning" => Self::Warning,
808 "information" => Self::Information,
809 _ => Self::Stop,
810 }
811 }
812
813 pub fn to_xml_value(self) -> &'static str {
815 match self {
816 Self::Stop => "stop",
817 Self::Warning => "warning",
818 Self::Information => "information",
819 }
820 }
821}
822
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub enum DefinedNameScope {
826 Workbook,
828 Sheet(u32),
830}
831
832pub trait StylesheetExt {
838 fn format_code(&self, id: u32) -> Option<String>;
842
843 fn is_date_format(&self, id: u32) -> bool;
845}
846
847impl StylesheetExt for crate::types::Stylesheet {
848 fn format_code(&self, id: u32) -> Option<String> {
849 #[cfg(feature = "sml-styling")]
850 if let Some(num_fmts) = &self.num_fmts
851 && let Some(fmt) = num_fmts.num_fmt.iter().find(|f| f.number_format_id == id)
852 {
853 return Some(fmt.format_code.clone());
854 }
855 builtin_format_code(id).map(|s| s.to_string())
856 }
857
858 fn is_date_format(&self, id: u32) -> bool {
859 if let Some(code) = self.format_code(id) {
860 is_date_format_code(&code)
861 } else {
862 matches!(id, 14..=22 | 45..=47)
864 }
865 }
866}
867
868pub trait DefinedNameExt {
870 fn is_builtin(&self) -> bool;
879
880 fn scope(&self) -> DefinedNameScope;
882}
883
884impl DefinedNameExt for crate::types::DefinedName {
885 fn is_builtin(&self) -> bool {
886 self.name.starts_with("_xlnm.")
887 }
888
889 fn scope(&self) -> DefinedNameScope {
890 match self.local_sheet_id {
891 Some(id) => DefinedNameScope::Sheet(id),
892 None => DefinedNameScope::Workbook,
893 }
894 }
895}
896
897pub fn builtin_format_code(id: u32) -> Option<&'static str> {
906 match id {
907 0 => Some("General"),
908 1 => Some("0"),
909 2 => Some("0.00"),
910 3 => Some("#,##0"),
911 4 => Some("#,##0.00"),
912 9 => Some("0%"),
913 10 => Some("0.00%"),
914 11 => Some("0.00E+00"),
915 12 => Some("# ?/?"),
916 13 => Some("# ??/??"),
917 14 => Some("mm-dd-yy"),
918 15 => Some("d-mmm-yy"),
919 16 => Some("d-mmm"),
920 17 => Some("mmm-yy"),
921 18 => Some("h:mm AM/PM"),
922 19 => Some("h:mm:ss AM/PM"),
923 20 => Some("h:mm"),
924 21 => Some("h:mm:ss"),
925 22 => Some("m/d/yy h:mm"),
926 37 => Some("#,##0 ;(#,##0)"),
927 38 => Some("#,##0 ;[Red](#,##0)"),
928 39 => Some("#,##0.00;(#,##0.00)"),
929 40 => Some("#,##0.00;[Red](#,##0.00)"),
930 45 => Some("mm:ss"),
931 46 => Some("[h]:mm:ss"),
932 47 => Some("mmss.0"),
933 48 => Some("##0.0E+0"),
934 49 => Some("@"),
935 _ => None,
936 }
937}
938
939pub fn excel_date_to_ymd(serial: f64) -> Option<(i32, u32, u32)> {
945 if serial < 1.0 {
946 return None;
947 }
948
949 let mut days = serial.floor() as i32;
950
951 if days > 60 {
954 days -= 1;
955 } else if days == 60 {
956 return Some((1900, 2, 29));
958 }
959
960 days -= 1; let mut year = 1900;
966 loop {
967 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
968 if days < days_in_year {
969 break;
970 }
971 days -= days_in_year;
972 year += 1;
973 }
974
975 let leap = is_leap_year(year);
977 let days_in_months: [i32; 12] = if leap {
978 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
979 } else {
980 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
981 };
982
983 let mut month = 1u32;
984 for &dim in &days_in_months {
985 if days < dim {
986 break;
987 }
988 days -= dim;
989 month += 1;
990 }
991
992 Some((year, month, (days + 1) as u32))
993}
994
995fn is_leap_year(year: i32) -> bool {
997 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
998}
999
1000pub fn excel_datetime_to_ymdhms(serial: f64) -> Option<(i32, u32, u32, u32, u32, u32)> {
1002 let (y, m, d) = excel_date_to_ymd(serial)?;
1003
1004 let time_fraction = serial.fract();
1006 let total_seconds = (time_fraction * 86400.0).round() as u32;
1007 let hours = total_seconds / 3600;
1008 let minutes = (total_seconds % 3600) / 60;
1009 let seconds = total_seconds % 60;
1010
1011 Some((y, m, d, hours, minutes, seconds))
1012}
1013
1014pub fn format_excel_date(serial: f64) -> Option<String> {
1016 let (y, m, d) = excel_date_to_ymd(serial)?;
1017 Some(format!("{:04}-{:02}-{:02}", y, m, d))
1018}
1019
1020pub fn format_excel_datetime(serial: f64) -> Option<String> {
1022 let (y, m, d, h, min, s) = excel_datetime_to_ymdhms(serial)?;
1023 if h == 0 && min == 0 && s == 0 {
1024 Some(format!("{:04}-{:02}-{:02}", y, m, d))
1025 } else {
1026 Some(format!(
1027 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
1028 y, m, d, h, min, s
1029 ))
1030 }
1031}
1032
1033fn is_date_format_code(code: &str) -> bool {
1035 let code = code.to_lowercase();
1037
1038 let mut clean = String::new();
1040 let mut in_bracket = false;
1041 for c in code.chars() {
1042 match c {
1043 '[' => in_bracket = true,
1044 ']' => in_bracket = false,
1045 _ if !in_bracket => clean.push(c),
1046 _ => {}
1047 }
1048 }
1049
1050 let date_tokens = ["y", "m", "d", "h", "s"];
1052 for token in date_tokens {
1053 if clean.contains(token) {
1054 return true;
1056 }
1057 }
1058
1059 false
1060}
1061
1062pub(crate) fn bootstrap<T: FromXml>(xml: &[u8]) -> Result<T> {
1070 let mut reader = Reader::from_reader(Cursor::new(xml));
1071 let mut buf = Vec::new();
1072 loop {
1073 match reader.read_event_into(&mut buf) {
1074 Ok(Event::Start(e)) => {
1075 return T::from_xml(&mut reader, &e, false)
1076 .map_err(|e| Error::Invalid(format!("{e:?}")));
1077 }
1078 Ok(Event::Empty(e)) => {
1079 return T::from_xml(&mut reader, &e, true)
1080 .map_err(|e| Error::Invalid(format!("{e:?}")));
1081 }
1082 Ok(Event::Eof) => break,
1083 Err(e) => return Err(Error::Xml(e)),
1084 _ => {}
1085 }
1086 buf.clear();
1087 }
1088 Err(Error::Invalid("element not found".into()))
1089}
1090
1091fn extract_shared_strings(sst: &crate::types::SharedStrings) -> Vec<String> {
1093 sst.si
1094 .iter()
1095 .map(|si| {
1096 if let Some(t) = &si.cell_type {
1097 t.clone()
1098 } else {
1099 si.reference.iter().map(|r| r.cell_type.as_str()).collect()
1100 }
1101 })
1102 .collect()
1103}
1104
1105fn rich_string_to_plain(rs: &crate::types::RichString) -> String {
1107 if let Some(t) = &rs.cell_type {
1108 t.clone()
1109 } else {
1110 rs.reference.iter().map(|r| r.cell_type.as_str()).collect()
1111 }
1112}
1113
1114#[allow(dead_code)]
1116fn parse_chart(xml: &[u8]) -> Result<Chart> {
1117 let mut reader = Reader::from_reader(Cursor::new(xml));
1118 let mut buf = Vec::new();
1119
1120 let mut title: Option<String> = None;
1121 let mut chart_type = ChartType::Unknown;
1122 let mut series: Vec<ChartSeries> = Vec::new();
1123
1124 let mut in_chart = false;
1125 let mut in_plot_area = false;
1126 let mut in_title = false;
1127 let mut in_title_tx = false;
1128 let mut in_title_rich = false;
1129 let mut in_title_p = false;
1130 let mut in_title_r = false;
1131 let mut in_title_t = false;
1132 let mut in_ser = false;
1133 let mut in_cat = false;
1134 let mut in_val = false;
1135 let mut in_str_ref = false;
1136 let mut in_num_ref = false;
1137 let mut in_str_cache = false;
1138 let mut in_num_cache = false;
1139 let mut in_pt = false;
1140 let mut in_v = false;
1141 let mut in_f = false;
1142 let mut in_tx = false;
1143 let mut in_ser_name_str_ref = false;
1144
1145 let mut title_text = String::new();
1146 let mut current_series_idx: u32 = 0;
1147 let mut current_series_name: Option<String> = None;
1148 let mut current_cat_ref: Option<String> = None;
1149 let mut current_val_ref: Option<String> = None;
1150 let mut current_cat_values: Vec<String> = Vec::new();
1151 let mut current_val_values: Vec<f64> = Vec::new();
1152 let mut current_ref = String::new();
1153 let mut current_v = String::new();
1154
1155 loop {
1156 match reader.read_event_into(&mut buf) {
1157 Ok(Event::Start(e)) => {
1158 let name = e.local_name();
1159 let name = name.as_ref();
1160 match name {
1161 b"chart" => in_chart = true,
1162 b"plotArea" if in_chart => in_plot_area = true,
1163 b"lineChart" | b"line3DChart" if in_plot_area => {
1164 chart_type = if name == b"line3DChart" {
1165 ChartType::Line3D
1166 } else {
1167 ChartType::Line
1168 };
1169 }
1170 b"barChart" | b"bar3DChart" if in_plot_area => {
1171 chart_type = if name == b"bar3DChart" {
1172 ChartType::Bar3D
1173 } else {
1174 ChartType::Bar
1175 };
1176 }
1177 b"areaChart" | b"area3DChart" if in_plot_area => {
1178 chart_type = if name == b"area3DChart" {
1179 ChartType::Area3D
1180 } else {
1181 ChartType::Area
1182 };
1183 }
1184 b"pieChart" | b"pie3DChart" if in_plot_area => {
1185 chart_type = if name == b"pie3DChart" {
1186 ChartType::Pie3D
1187 } else {
1188 ChartType::Pie
1189 };
1190 }
1191 b"doughnutChart" if in_plot_area => chart_type = ChartType::Doughnut,
1192 b"scatterChart" if in_plot_area => chart_type = ChartType::Scatter,
1193 b"bubbleChart" if in_plot_area => chart_type = ChartType::Bubble,
1194 b"radarChart" if in_plot_area => chart_type = ChartType::Radar,
1195 b"stockChart" if in_plot_area => chart_type = ChartType::Stock,
1196 b"surfaceChart" | b"surface3DChart" if in_plot_area => {
1197 chart_type = if name == b"surface3DChart" {
1198 ChartType::Surface3D
1199 } else {
1200 ChartType::Surface
1201 };
1202 }
1203 b"title" if in_chart && !in_plot_area => {
1204 in_title = true;
1205 title_text.clear();
1206 }
1207 b"tx" if in_title => in_title_tx = true,
1208 b"rich" if in_title_tx => in_title_rich = true,
1209 b"p" if in_title_rich => in_title_p = true,
1210 b"r" if in_title_p => in_title_r = true,
1211 b"t" if in_title_r => in_title_t = true,
1212 b"ser" if in_plot_area => {
1213 in_ser = true;
1214 current_series_idx = 0;
1215 current_series_name = None;
1216 current_cat_ref = None;
1217 current_val_ref = None;
1218 current_cat_values.clear();
1219 current_val_values.clear();
1220 }
1221 b"idx" if in_ser => {
1222 for attr in e.attributes().filter_map(|a| a.ok()) {
1223 if attr.key.as_ref() == b"val" {
1224 current_series_idx =
1225 String::from_utf8_lossy(&attr.value).parse().unwrap_or(0);
1226 }
1227 }
1228 }
1229 b"tx" if in_ser && !in_cat && !in_val => in_tx = true,
1230 b"strRef" if in_tx => in_ser_name_str_ref = true,
1231 b"v" if in_ser_name_str_ref => in_v = true,
1232 b"cat" if in_ser => in_cat = true,
1233 b"val" if in_ser => in_val = true,
1234 b"strRef" if in_cat || in_val => {
1235 in_str_ref = true;
1236 current_ref.clear();
1237 }
1238 b"numRef" if in_cat || in_val => {
1239 in_num_ref = true;
1240 current_ref.clear();
1241 }
1242 b"strCache" if in_str_ref => in_str_cache = true,
1243 b"numCache" if in_num_ref => in_num_cache = true,
1244 b"pt" if in_str_cache || in_num_cache => {
1245 in_pt = true;
1246 current_v.clear();
1247 }
1248 b"v" if in_pt => in_v = true,
1249 b"f" if (in_str_ref || in_num_ref) && !in_str_cache && !in_num_cache => {
1250 in_f = true;
1251 current_ref.clear();
1252 }
1253 _ => {}
1254 }
1255 }
1256 Ok(Event::Text(e)) => {
1257 let text = e.decode().unwrap_or_default();
1258 if in_title_t {
1259 title_text.push_str(&text);
1260 } else if in_v {
1261 current_v.push_str(&text);
1262 } else if in_f {
1263 current_ref.push_str(&text);
1264 }
1265 }
1266 Ok(Event::End(e)) => {
1267 let name = e.local_name();
1268 let name = name.as_ref();
1269 match name {
1270 b"chart" => in_chart = false,
1271 b"plotArea" => in_plot_area = false,
1272 b"title" if in_title => {
1273 in_title = false;
1274 if !title_text.is_empty() {
1275 title = Some(std::mem::take(&mut title_text));
1276 }
1277 }
1278 b"tx" if in_title => in_title_tx = false,
1279 b"rich" if in_title_rich => in_title_rich = false,
1280 b"p" if in_title_p => in_title_p = false,
1281 b"r" if in_title_r => in_title_r = false,
1282 b"t" if in_title_t => in_title_t = false,
1283 b"ser" if in_ser => {
1284 in_ser = false;
1285 series.push(ChartSeries {
1286 index: current_series_idx,
1287 name: current_series_name.take(),
1288 category_ref: current_cat_ref.take(),
1289 value_ref: current_val_ref.take(),
1290 categories: std::mem::take(&mut current_cat_values),
1291 values: std::mem::take(&mut current_val_values),
1292 });
1293 }
1294 b"tx" if in_tx => in_tx = false,
1295 b"strRef" if in_ser_name_str_ref => in_ser_name_str_ref = false,
1296 b"v" if in_v => {
1297 in_v = false;
1298 if in_pt {
1299 if in_str_cache && in_cat {
1300 current_cat_values.push(std::mem::take(&mut current_v));
1301 } else if in_num_cache {
1302 if let Ok(v) = current_v.parse::<f64>() {
1303 if in_cat {
1304 current_cat_values.push(current_v.clone());
1305 } else if in_val {
1306 current_val_values.push(v);
1307 }
1308 }
1309 current_v.clear();
1310 }
1311 } else if in_ser_name_str_ref {
1312 current_series_name = Some(std::mem::take(&mut current_v));
1313 }
1314 }
1315 b"cat" if in_cat => in_cat = false,
1316 b"val" if in_val => in_val = false,
1317 b"strRef" if in_str_ref => {
1318 in_str_ref = false;
1319 if in_cat && !current_ref.is_empty() {
1320 current_cat_ref = Some(std::mem::take(&mut current_ref));
1321 }
1322 }
1323 b"numRef" if in_num_ref => {
1324 in_num_ref = false;
1325 if in_cat && current_cat_ref.is_none() && !current_ref.is_empty() {
1326 current_cat_ref = Some(std::mem::take(&mut current_ref));
1327 } else if in_val && !current_ref.is_empty() {
1328 current_val_ref = Some(std::mem::take(&mut current_ref));
1329 }
1330 }
1331 b"strCache" if in_str_cache => in_str_cache = false,
1332 b"numCache" if in_num_cache => in_num_cache = false,
1333 b"pt" if in_pt => in_pt = false,
1334 b"f" if in_f => {
1335 in_f = false;
1336 if (in_str_ref || in_num_ref) && !current_ref.is_empty() {
1337 if in_cat && current_cat_ref.is_none() {
1338 current_cat_ref = Some(current_ref.clone());
1339 } else if in_val && current_val_ref.is_none() {
1340 current_val_ref = Some(current_ref.clone());
1341 }
1342 }
1343 }
1344 _ => {}
1345 }
1346 }
1347 Ok(Event::Eof) => break,
1348 Err(e) => return Err(Error::Xml(e)),
1349 _ => {}
1350 }
1351 buf.clear();
1352 }
1353
1354 Ok(Chart {
1355 title,
1356 chart_type,
1357 series,
1358 })
1359}
1360
1361fn resolve_path(base: &str, target: &str) -> String {
1364 let has_leading_slash = base.starts_with('/');
1365
1366 if target.starts_with('/') {
1367 return target.to_string();
1368 }
1369
1370 let base_dir = if let Some(idx) = base.rfind('/') {
1372 &base[..idx]
1373 } else {
1374 ""
1375 };
1376
1377 let mut parts: Vec<&str> = base_dir.split('/').filter(|s| !s.is_empty()).collect();
1379 for segment in target.split('/') {
1380 match segment {
1381 ".." => {
1382 parts.pop();
1383 }
1384 "." | "" => {}
1385 _ => {
1386 parts.push(segment);
1387 }
1388 }
1389 }
1390
1391 let result = parts.join("/");
1392 if has_leading_slash {
1393 format!("/{}", result)
1394 } else {
1395 result
1396 }
1397}
1398
1399#[cfg(test)]
1400mod tests {
1401 use super::*;
1402
1403 #[test]
1404 fn test_resolve_path() {
1405 assert_eq!(
1406 resolve_path("/xl/workbook.xml", "worksheets/sheet1.xml"),
1407 "/xl/worksheets/sheet1.xml"
1408 );
1409 assert_eq!(
1410 resolve_path("/xl/workbook.xml", "/xl/sharedStrings.xml"),
1411 "/xl/sharedStrings.xml"
1412 );
1413 assert_eq!(
1415 resolve_path("/xl/chartsheets/sheet1.xml", "../drawings/drawing1.xml"),
1416 "/xl/drawings/drawing1.xml"
1417 );
1418 assert_eq!(
1419 resolve_path("/xl/worksheets/sheet1.xml", "../comments1.xml"),
1420 "/xl/comments1.xml"
1421 );
1422 }
1423
1424 #[test]
1425 fn test_bootstrap_shared_strings() {
1426 let xml = r#"<?xml version="1.0"?>
1427 <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1428 <si><t>Hello</t></si>
1429 <si><t>World</t></si>
1430 <si><t></t></si>
1431 </sst>"#;
1432
1433 let sst: crate::types::SharedStrings = bootstrap(xml.as_bytes()).unwrap();
1434 let strings = extract_shared_strings(&sst);
1435 assert_eq!(strings, vec!["Hello", "World", ""]);
1436 }
1437
1438 #[test]
1439 #[cfg(feature = "sml-styling")]
1440 fn test_stylesheet_ext() {
1441 let xml = r#"<?xml version="1.0"?>
1442 <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1443 <numFmts count="1">
1444 <numFmt numFmtId="164" formatCode="0.00%"/>
1445 </numFmts>
1446 </styleSheet>"#;
1447
1448 let styles: crate::types::Stylesheet = bootstrap(xml.as_bytes()).unwrap();
1449
1450 assert_eq!(styles.format_code(164), Some("0.00%".to_string()));
1452
1453 assert_eq!(styles.format_code(14), Some("mm-dd-yy".to_string()));
1455 assert_eq!(styles.format_code(0), Some("General".to_string()));
1456 assert_eq!(styles.format_code(100), None);
1457
1458 assert!(styles.is_date_format(14));
1460 assert!(!styles.is_date_format(0));
1461 assert!(!styles.is_date_format(164)); }
1463
1464 #[test]
1465 #[cfg(feature = "sml-comments")]
1466 fn test_parse_comments_xml() {
1467 let xml = r#"<?xml version="1.0"?>
1468 <comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1469 <authors>
1470 <author>John Doe</author>
1471 <author>Jane Smith</author>
1472 </authors>
1473 <commentList>
1474 <comment ref="A1" authorId="0">
1475 <text><t>This is a comment on A1</t></text>
1476 </comment>
1477 <comment ref="B2" authorId="1">
1478 <text>
1479 <r><t>Multi-line</t></r>
1480 <r><t> comment</t></r>
1481 </text>
1482 </comment>
1483 </commentList>
1484 </comments>"#;
1485
1486 let comments = parse_comments_xml(xml.as_bytes()).unwrap();
1487 assert_eq!(comments.len(), 2);
1488
1489 assert_eq!(comments[0].reference, "A1");
1490 assert_eq!(comments[0].author, Some("John Doe".to_string()));
1491 assert_eq!(comments[0].text, "This is a comment on A1");
1492
1493 assert_eq!(comments[1].reference, "B2");
1494 assert_eq!(comments[1].author, Some("Jane Smith".to_string()));
1495 assert_eq!(comments[1].text, "Multi-line comment");
1496 }
1497
1498 #[test]
1499 fn test_bootstrap_defined_names() {
1500 let xml = r#"<?xml version="1.0"?>
1501 <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
1502 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1503 <sheets>
1504 <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
1505 </sheets>
1506 <definedNames>
1507 <definedName name="MyRange">Sheet1!$A$1:$B$10</definedName>
1508 <definedName name="LocalName" localSheetId="0">Sheet1!$C$1:$C$5</definedName>
1509 <definedName name="_xlnm.Print_Area" localSheetId="0" comment="Print area">Sheet1!$A$1:$F$20</definedName>
1510 </definedNames>
1511 </workbook>"#;
1512
1513 let wb: crate::types::Workbook = bootstrap(xml.as_bytes()).unwrap();
1514 let names = wb
1515 .defined_names
1516 .map(|dn| {
1517 let inner = *dn;
1518 inner.defined_name
1519 })
1520 .unwrap_or_default();
1521 assert_eq!(names.len(), 3);
1522
1523 assert_eq!(names[0].name, "MyRange");
1525 assert_eq!(names[0].text.as_deref(), Some("Sheet1!$A$1:$B$10"));
1526 assert!(names[0].local_sheet_id.is_none());
1527 assert!(!names[0].is_builtin());
1528 assert_eq!(names[0].scope(), DefinedNameScope::Workbook);
1529
1530 assert_eq!(names[1].name, "LocalName");
1532 assert_eq!(names[1].local_sheet_id, Some(0));
1533 assert_eq!(names[1].scope(), DefinedNameScope::Sheet(0));
1534
1535 assert_eq!(names[2].name, "_xlnm.Print_Area");
1537 assert!(names[2].is_builtin());
1538 assert_eq!(names[2].comment.as_deref(), Some("Print area"));
1539 }
1540
1541 #[test]
1542 fn test_excel_date_conversion() {
1543 assert_eq!(excel_date_to_ymd(36526.0), Some((2000, 1, 1)));
1546
1547 assert_eq!(excel_date_to_ymd(36525.0), Some((1999, 12, 31)));
1549
1550 assert_eq!(excel_date_to_ymd(1.0), Some((1900, 1, 1)));
1552
1553 assert_eq!(excel_date_to_ymd(61.0), Some((1900, 3, 1)));
1555
1556 assert_eq!(
1559 excel_datetime_to_ymdhms(36526.5),
1560 Some((2000, 1, 1, 12, 0, 0))
1561 );
1562
1563 assert_eq!(format_excel_date(36526.0), Some("2000-01-01".to_string()));
1565 assert_eq!(
1566 format_excel_datetime(36526.5),
1567 Some("2000-01-01 12:00:00".to_string())
1568 );
1569 }
1570
1571 #[test]
1572 fn test_builtin_format_codes() {
1573 assert_eq!(builtin_format_code(0), Some("General"));
1574 assert_eq!(builtin_format_code(1), Some("0"));
1575 assert_eq!(builtin_format_code(14), Some("mm-dd-yy"));
1576 assert_eq!(builtin_format_code(22), Some("m/d/yy h:mm"));
1577 assert_eq!(builtin_format_code(49), Some("@"));
1578 assert_eq!(builtin_format_code(100), None);
1579 }
1580
1581 #[test]
1582 fn test_is_date_format() {
1583 assert!(is_date_format_code("mm-dd-yy"));
1584 assert!(is_date_format_code("yyyy-mm-dd"));
1585 assert!(is_date_format_code("d-mmm-yy"));
1586 assert!(is_date_format_code("h:mm:ss"));
1587 assert!(is_date_format_code("[Red]yyyy-mm-dd")); assert!(!is_date_format_code("0.00"));
1589 assert!(!is_date_format_code("#,##0"));
1590 assert!(!is_date_format_code("General"));
1591 assert!(!is_date_format_code("@")); }
1593}