1use crate::parsers::{FromXml, ParseError};
32use crate::types::{Cell, CellType, Row, SheetData, Worksheet};
33use quick_xml::Reader;
34use quick_xml::events::Event;
35use std::io::Cursor;
36
37#[derive(Debug, Clone, PartialEq)]
39pub enum CellValue {
40 Empty,
42 String(String),
44 Number(f64),
46 Boolean(bool),
48 Error(String),
50}
51
52impl CellValue {
53 pub fn is_empty(&self) -> bool {
55 matches!(self, CellValue::Empty)
56 }
57
58 pub fn to_display_string(&self) -> String {
60 match self {
61 CellValue::Empty => String::new(),
62 CellValue::String(s) => s.clone(),
63 CellValue::Number(n) => n.to_string(),
64 CellValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
65 CellValue::Error(e) => e.clone(),
66 }
67 }
68
69 pub fn as_number(&self) -> Option<f64> {
71 match self {
72 CellValue::Number(n) => Some(*n),
73 CellValue::String(s) => s.parse().ok(),
74 _ => None,
75 }
76 }
77
78 pub fn as_bool(&self) -> Option<bool> {
80 match self {
81 CellValue::Boolean(b) => Some(*b),
82 CellValue::Number(n) => Some(*n != 0.0),
83 CellValue::String(s) => match s.to_lowercase().as_str() {
84 "true" | "1" => Some(true),
85 "false" | "0" => Some(false),
86 _ => None,
87 },
88 _ => None,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Default)]
98pub struct ResolveContext {
99 pub shared_strings: Vec<String>,
101 }
103
104impl ResolveContext {
105 pub fn new(shared_strings: Vec<String>) -> Self {
107 Self { shared_strings }
108 }
109
110 pub fn shared_string(&self, index: usize) -> Option<&str> {
112 self.shared_strings.get(index).map(|s| s.as_str())
113 }
114}
115
116pub trait CellExt {
122 fn reference_str(&self) -> Option<&str>;
124
125 fn column_number(&self) -> Option<u32>;
127
128 fn row_number(&self) -> Option<u32>;
130
131 fn has_formula(&self) -> bool;
133
134 fn formula_text(&self) -> Option<&str>;
136
137 fn raw_value(&self) -> Option<&str>;
139
140 fn cell_type(&self) -> Option<CellType>;
142
143 fn is_shared_string(&self) -> bool;
145
146 fn is_number(&self) -> bool;
148
149 fn is_boolean(&self) -> bool;
151
152 fn is_error(&self) -> bool;
154}
155
156impl CellExt for Cell {
157 fn reference_str(&self) -> Option<&str> {
158 self.reference.as_deref()
159 }
160
161 fn column_number(&self) -> Option<u32> {
162 let reference = self.reference.as_ref()?;
163 parse_column(reference)
164 }
165
166 fn row_number(&self) -> Option<u32> {
167 let reference = self.reference.as_ref()?;
168 parse_row(reference)
169 }
170
171 fn has_formula(&self) -> bool {
172 self.formula.is_some()
173 }
174
175 fn formula_text(&self) -> Option<&str> {
176 self.formula.as_ref().and_then(|f| f.text.as_deref())
177 }
178
179 fn raw_value(&self) -> Option<&str> {
180 self.value.as_deref()
181 }
182
183 fn cell_type(&self) -> Option<CellType> {
184 self.cell_type
185 }
186
187 fn is_shared_string(&self) -> bool {
188 matches!(self.cell_type, Some(CellType::SharedString))
189 }
190
191 fn is_number(&self) -> bool {
192 matches!(self.cell_type, Some(CellType::Number)) || self.cell_type.is_none()
193 }
194
195 fn is_boolean(&self) -> bool {
196 matches!(self.cell_type, Some(CellType::Boolean))
197 }
198
199 fn is_error(&self) -> bool {
200 matches!(self.cell_type, Some(CellType::Error))
201 }
202}
203
204pub trait CellResolveExt {
207 fn resolved_value(&self, ctx: &ResolveContext) -> CellValue;
209
210 fn value_as_string(&self, ctx: &ResolveContext) -> String;
212
213 fn value_as_number(&self, ctx: &ResolveContext) -> Option<f64>;
215
216 fn value_as_bool(&self, ctx: &ResolveContext) -> Option<bool>;
218}
219
220impl CellResolveExt for Cell {
221 fn resolved_value(&self, ctx: &ResolveContext) -> CellValue {
222 let raw = match &self.value {
223 Some(v) => v.as_str(),
224 None => return CellValue::Empty,
225 };
226
227 match &self.cell_type {
228 Some(CellType::SharedString) => {
229 if let Ok(idx) = raw.parse::<usize>()
231 && let Some(s) = ctx.shared_string(idx)
232 {
233 return CellValue::String(s.to_string());
234 }
235 CellValue::Error(format!("#REF! (invalid shared string index: {})", raw))
236 }
237 Some(CellType::Boolean) => {
238 CellValue::Boolean(raw == "1" || raw.eq_ignore_ascii_case("true"))
240 }
241 Some(CellType::Error) => {
242 CellValue::Error(raw.to_string())
244 }
245 Some(CellType::String) | Some(CellType::InlineString) => {
246 CellValue::String(raw.to_string())
248 }
249 Some(CellType::Number) | None => {
250 if raw.is_empty() {
252 CellValue::Empty
253 } else if let Ok(n) = raw.parse::<f64>() {
254 CellValue::Number(n)
255 } else {
256 CellValue::String(raw.to_string())
258 }
259 }
260 }
261 }
262
263 fn value_as_string(&self, ctx: &ResolveContext) -> String {
264 self.resolved_value(ctx).to_display_string()
265 }
266
267 fn value_as_number(&self, ctx: &ResolveContext) -> Option<f64> {
268 self.resolved_value(ctx).as_number()
269 }
270
271 fn value_as_bool(&self, ctx: &ResolveContext) -> Option<bool> {
272 self.resolved_value(ctx).as_bool()
273 }
274}
275
276pub trait RowExt {
282 fn row_number(&self) -> Option<u32>;
284
285 fn cell_count(&self) -> usize;
287
288 fn is_empty(&self) -> bool;
290
291 fn cell_at_column(&self, col: u32) -> Option<&Cell>;
293
294 fn cells_iter(&self) -> impl Iterator<Item = &Cell>;
296}
297
298impl RowExt for Row {
299 fn row_number(&self) -> Option<u32> {
300 self.reference
301 }
302
303 fn cell_count(&self) -> usize {
304 self.cells.len()
305 }
306
307 fn is_empty(&self) -> bool {
308 self.cells.is_empty()
309 }
310
311 fn cell_at_column(&self, col: u32) -> Option<&Cell> {
312 self.cells.iter().find(|c| {
313 c.reference
314 .as_ref()
315 .and_then(|r| parse_column(r))
316 .map(|c_col| c_col == col)
317 .unwrap_or(false)
318 })
319 }
320
321 fn cells_iter(&self) -> impl Iterator<Item = &Cell> {
322 self.cells.iter()
323 }
324}
325
326pub fn parse_worksheet(xml: &[u8]) -> Result<Worksheet, ParseError> {
335 let mut reader = Reader::from_reader(Cursor::new(xml));
336 let mut buf = Vec::new();
337
338 loop {
339 match reader.read_event_into(&mut buf) {
340 Ok(Event::Start(e)) => return Worksheet::from_xml(&mut reader, &e, false),
341 Ok(Event::Empty(e)) => return Worksheet::from_xml(&mut reader, &e, true),
342 Ok(Event::Eof) => break,
343 Err(e) => return Err(ParseError::Xml(e)),
344 _ => {}
345 }
346 buf.clear();
347 }
348 Err(ParseError::UnexpectedElement(
349 "no worksheet element found".to_string(),
350 ))
351}
352
353pub trait WorksheetExt {
359 fn sheet_data(&self) -> &SheetData;
361
362 fn row_count(&self) -> usize;
364
365 fn is_empty(&self) -> bool;
367
368 fn row(&self, row_num: u32) -> Option<&Row>;
370
371 fn cell(&self, reference: &str) -> Option<&Cell>;
373
374 fn rows(&self) -> impl Iterator<Item = &Row>;
376
377 #[cfg(feature = "sml-filtering")]
379 fn has_auto_filter(&self) -> bool;
380
381 fn has_merged_cells(&self) -> bool;
383
384 #[cfg(feature = "sml-styling")]
386 fn has_conditional_formatting(&self) -> bool;
387
388 #[cfg(feature = "sml-validation")]
390 fn has_data_validations(&self) -> bool;
391
392 #[cfg(feature = "sml-structure")]
394 fn has_freeze_panes(&self) -> bool;
395
396 #[cfg(feature = "sml-styling")]
401 fn get_row_height(&self, row_num: u32) -> Option<f64>;
402
403 #[cfg(feature = "sml-styling")]
409 fn get_column_width(&self, col_idx: u32) -> Option<f64>;
410}
411
412impl WorksheetExt for Worksheet {
413 fn sheet_data(&self) -> &SheetData {
414 &self.sheet_data
415 }
416
417 fn row_count(&self) -> usize {
418 self.sheet_data.row.len()
419 }
420
421 fn is_empty(&self) -> bool {
422 self.sheet_data.row.is_empty()
423 }
424
425 fn row(&self, row_num: u32) -> Option<&Row> {
426 self.sheet_data
427 .row
428 .iter()
429 .find(|r| r.reference == Some(row_num))
430 }
431
432 fn cell(&self, reference: &str) -> Option<&Cell> {
433 let col = parse_column(reference)?;
434 let row_num = parse_row(reference)?;
435 let row = self.row(row_num)?;
436 row.cells.iter().find(|c| {
437 c.reference
438 .as_ref()
439 .and_then(|r| parse_column(r))
440 .map(|c_col| c_col == col)
441 .unwrap_or(false)
442 })
443 }
444
445 fn rows(&self) -> impl Iterator<Item = &Row> {
446 self.sheet_data.row.iter()
447 }
448
449 #[cfg(feature = "sml-filtering")]
450 fn has_auto_filter(&self) -> bool {
451 self.auto_filter.is_some()
452 }
453
454 fn has_merged_cells(&self) -> bool {
455 self.merged_cells.is_some()
456 }
457
458 #[cfg(feature = "sml-styling")]
459 fn has_conditional_formatting(&self) -> bool {
460 !self.conditional_formatting.is_empty()
461 }
462
463 #[cfg(feature = "sml-validation")]
464 fn has_data_validations(&self) -> bool {
465 self.data_validations.is_some()
466 }
467
468 #[cfg(feature = "sml-structure")]
469 fn has_freeze_panes(&self) -> bool {
470 self.sheet_views
472 .as_ref()
473 .is_some_and(|views| views.sheet_view.iter().any(|sv| sv.pane.is_some()))
474 }
475
476 #[cfg(feature = "sml-styling")]
477 fn get_row_height(&self, row_num: u32) -> Option<f64> {
478 self.sheet_data
479 .row
480 .iter()
481 .find(|r| r.reference == Some(row_num))
482 .and_then(|r| r.height)
483 }
484
485 #[cfg(feature = "sml-styling")]
486 fn get_column_width(&self, col_idx: u32) -> Option<f64> {
487 self.cols
488 .iter()
489 .flat_map(|cols| &cols.col)
490 .find(|c| c.start_column <= col_idx && col_idx <= c.end_column)
491 .and_then(|c| c.width)
492 }
493}
494
495pub trait SheetDataExt {
497 fn row(&self, row_num: u32) -> Option<&Row>;
499
500 fn rows(&self) -> impl Iterator<Item = &Row>;
502}
503
504impl SheetDataExt for SheetData {
505 fn row(&self, row_num: u32) -> Option<&Row> {
506 self.row.iter().find(|r| r.reference == Some(row_num))
507 }
508
509 fn rows(&self) -> impl Iterator<Item = &Row> {
510 self.row.iter()
511 }
512}
513
514#[derive(Debug, Clone)]
542pub struct ResolvedSheet {
543 name: String,
545 worksheet: Worksheet,
547 context: ResolveContext,
549 comments: Vec<Comment>,
551 charts: Vec<Chart>,
553 #[cfg(feature = "sml-pivot")]
555 pivot_tables: Vec<crate::types::CTPivotTableDefinition>,
556}
557
558#[derive(Debug, Clone)]
560pub struct Comment {
561 pub reference: String,
563 pub author: Option<String>,
565 pub text: String,
567}
568
569#[derive(Debug, Clone)]
571pub struct Chart {
572 pub title: Option<String>,
574 pub chart_type: ChartType,
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum ChartType {
581 Bar,
583 Column,
585 Line,
587 Pie,
589 Area,
591 Scatter,
593 Doughnut,
595 Radar,
597 Surface,
599 Bubble,
601 Stock,
603 Unknown,
605}
606
607impl ResolvedSheet {
608 pub fn new(name: String, worksheet: Worksheet, shared_strings: Vec<String>) -> Self {
610 Self {
611 name,
612 worksheet,
613 context: ResolveContext::new(shared_strings),
614 comments: Vec::new(),
615 charts: Vec::new(),
616 #[cfg(feature = "sml-pivot")]
617 pivot_tables: Vec::new(),
618 }
619 }
620
621 pub fn with_extras(
623 name: String,
624 worksheet: Worksheet,
625 shared_strings: Vec<String>,
626 comments: Vec<Comment>,
627 charts: Vec<Chart>,
628 #[cfg(feature = "sml-pivot")] pivot_tables: Vec<crate::types::CTPivotTableDefinition>,
629 ) -> Self {
630 Self {
631 name,
632 worksheet,
633 context: ResolveContext::new(shared_strings),
634 comments,
635 charts,
636 #[cfg(feature = "sml-pivot")]
637 pivot_tables,
638 }
639 }
640
641 pub fn name(&self) -> &str {
643 &self.name
644 }
645
646 pub fn worksheet(&self) -> &Worksheet {
648 &self.worksheet
649 }
650
651 pub fn context(&self) -> &ResolveContext {
653 &self.context
654 }
655
656 pub fn row_count(&self) -> usize {
662 self.worksheet.row_count()
663 }
664
665 pub fn is_empty(&self) -> bool {
667 self.worksheet.is_empty()
668 }
669
670 pub fn row(&self, row_num: u32) -> Option<&Row> {
672 self.worksheet.row(row_num)
673 }
674
675 pub fn rows(&self) -> impl Iterator<Item = &Row> {
677 self.worksheet.rows()
678 }
679
680 pub fn cell(&self, reference: &str) -> Option<&Cell> {
682 self.worksheet.cell(reference)
683 }
684
685 pub fn cell_value(&self, cell: &Cell) -> CellValue {
691 cell.resolved_value(&self.context)
692 }
693
694 pub fn cell_value_string(&self, cell: &Cell) -> String {
696 cell.value_as_string(&self.context)
697 }
698
699 pub fn cell_value_number(&self, cell: &Cell) -> Option<f64> {
701 cell.value_as_number(&self.context)
702 }
703
704 pub fn cell_value_bool(&self, cell: &Cell) -> Option<bool> {
706 cell.value_as_bool(&self.context)
707 }
708
709 pub fn value_at(&self, reference: &str) -> Option<String> {
711 self.cell(reference).map(|c| self.cell_value_string(c))
712 }
713
714 pub fn number_at(&self, reference: &str) -> Option<f64> {
716 self.cell(reference).and_then(|c| self.cell_value_number(c))
717 }
718
719 #[cfg(feature = "sml-filtering")]
725 pub fn has_auto_filter(&self) -> bool {
726 self.worksheet.has_auto_filter()
727 }
728
729 pub fn has_merged_cells(&self) -> bool {
731 self.worksheet.has_merged_cells()
732 }
733
734 #[cfg(feature = "sml-styling")]
736 pub fn has_conditional_formatting(&self) -> bool {
737 self.worksheet.has_conditional_formatting()
738 }
739
740 #[cfg(feature = "sml-validation")]
742 pub fn has_data_validations(&self) -> bool {
743 self.worksheet.has_data_validations()
744 }
745
746 #[cfg(feature = "sml-structure")]
748 pub fn has_freeze_panes(&self) -> bool {
749 self.worksheet.has_freeze_panes()
750 }
751
752 pub fn comments(&self) -> &[Comment] {
758 &self.comments
759 }
760
761 pub fn comment(&self, reference: &str) -> Option<&Comment> {
763 self.comments.iter().find(|c| c.reference == reference)
764 }
765
766 pub fn has_comment(&self, reference: &str) -> bool {
768 self.comment(reference).is_some()
769 }
770
771 pub fn charts(&self) -> &[Chart] {
777 &self.charts
778 }
779
780 #[cfg(feature = "sml-pivot")]
791 pub fn pivot_tables(&self) -> &[crate::types::CTPivotTableDefinition] {
792 &self.pivot_tables
793 }
794
795 pub fn dimensions(&self) -> Option<(u32, u32, u32, u32)> {
803 if self.worksheet.sheet_data.row.is_empty() {
804 return None;
805 }
806
807 let mut min_row = u32::MAX;
808 let mut max_row = 0u32;
809 let mut min_col = u32::MAX;
810 let mut max_col = 0u32;
811
812 for row in &self.worksheet.sheet_data.row {
813 if let Some(row_num) = row.reference {
814 min_row = min_row.min(row_num);
815 max_row = max_row.max(row_num);
816 }
817 for cell in &row.cells {
818 if let Some(col) = cell.column_number() {
819 min_col = min_col.min(col);
820 max_col = max_col.max(col);
821 }
822 }
823 }
824
825 if min_row == u32::MAX {
826 None
827 } else {
828 Some((min_row, min_col, max_row, max_col))
829 }
830 }
831
832 pub fn merged_cells(&self) -> Option<&crate::types::MergedCells> {
834 self.worksheet.merged_cells.as_deref()
835 }
836
837 #[cfg(feature = "sml-styling")]
839 pub fn conditional_formatting(&self) -> &[crate::types::ConditionalFormatting] {
840 &self.worksheet.conditional_formatting
841 }
842
843 #[cfg(feature = "sml-validation")]
845 pub fn data_validations(&self) -> Option<&crate::types::DataValidations> {
846 self.worksheet.data_validations.as_deref()
847 }
848
849 #[cfg(feature = "sml-filtering")]
851 pub fn auto_filter(&self) -> Option<&crate::types::AutoFilter> {
852 self.worksheet.auto_filter.as_deref()
853 }
854
855 pub fn sheet_views(&self) -> Option<&crate::types::SheetViews> {
857 self.worksheet.sheet_views.as_deref()
858 }
859
860 #[cfg(feature = "sml-structure")]
862 pub fn freeze_pane(&self) -> Option<&crate::types::Pane> {
863 self.worksheet
864 .sheet_views
865 .as_ref()
866 .and_then(|views| views.sheet_view.first())
867 .and_then(|view| view.pane.as_deref())
868 }
869
870 #[cfg(feature = "sml-styling")]
872 pub fn columns(&self) -> &[crate::types::Columns] {
873 &self.worksheet.cols
874 }
875}
876
877fn parse_column(reference: &str) -> Option<u32> {
883 let mut col: u32 = 0;
884 for ch in reference.chars() {
885 if ch.is_ascii_alphabetic() {
886 col = col * 26 + (ch.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
887 } else {
888 break;
889 }
890 }
891 if col > 0 { Some(col) } else { None }
892}
893
894fn parse_row(reference: &str) -> Option<u32> {
896 let digits: String = reference.chars().filter(|c| c.is_ascii_digit()).collect();
897 digits.parse().ok()
898}
899
900#[cfg(feature = "sml-pivot")]
911pub trait PivotTableExt {
912 fn name(&self) -> &str;
914
915 fn location_reference(&self) -> &str;
919
920 fn data_field_names(&self) -> Vec<&str>;
925
926 fn row_field_indices(&self) -> Vec<i32>;
931
932 fn col_field_indices(&self) -> Vec<i32>;
937}
938
939#[cfg(feature = "sml-pivot")]
940impl PivotTableExt for crate::types::CTPivotTableDefinition {
941 fn name(&self) -> &str {
942 &self.name
943 }
944
945 fn location_reference(&self) -> &str {
946 &self.location.reference
947 }
948
949 fn data_field_names(&self) -> Vec<&str> {
950 self.data_fields
951 .as_ref()
952 .map(|df| {
953 df.data_field
954 .iter()
955 .map(|f| f.name.as_deref().unwrap_or(""))
956 .collect()
957 })
958 .unwrap_or_default()
959 }
960
961 fn row_field_indices(&self) -> Vec<i32> {
962 self.row_fields
963 .as_ref()
964 .map(|rf| rf.field.iter().map(|f| f.x).collect())
965 .unwrap_or_default()
966 }
967
968 fn col_field_indices(&self) -> Vec<i32> {
969 self.col_fields
970 .as_ref()
971 .map(|cf| cf.field.iter().map(|f| f.x).collect())
972 .unwrap_or_default()
973 }
974}
975
976#[cfg(feature = "sml-styling")]
987pub trait ConditionalFormattingExt {
988 fn cell_range(&self) -> Option<&str>;
992
993 fn rules(&self) -> &[crate::types::ConditionalRule];
995
996 fn rule_count(&self) -> usize;
998}
999
1000#[cfg(feature = "sml-styling")]
1001impl ConditionalFormattingExt for crate::types::ConditionalFormatting {
1002 fn cell_range(&self) -> Option<&str> {
1003 self.square_reference.as_deref()
1004 }
1005
1006 fn rules(&self) -> &[crate::types::ConditionalRule] {
1007 &self.cf_rule
1008 }
1009
1010 fn rule_count(&self) -> usize {
1011 self.cf_rule.len()
1012 }
1013}
1014
1015#[cfg(feature = "sml-styling")]
1022pub trait ConditionalRuleExt {
1023 fn rule_type(&self) -> Option<&crate::types::ConditionalType>;
1025
1026 fn priority(&self) -> i32;
1028
1029 fn has_color_scale(&self) -> bool;
1031
1032 fn has_data_bar(&self) -> bool;
1034
1035 fn has_icon_set(&self) -> bool;
1037
1038 fn formulas(&self) -> &[crate::types::STFormula];
1044}
1045
1046#[cfg(feature = "sml-styling")]
1047impl ConditionalRuleExt for crate::types::ConditionalRule {
1048 fn rule_type(&self) -> Option<&crate::types::ConditionalType> {
1049 self.r#type.as_ref()
1050 }
1051
1052 fn priority(&self) -> i32 {
1053 self.priority
1054 }
1055
1056 fn has_color_scale(&self) -> bool {
1057 self.color_scale.is_some()
1058 }
1059
1060 fn has_data_bar(&self) -> bool {
1061 self.data_bar.is_some()
1062 }
1063
1064 fn has_icon_set(&self) -> bool {
1065 self.icon_set.is_some()
1066 }
1067
1068 fn formulas(&self) -> &[crate::types::STFormula] {
1069 &self.formula
1070 }
1071}
1072
1073#[cfg(feature = "sml-styling")]
1083pub trait FontExt {
1084 fn is_bold(&self) -> bool;
1086 fn is_italic(&self) -> bool;
1088 fn is_strikethrough(&self) -> bool;
1090 fn is_outline(&self) -> bool;
1092 fn is_shadow(&self) -> bool;
1094 fn is_condense(&self) -> bool;
1096 fn is_extend(&self) -> bool;
1098 fn font_name(&self) -> Option<&str>;
1100 fn font_size(&self) -> Option<f64>;
1102 fn font_color(&self) -> Option<&crate::types::Color>;
1104 fn vertical_align(&self) -> Option<crate::types::VerticalAlignRun>;
1106 fn font_scheme(&self) -> Option<crate::types::FontScheme>;
1108}
1109
1110#[cfg(feature = "sml-styling")]
1111impl FontExt for crate::types::Font {
1112 fn is_bold(&self) -> bool {
1113 self.b.as_ref().is_some_and(|v| v.value.unwrap_or(false))
1114 }
1115
1116 fn is_italic(&self) -> bool {
1117 self.i.as_ref().is_some_and(|v| v.value.unwrap_or(false))
1118 }
1119
1120 fn is_strikethrough(&self) -> bool {
1121 self.strike
1122 .as_ref()
1123 .is_some_and(|v| v.value.unwrap_or(false))
1124 }
1125
1126 fn is_outline(&self) -> bool {
1127 self.outline
1128 .as_ref()
1129 .is_some_and(|v| v.value.unwrap_or(false))
1130 }
1131
1132 fn is_shadow(&self) -> bool {
1133 self.shadow
1134 .as_ref()
1135 .is_some_and(|v| v.value.unwrap_or(false))
1136 }
1137
1138 fn is_condense(&self) -> bool {
1139 self.condense
1140 .as_ref()
1141 .is_some_and(|v| v.value.unwrap_or(false))
1142 }
1143
1144 fn is_extend(&self) -> bool {
1145 self.extend
1146 .as_ref()
1147 .is_some_and(|v| v.value.unwrap_or(false))
1148 }
1149
1150 fn font_name(&self) -> Option<&str> {
1151 self.name.as_deref().map(|n| n.value.as_str())
1152 }
1153
1154 fn font_size(&self) -> Option<f64> {
1155 self.sz.as_deref().map(|s| s.value)
1156 }
1157
1158 fn font_color(&self) -> Option<&crate::types::Color> {
1159 self.color.as_deref()
1160 }
1161
1162 fn vertical_align(&self) -> Option<crate::types::VerticalAlignRun> {
1163 self.vert_align.as_deref().map(|v| v.value)
1164 }
1165
1166 fn font_scheme(&self) -> Option<crate::types::FontScheme> {
1167 self.scheme.as_deref().map(|s| s.value)
1168 }
1169}
1170
1171#[cfg(feature = "sml-styling")]
1176pub trait FillExt {
1177 fn pattern_fill(&self) -> Option<&crate::types::PatternFill>;
1179 fn gradient_fill(&self) -> Option<&crate::types::GradientFill>;
1181 fn has_fill(&self) -> bool;
1183}
1184
1185#[cfg(feature = "sml-styling")]
1186impl FillExt for crate::types::Fill {
1187 fn pattern_fill(&self) -> Option<&crate::types::PatternFill> {
1188 self.pattern_fill.as_deref()
1189 }
1190
1191 fn gradient_fill(&self) -> Option<&crate::types::GradientFill> {
1192 self.gradient_fill.as_deref()
1193 }
1194
1195 fn has_fill(&self) -> bool {
1196 self.pattern_fill.is_some() || self.gradient_fill.is_some()
1197 }
1198}
1199
1200pub trait PatternFillExt {
1204 fn pattern_type(&self) -> Option<crate::types::PatternType>;
1206 fn foreground_color(&self) -> Option<&crate::types::Color>;
1208 fn background_color(&self) -> Option<&crate::types::Color>;
1210}
1211
1212impl PatternFillExt for crate::types::PatternFill {
1213 fn pattern_type(&self) -> Option<crate::types::PatternType> {
1214 self.pattern_type
1215 }
1216
1217 fn foreground_color(&self) -> Option<&crate::types::Color> {
1218 self.fg_color.as_deref()
1219 }
1220
1221 fn background_color(&self) -> Option<&crate::types::Color> {
1222 self.bg_color.as_deref()
1223 }
1224}
1225
1226#[cfg(feature = "sml-styling")]
1231pub trait BorderExt {
1232 fn left_border(&self) -> Option<&crate::types::BorderProperties>;
1234 fn right_border(&self) -> Option<&crate::types::BorderProperties>;
1236 fn top_border(&self) -> Option<&crate::types::BorderProperties>;
1238 fn bottom_border(&self) -> Option<&crate::types::BorderProperties>;
1240 fn diagonal_border(&self) -> Option<&crate::types::BorderProperties>;
1242 fn is_diagonal_up(&self) -> bool;
1244 fn is_diagonal_down(&self) -> bool;
1246 fn is_outline_applied(&self) -> bool;
1248}
1249
1250#[cfg(feature = "sml-styling")]
1251impl BorderExt for crate::types::Border {
1252 fn left_border(&self) -> Option<&crate::types::BorderProperties> {
1253 self.left.as_deref()
1254 }
1255
1256 fn right_border(&self) -> Option<&crate::types::BorderProperties> {
1257 self.right.as_deref()
1258 }
1259
1260 fn top_border(&self) -> Option<&crate::types::BorderProperties> {
1261 self.top.as_deref()
1262 }
1263
1264 fn bottom_border(&self) -> Option<&crate::types::BorderProperties> {
1265 self.bottom.as_deref()
1266 }
1267
1268 fn diagonal_border(&self) -> Option<&crate::types::BorderProperties> {
1269 self.diagonal.as_deref()
1270 }
1271
1272 fn is_diagonal_up(&self) -> bool {
1273 self.diagonal_up.unwrap_or(false)
1274 }
1275
1276 fn is_diagonal_down(&self) -> bool {
1277 self.diagonal_down.unwrap_or(false)
1278 }
1279
1280 fn is_outline_applied(&self) -> bool {
1281 self.outline.unwrap_or(false)
1282 }
1283}
1284
1285pub trait BorderPropertiesExt {
1289 fn border_style(&self) -> Option<crate::types::BorderStyle>;
1291 fn border_color(&self) -> Option<&crate::types::Color>;
1293 fn has_style(&self) -> bool;
1295}
1296
1297impl BorderPropertiesExt for crate::types::BorderProperties {
1298 fn border_style(&self) -> Option<crate::types::BorderStyle> {
1299 self.style
1300 }
1301
1302 fn border_color(&self) -> Option<&crate::types::Color> {
1303 self.color.as_deref()
1304 }
1305
1306 fn has_style(&self) -> bool {
1307 self.style.is_some()
1308 }
1309}
1310
1311#[cfg(feature = "sml-styling")]
1316pub trait CellAlignmentExt {
1317 fn horizontal_alignment(&self) -> Option<crate::types::HorizontalAlignment>;
1319 fn vertical_alignment(&self) -> Option<crate::types::VerticalAlignment>;
1321 fn text_rotation(&self) -> Option<u32>;
1323 fn is_wrap_text(&self) -> bool;
1325 fn is_shrink_to_fit(&self) -> bool;
1327 fn indent(&self) -> Option<u32>;
1329}
1330
1331#[cfg(feature = "sml-styling")]
1332impl CellAlignmentExt for crate::types::CellAlignment {
1333 fn horizontal_alignment(&self) -> Option<crate::types::HorizontalAlignment> {
1334 self.horizontal
1335 }
1336
1337 fn vertical_alignment(&self) -> Option<crate::types::VerticalAlignment> {
1338 self.vertical
1339 }
1340
1341 fn text_rotation(&self) -> Option<u32> {
1342 self.text_rotation.as_deref().and_then(|r| r.parse().ok())
1343 }
1344
1345 fn is_wrap_text(&self) -> bool {
1346 self.wrap_text.unwrap_or(false)
1347 }
1348
1349 fn is_shrink_to_fit(&self) -> bool {
1350 self.shrink_to_fit.unwrap_or(false)
1351 }
1352
1353 fn indent(&self) -> Option<u32> {
1354 self.indent
1355 }
1356}
1357
1358#[cfg(feature = "sml-protection")]
1363pub trait CellProtectionExt {
1364 fn is_locked(&self) -> bool;
1366 fn is_formula_hidden(&self) -> bool;
1368}
1369
1370#[cfg(feature = "sml-protection")]
1371impl CellProtectionExt for crate::types::CellProtection {
1372 fn is_locked(&self) -> bool {
1373 self.locked.unwrap_or(true)
1376 }
1377
1378 fn is_formula_hidden(&self) -> bool {
1379 self.hidden.unwrap_or(false)
1380 }
1381}
1382
1383pub trait FormatExt {
1390 #[cfg(feature = "sml-styling")]
1392 fn number_format_id(&self) -> u32;
1393 #[cfg(feature = "sml-styling")]
1395 fn font_id(&self) -> u32;
1396 #[cfg(feature = "sml-styling")]
1398 fn fill_id(&self) -> u32;
1399 #[cfg(feature = "sml-styling")]
1401 fn border_id(&self) -> u32;
1402 #[cfg(feature = "sml-styling")]
1404 fn apply_alignment(&self) -> bool;
1405 #[cfg(feature = "sml-styling")]
1407 fn apply_font(&self) -> bool;
1408 #[cfg(feature = "sml-styling")]
1410 fn apply_fill(&self) -> bool;
1411 #[cfg(feature = "sml-styling")]
1413 fn apply_border(&self) -> bool;
1414 #[cfg(feature = "sml-styling")]
1416 fn alignment(&self) -> Option<&crate::types::CellAlignment>;
1417 #[cfg(feature = "sml-protection")]
1419 fn protection(&self) -> Option<&crate::types::CellProtection>;
1420}
1421
1422impl FormatExt for crate::types::Format {
1423 #[cfg(feature = "sml-styling")]
1424 fn number_format_id(&self) -> u32 {
1425 self.number_format_id.unwrap_or(0)
1426 }
1427
1428 #[cfg(feature = "sml-styling")]
1429 fn font_id(&self) -> u32 {
1430 self.font_id.unwrap_or(0)
1431 }
1432
1433 #[cfg(feature = "sml-styling")]
1434 fn fill_id(&self) -> u32 {
1435 self.fill_id.unwrap_or(0)
1436 }
1437
1438 #[cfg(feature = "sml-styling")]
1439 fn border_id(&self) -> u32 {
1440 self.border_id.unwrap_or(0)
1441 }
1442
1443 #[cfg(feature = "sml-styling")]
1444 fn apply_alignment(&self) -> bool {
1445 self.apply_alignment.unwrap_or(false)
1446 }
1447
1448 #[cfg(feature = "sml-styling")]
1449 fn apply_font(&self) -> bool {
1450 self.apply_font.unwrap_or(false)
1451 }
1452
1453 #[cfg(feature = "sml-styling")]
1454 fn apply_fill(&self) -> bool {
1455 self.apply_fill.unwrap_or(false)
1456 }
1457
1458 #[cfg(feature = "sml-styling")]
1459 fn apply_border(&self) -> bool {
1460 self.apply_border.unwrap_or(false)
1461 }
1462
1463 #[cfg(feature = "sml-styling")]
1464 fn alignment(&self) -> Option<&crate::types::CellAlignment> {
1465 self.alignment.as_deref()
1466 }
1467
1468 #[cfg(feature = "sml-protection")]
1469 fn protection(&self) -> Option<&crate::types::CellProtection> {
1470 self.protection.as_deref()
1471 }
1472}
1473
1474#[cfg(feature = "sml-styling")]
1478pub trait WorksheetConditionalFormattingExt {
1479 fn conditional_formattings(&self) -> &[crate::types::ConditionalFormatting];
1481}
1482
1483#[cfg(feature = "sml-styling")]
1484impl WorksheetConditionalFormattingExt for crate::types::Worksheet {
1485 fn conditional_formattings(&self) -> &[crate::types::ConditionalFormatting] {
1486 &self.conditional_formatting
1487 }
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492 use super::*;
1493
1494 #[test]
1495 fn test_parse_column() {
1496 assert_eq!(parse_column("A1"), Some(1));
1497 assert_eq!(parse_column("B5"), Some(2));
1498 assert_eq!(parse_column("Z1"), Some(26));
1499 assert_eq!(parse_column("AA1"), Some(27));
1500 assert_eq!(parse_column("AB1"), Some(28));
1501 assert_eq!(parse_column("AZ1"), Some(52));
1502 assert_eq!(parse_column("BA1"), Some(53));
1503 }
1504
1505 #[test]
1506 fn test_parse_row() {
1507 assert_eq!(parse_row("A1"), Some(1));
1508 assert_eq!(parse_row("B5"), Some(5));
1509 assert_eq!(parse_row("AA100"), Some(100));
1510 assert_eq!(parse_row("ZZ9999"), Some(9999));
1511 }
1512
1513 #[test]
1514 #[cfg(feature = "full")]
1515 fn test_cell_ext() {
1516 let cell = Cell {
1517 reference: Some("B5".to_string()),
1518 cell_type: Some(CellType::Number),
1519 value: Some("42.5".to_string()),
1520 formula: None,
1521 style_index: None,
1522 cm: None,
1523 vm: None,
1524 placeholder: None,
1525 is: None,
1526 extension_list: None,
1527 #[cfg(feature = "extra-attrs")]
1528 extra_attrs: Default::default(),
1529 #[cfg(feature = "extra-children")]
1530 extra_children: Default::default(),
1531 };
1532
1533 assert_eq!(cell.column_number(), Some(2));
1534 assert_eq!(cell.row_number(), Some(5));
1535 assert!(!cell.has_formula());
1536 assert!(cell.is_number());
1537 assert!(!cell.is_shared_string());
1538 }
1539
1540 #[test]
1541 #[cfg(feature = "full")]
1542 fn test_cell_resolve_number() {
1543 let cell = Cell {
1544 reference: Some("A1".to_string()),
1545 cell_type: Some(CellType::Number),
1546 value: Some("123.45".to_string()),
1547 formula: None,
1548 style_index: None,
1549 cm: None,
1550 vm: None,
1551 placeholder: None,
1552 is: None,
1553 extension_list: None,
1554 #[cfg(feature = "extra-attrs")]
1555 extra_attrs: Default::default(),
1556 #[cfg(feature = "extra-children")]
1557 extra_children: Default::default(),
1558 };
1559
1560 let ctx = ResolveContext::default();
1561 assert_eq!(cell.resolved_value(&ctx), CellValue::Number(123.45));
1562 assert_eq!(cell.value_as_string(&ctx), "123.45");
1563 assert_eq!(cell.value_as_number(&ctx), Some(123.45));
1564 }
1565
1566 #[test]
1567 #[cfg(feature = "full")]
1568 fn test_cell_resolve_shared_string() {
1569 let cell = Cell {
1570 reference: Some("A1".to_string()),
1571 cell_type: Some(CellType::SharedString),
1572 value: Some("0".to_string()), formula: None,
1574 style_index: None,
1575 cm: None,
1576 vm: None,
1577 placeholder: None,
1578 is: None,
1579 extension_list: None,
1580 #[cfg(feature = "extra-attrs")]
1581 extra_attrs: Default::default(),
1582 #[cfg(feature = "extra-children")]
1583 extra_children: Default::default(),
1584 };
1585
1586 let ctx = ResolveContext::new(vec!["Hello".to_string(), "World".to_string()]);
1587 assert_eq!(
1588 cell.resolved_value(&ctx),
1589 CellValue::String("Hello".to_string())
1590 );
1591 assert_eq!(cell.value_as_string(&ctx), "Hello");
1592 }
1593
1594 #[test]
1595 #[cfg(feature = "full")]
1596 fn test_cell_resolve_boolean() {
1597 let cell = Cell {
1598 reference: Some("A1".to_string()),
1599 cell_type: Some(CellType::Boolean),
1600 value: Some("1".to_string()),
1601 formula: None,
1602 style_index: None,
1603 cm: None,
1604 vm: None,
1605 placeholder: None,
1606 is: None,
1607 extension_list: None,
1608 #[cfg(feature = "extra-attrs")]
1609 extra_attrs: Default::default(),
1610 #[cfg(feature = "extra-children")]
1611 extra_children: Default::default(),
1612 };
1613
1614 let ctx = ResolveContext::default();
1615 assert_eq!(cell.resolved_value(&ctx), CellValue::Boolean(true));
1616 assert_eq!(cell.value_as_string(&ctx), "TRUE");
1617 assert_eq!(cell.value_as_bool(&ctx), Some(true));
1618 }
1619
1620 #[test]
1621 fn test_parse_worksheet() {
1622 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1623 <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1624 <sheetData>
1625 <row r="1" spans="1:3">
1626 <c r="A1" t="s"><v>0</v></c>
1627 <c r="B1"><v>42.5</v></c>
1628 <c r="C1" t="b"><v>1</v></c>
1629 </row>
1630 <row r="2">
1631 <c r="A2"><v>100</v></c>
1632 </row>
1633 </sheetData>
1634 </worksheet>"#;
1635
1636 let worksheet = parse_worksheet(xml).expect("parse failed");
1637
1638 assert_eq!(worksheet.row_count(), 2);
1639 assert!(!worksheet.is_empty());
1640
1641 let row1 = worksheet.row(1).expect("row 1 should exist");
1643 assert_eq!(row1.cells.len(), 3);
1644
1645 let row2 = worksheet.row(2).expect("row 2 should exist");
1646 assert_eq!(row2.cells.len(), 1);
1647
1648 let cell_a1 = worksheet.cell("A1").expect("A1 should exist");
1650 assert_eq!(cell_a1.value.as_deref(), Some("0"));
1651 assert!(cell_a1.is_shared_string());
1652
1653 let cell_b1 = worksheet.cell("B1").expect("B1 should exist");
1654 assert_eq!(cell_b1.value.as_deref(), Some("42.5"));
1655 assert!(cell_b1.is_number());
1656
1657 assert!(worksheet.cell("Z99").is_none());
1659 }
1660
1661 #[test]
1662 fn test_worksheet_ext_with_resolve() {
1663 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1664 <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1665 <sheetData>
1666 <row r="1">
1667 <c r="A1" t="s"><v>0</v></c>
1668 <c r="B1" t="s"><v>1</v></c>
1669 </row>
1670 </sheetData>
1671 </worksheet>"#;
1672
1673 let worksheet = parse_worksheet(xml).expect("parse failed");
1674 let ctx = ResolveContext::new(vec!["Hello".to_string(), "World".to_string()]);
1675
1676 let cell_a1 = worksheet.cell("A1").expect("A1 should exist");
1677 assert_eq!(cell_a1.value_as_string(&ctx), "Hello");
1678
1679 let cell_b1 = worksheet.cell("B1").expect("B1 should exist");
1680 assert_eq!(cell_b1.value_as_string(&ctx), "World");
1681 }
1682
1683 #[test]
1684 fn test_resolved_sheet() {
1685 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1686 <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1687 <sheetData>
1688 <row r="1">
1689 <c r="A1" t="s"><v>0</v></c>
1690 <c r="B1"><v>42.5</v></c>
1691 <c r="C1" t="b"><v>1</v></c>
1692 </row>
1693 <row r="2">
1694 <c r="A2" t="s"><v>1</v></c>
1695 <c r="B2"><v>100</v></c>
1696 </row>
1697 </sheetData>
1698 </worksheet>"#;
1699
1700 let worksheet = parse_worksheet(xml).expect("parse failed");
1701 let shared_strings = vec!["Hello".to_string(), "World".to_string()];
1702 let sheet = ResolvedSheet::new("Sheet1".to_string(), worksheet, shared_strings);
1703
1704 assert_eq!(sheet.name(), "Sheet1");
1706 assert_eq!(sheet.row_count(), 2);
1707 assert!(!sheet.is_empty());
1708
1709 let cell_a1 = sheet.cell("A1").expect("A1");
1711 assert_eq!(sheet.cell_value_string(cell_a1), "Hello");
1712
1713 let cell_b1 = sheet.cell("B1").expect("B1");
1714 assert_eq!(sheet.cell_value_number(cell_b1), Some(42.5));
1715
1716 let cell_c1 = sheet.cell("C1").expect("C1");
1717 assert_eq!(sheet.cell_value_bool(cell_c1), Some(true));
1718
1719 assert_eq!(sheet.value_at("A1"), Some("Hello".to_string()));
1721 assert_eq!(sheet.value_at("A2"), Some("World".to_string()));
1722 assert_eq!(sheet.number_at("B1"), Some(42.5));
1723 assert_eq!(sheet.number_at("B2"), Some(100.0));
1724
1725 assert!(sheet.value_at("Z99").is_none());
1727 }
1728
1729 #[test]
1730 #[cfg(feature = "sml-styling")]
1731 fn test_conditional_formatting_cell_range() {
1732 use crate::workbook::bootstrap;
1733 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1734 <conditionalFormatting sqref="A1:B5" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1735 </conditionalFormatting>"#;
1736 let cf: crate::types::ConditionalFormatting = bootstrap(xml).expect("parse failed");
1737 assert_eq!(cf.cell_range(), Some("A1:B5"));
1738 assert_eq!(cf.rule_count(), 0);
1739 }
1740
1741 #[test]
1742 #[cfg(feature = "sml-styling")]
1743 fn test_conditional_rule_type() {
1744 use crate::workbook::bootstrap;
1745 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1746 <cfRule type="colorScale" priority="1" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1747 <colorScale>
1748 <cfvo type="min"/>
1749 <cfvo type="max"/>
1750 <color rgb="FF0000"/>
1751 <color rgb="00FF00"/>
1752 </colorScale>
1753 </cfRule>"#;
1754 let rule: crate::types::ConditionalRule = bootstrap(xml).expect("parse failed");
1755 assert_eq!(
1756 rule.rule_type(),
1757 Some(&crate::types::ConditionalType::ColorScale)
1758 );
1759 assert_eq!(rule.priority(), 1);
1760 }
1761
1762 #[test]
1763 #[cfg(feature = "sml-styling")]
1764 fn test_conditional_rule_has_color_scale() {
1765 use crate::workbook::bootstrap;
1766 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1767 <cfRule type="colorScale" priority="1" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1768 <colorScale>
1769 <cfvo type="min"/>
1770 <cfvo type="max"/>
1771 <color rgb="FF0000"/>
1772 <color rgb="00FF00"/>
1773 </colorScale>
1774 </cfRule>"#;
1775 let rule: crate::types::ConditionalRule = bootstrap(xml).expect("parse failed");
1776 assert!(rule.has_color_scale());
1777 assert!(!rule.has_data_bar());
1778 assert!(!rule.has_icon_set());
1779 }
1780
1781 #[test]
1782 #[cfg(feature = "sml-pivot")]
1783 fn test_pivot_table_name() {
1784 use crate::workbook::bootstrap;
1785 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1786 <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
1787 name="PivotTable1" cacheId="1" dataCaption="Values">
1788 <location ref="A1:D10" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>
1789 </pivotTableDefinition>"#;
1790 let pt: crate::types::CTPivotTableDefinition = bootstrap(xml).expect("parse failed");
1791 assert_eq!(pt.name(), "PivotTable1");
1792 }
1793
1794 #[test]
1795 #[cfg(feature = "sml-pivot")]
1796 fn test_pivot_table_location() {
1797 use crate::workbook::bootstrap;
1798 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1799 <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
1800 name="PivotTable1" cacheId="1" dataCaption="Values">
1801 <location ref="A1:D10" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>
1802 </pivotTableDefinition>"#;
1803 let pt: crate::types::CTPivotTableDefinition = bootstrap(xml).expect("parse failed");
1804 assert_eq!(pt.location_reference(), "A1:D10");
1805 }
1806
1807 #[cfg(feature = "sml-styling")]
1812 #[test]
1813 fn test_font_ext_bold_italic() {
1814 use crate::workbook::bootstrap;
1815 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1816 <font xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1817 <b val="1"/>
1818 <i val="0"/>
1819 <name val="Calibri"/>
1820 <sz val="11"/>
1821 </font>"#;
1822 let font: crate::types::Font = bootstrap(xml).expect("parse failed");
1823 assert!(font.is_bold());
1824 assert!(!font.is_italic());
1825 assert_eq!(font.font_name(), Some("Calibri"));
1826 assert_eq!(font.font_size(), Some(11.0));
1827 }
1828
1829 #[cfg(feature = "sml-styling")]
1830 #[test]
1831 fn test_font_ext_defaults() {
1832 use crate::workbook::bootstrap;
1833 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1834 <font xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1835 "#;
1836 let font: crate::types::Font = bootstrap(xml).expect("parse failed");
1837 assert!(!font.is_bold());
1838 assert!(!font.is_italic());
1839 assert!(!font.is_strikethrough());
1840 assert!(font.font_name().is_none());
1841 assert!(font.font_size().is_none());
1842 assert!(font.font_color().is_none());
1843 assert!(font.vertical_align().is_none());
1844 assert!(font.font_scheme().is_none());
1845 }
1846
1847 #[cfg(feature = "sml-styling")]
1852 #[test]
1853 fn test_fill_ext_pattern() {
1854 use crate::workbook::bootstrap;
1855 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1856 <fill xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1857 <patternFill patternType="solid"/>
1858 </fill>"#;
1859 let fill: crate::types::Fill = bootstrap(xml).expect("parse failed");
1860 assert!(fill.has_fill());
1861 assert!(fill.pattern_fill().is_some());
1862 assert!(fill.gradient_fill().is_none());
1863 }
1864
1865 #[cfg(feature = "sml-styling")]
1866 #[test]
1867 fn test_fill_ext_no_fill() {
1868 use crate::workbook::bootstrap;
1869 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1870 <fill xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1871 <patternFill patternType="none"/>
1872 </fill>"#;
1873 let fill: crate::types::Fill = bootstrap(xml).expect("parse failed");
1874 assert!(fill.has_fill()); let pf = fill.pattern_fill().unwrap();
1876 use crate::ext::PatternFillExt;
1877 use crate::types::PatternType;
1878 assert_eq!(pf.pattern_type(), Some(PatternType::None));
1879 }
1880
1881 #[cfg(feature = "sml-styling")]
1886 #[test]
1887 fn test_border_ext_diagonal_flags() {
1888 use crate::workbook::bootstrap;
1889 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1890 <border diagonalUp="1" diagonalDown="0"
1891 xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1892 </border>"#;
1893 let border: crate::types::Border = bootstrap(xml).expect("parse failed");
1894 assert!(border.is_diagonal_up());
1895 assert!(!border.is_diagonal_down());
1896 assert!(border.left_border().is_none());
1897 assert!(border.diagonal_border().is_none());
1898 }
1899
1900 #[cfg(feature = "sml-styling")]
1905 #[test]
1906 fn test_cell_alignment_ext() {
1907 use crate::workbook::bootstrap;
1908 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1909 <alignment horizontal="center" vertical="bottom" wrapText="1" indent="2"
1910 xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1911 "#;
1912 let align: crate::types::CellAlignment = bootstrap(xml).expect("parse failed");
1913 use crate::types::{HorizontalAlignment, VerticalAlignment};
1914 assert_eq!(
1915 align.horizontal_alignment(),
1916 Some(HorizontalAlignment::Center)
1917 );
1918 assert_eq!(align.vertical_alignment(), Some(VerticalAlignment::Bottom));
1919 assert!(align.is_wrap_text());
1920 assert!(!align.is_shrink_to_fit());
1921 assert_eq!(align.indent(), Some(2));
1922 }
1923
1924 #[cfg(feature = "sml-styling")]
1929 #[test]
1930 fn test_format_ext_ids() {
1931 use crate::workbook::bootstrap;
1932 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1933 <xf numFmtId="4" fontId="1" fillId="2" borderId="3" applyFont="1" applyFill="0"
1934 xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1935 "#;
1936 let fmt: crate::types::Format = bootstrap(xml).expect("parse failed");
1937 use crate::ext::FormatExt;
1938 assert_eq!(fmt.number_format_id(), 4);
1939 assert_eq!(fmt.font_id(), 1);
1940 assert_eq!(fmt.fill_id(), 2);
1941 assert_eq!(fmt.border_id(), 3);
1942 assert!(fmt.apply_font());
1943 assert!(!fmt.apply_fill());
1944 assert!(fmt.alignment().is_none());
1945 }
1946
1947 #[cfg(feature = "sml-styling")]
1952 #[test]
1953 fn test_worksheet_get_row_height() {
1954 let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1955 <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1956 <sheetData>
1957 <row r="1" ht="20" customHeight="1">
1958 <c r="A1"><v>1</v></c>
1959 </row>
1960 <row r="2">
1961 <c r="A2"><v>2</v></c>
1962 </row>
1963 </sheetData>
1964 </worksheet>"#;
1965 let ws = parse_worksheet(xml).expect("parse failed");
1966 assert_eq!(ws.get_row_height(1), Some(20.0));
1967 assert_eq!(ws.get_row_height(2), None); assert_eq!(ws.get_row_height(99), None); }
1970}