Skip to main content

sheetkit_xml/
worksheet.rs

1//! Worksheet XML schema structures.
2//!
3//! Represents `xl/worksheets/sheet*.xml` in the OOXML package.
4
5use std::fmt;
6
7use serde::de::Deserializer;
8use serde::ser::Serializer;
9use serde::{Deserialize, Serialize};
10
11use crate::namespaces;
12
13/// Worksheet root element.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename = "worksheet")]
16pub struct WorksheetXml {
17    #[serde(rename = "@xmlns")]
18    pub xmlns: String,
19
20    #[serde(rename = "@xmlns:r")]
21    pub xmlns_r: String,
22
23    #[serde(rename = "sheetPr", skip_serializing_if = "Option::is_none")]
24    pub sheet_pr: Option<SheetPr>,
25
26    #[serde(rename = "dimension", skip_serializing_if = "Option::is_none")]
27    pub dimension: Option<Dimension>,
28
29    #[serde(rename = "sheetViews", skip_serializing_if = "Option::is_none")]
30    pub sheet_views: Option<SheetViews>,
31
32    #[serde(rename = "sheetFormatPr", skip_serializing_if = "Option::is_none")]
33    pub sheet_format_pr: Option<SheetFormatPr>,
34
35    #[serde(rename = "cols", skip_serializing_if = "Option::is_none")]
36    pub cols: Option<Cols>,
37
38    #[serde(rename = "sheetData")]
39    pub sheet_data: SheetData,
40
41    #[serde(rename = "sheetProtection", skip_serializing_if = "Option::is_none")]
42    pub sheet_protection: Option<SheetProtection>,
43
44    #[serde(rename = "autoFilter", skip_serializing_if = "Option::is_none")]
45    pub auto_filter: Option<AutoFilter>,
46
47    #[serde(rename = "mergeCells", skip_serializing_if = "Option::is_none")]
48    pub merge_cells: Option<MergeCells>,
49
50    #[serde(
51        rename = "conditionalFormatting",
52        default,
53        skip_serializing_if = "Vec::is_empty"
54    )]
55    pub conditional_formatting: Vec<ConditionalFormatting>,
56
57    #[serde(rename = "dataValidations", skip_serializing_if = "Option::is_none")]
58    pub data_validations: Option<DataValidations>,
59
60    #[serde(rename = "hyperlinks", skip_serializing_if = "Option::is_none")]
61    pub hyperlinks: Option<Hyperlinks>,
62
63    #[serde(rename = "printOptions", skip_serializing_if = "Option::is_none")]
64    pub print_options: Option<PrintOptions>,
65
66    #[serde(rename = "pageMargins", skip_serializing_if = "Option::is_none")]
67    pub page_margins: Option<PageMargins>,
68
69    #[serde(rename = "pageSetup", skip_serializing_if = "Option::is_none")]
70    pub page_setup: Option<PageSetup>,
71
72    #[serde(rename = "headerFooter", skip_serializing_if = "Option::is_none")]
73    pub header_footer: Option<HeaderFooter>,
74
75    #[serde(rename = "rowBreaks", skip_serializing_if = "Option::is_none")]
76    pub row_breaks: Option<RowBreaks>,
77
78    #[serde(rename = "drawing", skip_serializing_if = "Option::is_none")]
79    pub drawing: Option<DrawingRef>,
80
81    #[serde(rename = "legacyDrawing", skip_serializing_if = "Option::is_none")]
82    pub legacy_drawing: Option<LegacyDrawingRef>,
83
84    #[serde(rename = "tableParts", skip_serializing_if = "Option::is_none")]
85    pub table_parts: Option<TableParts>,
86}
87
88/// Sheet dimension reference.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct Dimension {
91    #[serde(rename = "@ref")]
92    pub reference: String,
93}
94
95/// Sheet views container.
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct SheetViews {
98    #[serde(rename = "sheetView")]
99    pub sheet_views: Vec<SheetView>,
100}
101
102/// Individual sheet view.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct SheetView {
105    #[serde(rename = "@tabSelected", skip_serializing_if = "Option::is_none")]
106    pub tab_selected: Option<bool>,
107
108    #[serde(rename = "@zoomScale", skip_serializing_if = "Option::is_none")]
109    pub zoom_scale: Option<u32>,
110
111    #[serde(rename = "@workbookViewId")]
112    pub workbook_view_id: u32,
113
114    #[serde(rename = "pane", skip_serializing_if = "Option::is_none")]
115    pub pane: Option<Pane>,
116
117    #[serde(rename = "selection", default)]
118    pub selection: Vec<Selection>,
119}
120
121/// Pane definition for split or frozen panes.
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct Pane {
124    #[serde(rename = "@xSplit", skip_serializing_if = "Option::is_none")]
125    pub x_split: Option<u32>,
126
127    #[serde(rename = "@ySplit", skip_serializing_if = "Option::is_none")]
128    pub y_split: Option<u32>,
129
130    #[serde(rename = "@topLeftCell", skip_serializing_if = "Option::is_none")]
131    pub top_left_cell: Option<String>,
132
133    #[serde(rename = "@activePane", skip_serializing_if = "Option::is_none")]
134    pub active_pane: Option<String>,
135
136    #[serde(rename = "@state", skip_serializing_if = "Option::is_none")]
137    pub state: Option<String>,
138}
139
140/// Cell selection.
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct Selection {
143    #[serde(rename = "@pane", skip_serializing_if = "Option::is_none")]
144    pub pane: Option<String>,
145
146    #[serde(rename = "@activeCell", skip_serializing_if = "Option::is_none")]
147    pub active_cell: Option<String>,
148
149    #[serde(rename = "@sqref", skip_serializing_if = "Option::is_none")]
150    pub sqref: Option<String>,
151}
152
153/// Sheet properties.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
155pub struct SheetPr {
156    #[serde(rename = "@codeName", skip_serializing_if = "Option::is_none")]
157    pub code_name: Option<String>,
158
159    #[serde(rename = "@filterMode", skip_serializing_if = "Option::is_none")]
160    pub filter_mode: Option<bool>,
161
162    #[serde(rename = "tabColor", skip_serializing_if = "Option::is_none")]
163    pub tab_color: Option<TabColor>,
164
165    #[serde(rename = "outlinePr", skip_serializing_if = "Option::is_none")]
166    pub outline_pr: Option<OutlinePr>,
167}
168
169/// Tab color specification.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct TabColor {
172    #[serde(rename = "@rgb", skip_serializing_if = "Option::is_none")]
173    pub rgb: Option<String>,
174
175    #[serde(rename = "@theme", skip_serializing_if = "Option::is_none")]
176    pub theme: Option<u32>,
177
178    #[serde(rename = "@indexed", skip_serializing_if = "Option::is_none")]
179    pub indexed: Option<u32>,
180}
181
182/// Outline properties for grouping.
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
184pub struct OutlinePr {
185    #[serde(rename = "@summaryBelow", skip_serializing_if = "Option::is_none")]
186    pub summary_below: Option<bool>,
187
188    #[serde(rename = "@summaryRight", skip_serializing_if = "Option::is_none")]
189    pub summary_right: Option<bool>,
190}
191
192/// Sheet format properties.
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct SheetFormatPr {
195    #[serde(rename = "@defaultRowHeight")]
196    pub default_row_height: f64,
197
198    #[serde(rename = "@defaultColWidth", skip_serializing_if = "Option::is_none")]
199    pub default_col_width: Option<f64>,
200
201    #[serde(rename = "@customHeight", skip_serializing_if = "Option::is_none")]
202    pub custom_height: Option<bool>,
203
204    #[serde(rename = "@outlineLevelRow", skip_serializing_if = "Option::is_none")]
205    pub outline_level_row: Option<u8>,
206
207    #[serde(rename = "@outlineLevelCol", skip_serializing_if = "Option::is_none")]
208    pub outline_level_col: Option<u8>,
209}
210
211/// Sheet protection settings.
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
213pub struct SheetProtection {
214    #[serde(rename = "@password", skip_serializing_if = "Option::is_none")]
215    pub password: Option<String>,
216
217    #[serde(rename = "@sheet", skip_serializing_if = "Option::is_none")]
218    pub sheet: Option<bool>,
219
220    #[serde(rename = "@objects", skip_serializing_if = "Option::is_none")]
221    pub objects: Option<bool>,
222
223    #[serde(rename = "@scenarios", skip_serializing_if = "Option::is_none")]
224    pub scenarios: Option<bool>,
225
226    #[serde(rename = "@selectLockedCells", skip_serializing_if = "Option::is_none")]
227    pub select_locked_cells: Option<bool>,
228
229    #[serde(
230        rename = "@selectUnlockedCells",
231        skip_serializing_if = "Option::is_none"
232    )]
233    pub select_unlocked_cells: Option<bool>,
234
235    #[serde(rename = "@formatCells", skip_serializing_if = "Option::is_none")]
236    pub format_cells: Option<bool>,
237
238    #[serde(rename = "@formatColumns", skip_serializing_if = "Option::is_none")]
239    pub format_columns: Option<bool>,
240
241    #[serde(rename = "@formatRows", skip_serializing_if = "Option::is_none")]
242    pub format_rows: Option<bool>,
243
244    #[serde(rename = "@insertColumns", skip_serializing_if = "Option::is_none")]
245    pub insert_columns: Option<bool>,
246
247    #[serde(rename = "@insertRows", skip_serializing_if = "Option::is_none")]
248    pub insert_rows: Option<bool>,
249
250    #[serde(rename = "@insertHyperlinks", skip_serializing_if = "Option::is_none")]
251    pub insert_hyperlinks: Option<bool>,
252
253    #[serde(rename = "@deleteColumns", skip_serializing_if = "Option::is_none")]
254    pub delete_columns: Option<bool>,
255
256    #[serde(rename = "@deleteRows", skip_serializing_if = "Option::is_none")]
257    pub delete_rows: Option<bool>,
258
259    #[serde(rename = "@sort", skip_serializing_if = "Option::is_none")]
260    pub sort: Option<bool>,
261
262    #[serde(rename = "@autoFilter", skip_serializing_if = "Option::is_none")]
263    pub auto_filter: Option<bool>,
264
265    #[serde(rename = "@pivotTables", skip_serializing_if = "Option::is_none")]
266    pub pivot_tables: Option<bool>,
267}
268
269/// Columns container.
270#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
271pub struct Cols {
272    #[serde(rename = "col")]
273    pub cols: Vec<Col>,
274}
275
276/// Individual column definition.
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278pub struct Col {
279    #[serde(rename = "@min")]
280    pub min: u32,
281
282    #[serde(rename = "@max")]
283    pub max: u32,
284
285    #[serde(rename = "@width", skip_serializing_if = "Option::is_none")]
286    pub width: Option<f64>,
287
288    #[serde(rename = "@style", skip_serializing_if = "Option::is_none")]
289    pub style: Option<u32>,
290
291    #[serde(rename = "@hidden", skip_serializing_if = "Option::is_none")]
292    pub hidden: Option<bool>,
293
294    #[serde(rename = "@customWidth", skip_serializing_if = "Option::is_none")]
295    pub custom_width: Option<bool>,
296
297    #[serde(rename = "@outlineLevel", skip_serializing_if = "Option::is_none")]
298    pub outline_level: Option<u8>,
299}
300
301/// Sheet data container holding all rows.
302#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303pub struct SheetData {
304    #[serde(rename = "row", default)]
305    pub rows: Vec<Row>,
306}
307
308/// A single row of cells.
309#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
310pub struct Row {
311    /// 1-based row number.
312    #[serde(rename = "@r")]
313    pub r: u32,
314
315    #[serde(rename = "@spans", skip_serializing_if = "Option::is_none")]
316    pub spans: Option<String>,
317
318    #[serde(rename = "@s", skip_serializing_if = "Option::is_none")]
319    pub s: Option<u32>,
320
321    #[serde(rename = "@customFormat", skip_serializing_if = "Option::is_none")]
322    pub custom_format: Option<bool>,
323
324    #[serde(rename = "@ht", skip_serializing_if = "Option::is_none")]
325    pub ht: Option<f64>,
326
327    #[serde(rename = "@hidden", skip_serializing_if = "Option::is_none")]
328    pub hidden: Option<bool>,
329
330    #[serde(rename = "@customHeight", skip_serializing_if = "Option::is_none")]
331    pub custom_height: Option<bool>,
332
333    #[serde(rename = "@outlineLevel", skip_serializing_if = "Option::is_none")]
334    pub outline_level: Option<u8>,
335
336    #[serde(rename = "c", default)]
337    pub cells: Vec<Cell>,
338}
339
340/// Inline cell reference (e.g., "A1", "XFD1048576") stored without heap allocation.
341/// Max Excel cell ref is "XFD1048576" = 10 chars, so [u8; 10] + u8 length suffices.
342#[derive(Clone, Copy, Default, PartialEq, Eq)]
343pub struct CompactCellRef {
344    buf: [u8; 10],
345    len: u8,
346}
347
348impl CompactCellRef {
349    /// Create a new CompactCellRef from a string slice. Panics if the input exceeds 10 bytes.
350    pub fn new(s: &str) -> Self {
351        assert!(
352            s.len() <= 10,
353            "cell reference too long ({} bytes): {s}",
354            s.len()
355        );
356        let mut buf = [0u8; 10];
357        buf[..s.len()].copy_from_slice(s.as_bytes());
358        Self {
359            buf,
360            len: s.len() as u8,
361        }
362    }
363
364    /// Return the cell reference as a string slice.
365    pub fn as_str(&self) -> &str {
366        // Safety: we only ever store valid UTF-8 (ASCII cell refs).
367        unsafe { std::str::from_utf8_unchecked(&self.buf[..self.len as usize]) }
368    }
369
370    /// Convert 1-based column and row numbers into a CompactCellRef with zero heap allocation.
371    pub fn from_coordinates(col: u32, row: u32) -> Self {
372        let mut buf = [0u8; 10];
373        let mut pos = 0;
374
375        // Convert column number to letters (A-XFD)
376        let mut col_buf = [0u8; 3];
377        let mut col_len = 0;
378        let mut c = col;
379        while c > 0 {
380            c -= 1;
381            col_buf[col_len] = b'A' + (c % 26) as u8;
382            col_len += 1;
383            c /= 26;
384        }
385        // Write column letters in correct (reversed) order
386        for i in (0..col_len).rev() {
387            buf[pos] = col_buf[i];
388            pos += 1;
389        }
390
391        // Convert row number to digits
392        let mut row_buf = [0u8; 7];
393        let mut row_len = 0;
394        let mut r = row;
395        while r > 0 {
396            row_buf[row_len] = b'0' + (r % 10) as u8;
397            row_len += 1;
398            r /= 10;
399        }
400        // Write row digits in correct (reversed) order
401        for i in (0..row_len).rev() {
402            buf[pos] = row_buf[i];
403            pos += 1;
404        }
405
406        Self {
407            buf,
408            len: pos as u8,
409        }
410    }
411}
412
413impl fmt::Display for CompactCellRef {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        f.write_str(self.as_str())
416    }
417}
418
419impl fmt::Debug for CompactCellRef {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        write!(f, "CompactCellRef(\"{}\")", self.as_str())
422    }
423}
424
425impl From<&str> for CompactCellRef {
426    fn from(s: &str) -> Self {
427        Self::new(s)
428    }
429}
430
431impl From<String> for CompactCellRef {
432    fn from(s: String) -> Self {
433        Self::new(&s)
434    }
435}
436
437impl AsRef<str> for CompactCellRef {
438    fn as_ref(&self) -> &str {
439        self.as_str()
440    }
441}
442
443impl PartialEq<&str> for CompactCellRef {
444    fn eq(&self, other: &&str) -> bool {
445        self.as_str() == *other
446    }
447}
448
449impl PartialEq<str> for CompactCellRef {
450    fn eq(&self, other: &str) -> bool {
451        self.as_str() == other
452    }
453}
454
455impl Serialize for CompactCellRef {
456    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
457        serializer.serialize_str(self.as_str())
458    }
459}
460
461impl<'de> Deserialize<'de> for CompactCellRef {
462    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
463        let s = String::deserialize(deserializer)?;
464        Ok(CompactCellRef::new(&s))
465    }
466}
467
468/// A single cell.
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
470pub struct Cell {
471    /// Cell reference (e.g., "A1").
472    #[serde(rename = "@r")]
473    pub r: CompactCellRef,
474
475    /// Cached 1-based column number parsed from `r`. Populated at load time
476    /// or at creation time to avoid repeated string parsing.
477    #[serde(skip)]
478    pub col: u32,
479
480    /// Style index.
481    #[serde(rename = "@s", skip_serializing_if = "Option::is_none")]
482    pub s: Option<u32>,
483
484    /// Cell data type.
485    #[serde(rename = "@t", default, skip_serializing_if = "CellTypeTag::is_none")]
486    pub t: CellTypeTag,
487
488    /// Cell value.
489    #[serde(rename = "v", skip_serializing_if = "Option::is_none")]
490    pub v: Option<String>,
491
492    /// Cell formula.
493    #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
494    pub f: Option<CellFormula>,
495
496    /// Inline string.
497    #[serde(rename = "is", skip_serializing_if = "Option::is_none")]
498    pub is: Option<InlineString>,
499}
500
501/// Cell data type tag, replacing `Option<String>` for zero-allocation matching.
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
503pub enum CellTypeTag {
504    /// No type attribute (typically means Number).
505    #[default]
506    None,
507    /// Shared string ("s").
508    SharedString,
509    /// Explicit number ("n").
510    Number,
511    /// Boolean ("b").
512    Boolean,
513    /// Error ("e").
514    Error,
515    /// Inline string ("inlineStr").
516    InlineString,
517    /// Formula string result ("str").
518    FormulaString,
519    /// Date ("d").
520    Date,
521}
522
523impl CellTypeTag {
524    /// Returns `true` when the tag is `None`, used for `skip_serializing_if`.
525    pub fn is_none(&self) -> bool {
526        matches!(self, CellTypeTag::None)
527    }
528
529    /// Returns the XML string representation, or `None` for the default variant.
530    pub fn as_str(&self) -> Option<&'static str> {
531        match self {
532            CellTypeTag::None => Option::None,
533            CellTypeTag::SharedString => Some("s"),
534            CellTypeTag::Number => Some("n"),
535            CellTypeTag::Boolean => Some("b"),
536            CellTypeTag::Error => Some("e"),
537            CellTypeTag::InlineString => Some("inlineStr"),
538            CellTypeTag::FormulaString => Some("str"),
539            CellTypeTag::Date => Some("d"),
540        }
541    }
542}
543
544impl Serialize for CellTypeTag {
545    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
546        match self.as_str() {
547            Some(s) => serializer.serialize_str(s),
548            Option::None => serializer.serialize_none(),
549        }
550    }
551}
552
553impl<'de> Deserialize<'de> for CellTypeTag {
554    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
555        let s = String::deserialize(deserializer)?;
556        match s.as_str() {
557            "s" => Ok(CellTypeTag::SharedString),
558            "n" => Ok(CellTypeTag::Number),
559            "b" => Ok(CellTypeTag::Boolean),
560            "e" => Ok(CellTypeTag::Error),
561            "inlineStr" => Ok(CellTypeTag::InlineString),
562            "str" => Ok(CellTypeTag::FormulaString),
563            "d" => Ok(CellTypeTag::Date),
564            _ => Ok(CellTypeTag::None),
565        }
566    }
567}
568
569/// Cell formula.
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
571pub struct CellFormula {
572    #[serde(rename = "@t", skip_serializing_if = "Option::is_none")]
573    pub t: Option<String>,
574
575    #[serde(rename = "@ref", skip_serializing_if = "Option::is_none")]
576    pub reference: Option<String>,
577
578    #[serde(rename = "@si", skip_serializing_if = "Option::is_none")]
579    pub si: Option<u32>,
580
581    #[serde(rename = "$value", skip_serializing_if = "Option::is_none")]
582    pub value: Option<String>,
583}
584
585/// Inline string within a cell.
586#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
587pub struct InlineString {
588    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
589    pub t: Option<String>,
590}
591
592/// Auto filter.
593#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
594pub struct AutoFilter {
595    #[serde(rename = "@ref")]
596    pub reference: String,
597}
598
599/// Data validations container.
600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
601pub struct DataValidations {
602    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
603    pub count: Option<u32>,
604
605    #[serde(
606        rename = "@disablePrompts",
607        skip_serializing_if = "Option::is_none",
608        default
609    )]
610    pub disable_prompts: Option<bool>,
611
612    #[serde(rename = "@xWindow", skip_serializing_if = "Option::is_none", default)]
613    pub x_window: Option<u32>,
614
615    #[serde(rename = "@yWindow", skip_serializing_if = "Option::is_none", default)]
616    pub y_window: Option<u32>,
617
618    #[serde(rename = "dataValidation", default)]
619    pub data_validations: Vec<DataValidation>,
620}
621
622/// Individual data validation rule.
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub struct DataValidation {
625    #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
626    pub validation_type: Option<String>,
627
628    #[serde(rename = "@operator", skip_serializing_if = "Option::is_none")]
629    pub operator: Option<String>,
630
631    #[serde(rename = "@allowBlank", skip_serializing_if = "Option::is_none")]
632    pub allow_blank: Option<bool>,
633
634    #[serde(
635        rename = "@showDropDown",
636        skip_serializing_if = "Option::is_none",
637        default
638    )]
639    pub show_drop_down: Option<bool>,
640
641    #[serde(rename = "@showInputMessage", skip_serializing_if = "Option::is_none")]
642    pub show_input_message: Option<bool>,
643
644    #[serde(rename = "@showErrorMessage", skip_serializing_if = "Option::is_none")]
645    pub show_error_message: Option<bool>,
646
647    #[serde(rename = "@errorStyle", skip_serializing_if = "Option::is_none")]
648    pub error_style: Option<String>,
649
650    #[serde(rename = "@imeMode", skip_serializing_if = "Option::is_none", default)]
651    pub ime_mode: Option<String>,
652
653    #[serde(rename = "@errorTitle", skip_serializing_if = "Option::is_none")]
654    pub error_title: Option<String>,
655
656    #[serde(rename = "@error", skip_serializing_if = "Option::is_none")]
657    pub error: Option<String>,
658
659    #[serde(rename = "@promptTitle", skip_serializing_if = "Option::is_none")]
660    pub prompt_title: Option<String>,
661
662    #[serde(rename = "@prompt", skip_serializing_if = "Option::is_none")]
663    pub prompt: Option<String>,
664
665    #[serde(rename = "@sqref")]
666    pub sqref: String,
667
668    #[serde(rename = "formula1", skip_serializing_if = "Option::is_none")]
669    pub formula1: Option<String>,
670
671    #[serde(rename = "formula2", skip_serializing_if = "Option::is_none")]
672    pub formula2: Option<String>,
673}
674
675/// Merge cells container.
676#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
677pub struct MergeCells {
678    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
679    pub count: Option<u32>,
680
681    #[serde(rename = "mergeCell", default)]
682    pub merge_cells: Vec<MergeCell>,
683}
684
685/// Individual merge cell reference.
686#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
687pub struct MergeCell {
688    #[serde(rename = "@ref")]
689    pub reference: String,
690}
691
692/// Hyperlinks container.
693#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
694pub struct Hyperlinks {
695    #[serde(rename = "hyperlink", default)]
696    pub hyperlinks: Vec<Hyperlink>,
697}
698
699/// Individual hyperlink.
700#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
701pub struct Hyperlink {
702    #[serde(rename = "@ref")]
703    pub reference: String,
704
705    #[serde(
706        rename = "@r:id",
707        alias = "@id",
708        skip_serializing_if = "Option::is_none"
709    )]
710    pub r_id: Option<String>,
711
712    #[serde(rename = "@location", skip_serializing_if = "Option::is_none")]
713    pub location: Option<String>,
714
715    #[serde(rename = "@display", skip_serializing_if = "Option::is_none")]
716    pub display: Option<String>,
717
718    #[serde(rename = "@tooltip", skip_serializing_if = "Option::is_none")]
719    pub tooltip: Option<String>,
720}
721
722/// Page margins.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724pub struct PageMargins {
725    #[serde(rename = "@left")]
726    pub left: f64,
727
728    #[serde(rename = "@right")]
729    pub right: f64,
730
731    #[serde(rename = "@top")]
732    pub top: f64,
733
734    #[serde(rename = "@bottom")]
735    pub bottom: f64,
736
737    #[serde(rename = "@header")]
738    pub header: f64,
739
740    #[serde(rename = "@footer")]
741    pub footer: f64,
742}
743
744/// Page setup.
745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
746pub struct PageSetup {
747    #[serde(rename = "@paperSize", skip_serializing_if = "Option::is_none")]
748    pub paper_size: Option<u32>,
749
750    #[serde(rename = "@orientation", skip_serializing_if = "Option::is_none")]
751    pub orientation: Option<String>,
752
753    #[serde(rename = "@scale", skip_serializing_if = "Option::is_none")]
754    pub scale: Option<u32>,
755
756    #[serde(rename = "@fitToWidth", skip_serializing_if = "Option::is_none")]
757    pub fit_to_width: Option<u32>,
758
759    #[serde(rename = "@fitToHeight", skip_serializing_if = "Option::is_none")]
760    pub fit_to_height: Option<u32>,
761
762    #[serde(rename = "@firstPageNumber", skip_serializing_if = "Option::is_none")]
763    pub first_page_number: Option<u32>,
764
765    #[serde(rename = "@horizontalDpi", skip_serializing_if = "Option::is_none")]
766    pub horizontal_dpi: Option<u32>,
767
768    #[serde(rename = "@verticalDpi", skip_serializing_if = "Option::is_none")]
769    pub vertical_dpi: Option<u32>,
770
771    #[serde(
772        rename = "@r:id",
773        alias = "@id",
774        skip_serializing_if = "Option::is_none"
775    )]
776    pub r_id: Option<String>,
777}
778
779/// Header and footer for printing.
780#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
781pub struct HeaderFooter {
782    #[serde(rename = "oddHeader", skip_serializing_if = "Option::is_none")]
783    pub odd_header: Option<String>,
784
785    #[serde(rename = "oddFooter", skip_serializing_if = "Option::is_none")]
786    pub odd_footer: Option<String>,
787}
788
789/// Print options.
790#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
791pub struct PrintOptions {
792    #[serde(rename = "@gridLines", skip_serializing_if = "Option::is_none")]
793    pub grid_lines: Option<bool>,
794
795    #[serde(rename = "@headings", skip_serializing_if = "Option::is_none")]
796    pub headings: Option<bool>,
797
798    #[serde(
799        rename = "@horizontalCentered",
800        skip_serializing_if = "Option::is_none"
801    )]
802    pub horizontal_centered: Option<bool>,
803
804    #[serde(rename = "@verticalCentered", skip_serializing_if = "Option::is_none")]
805    pub vertical_centered: Option<bool>,
806}
807
808/// Row page breaks container.
809#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
810pub struct RowBreaks {
811    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
812    pub count: Option<u32>,
813
814    #[serde(rename = "@manualBreakCount", skip_serializing_if = "Option::is_none")]
815    pub manual_break_count: Option<u32>,
816
817    #[serde(rename = "brk", default)]
818    pub brk: Vec<Break>,
819}
820
821/// Individual page break entry.
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
823pub struct Break {
824    #[serde(rename = "@id")]
825    pub id: u32,
826
827    #[serde(rename = "@max", skip_serializing_if = "Option::is_none")]
828    pub max: Option<u32>,
829
830    #[serde(rename = "@man", skip_serializing_if = "Option::is_none")]
831    pub man: Option<bool>,
832}
833
834/// Drawing reference.
835#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
836pub struct DrawingRef {
837    #[serde(rename = "@r:id", alias = "@id")]
838    pub r_id: String,
839}
840
841/// Legacy drawing reference (VML).
842#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
843pub struct LegacyDrawingRef {
844    #[serde(rename = "@r:id", alias = "@id")]
845    pub r_id: String,
846}
847
848/// Table parts container.
849#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
850pub struct TableParts {
851    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
852    pub count: Option<u32>,
853
854    #[serde(rename = "tablePart", default)]
855    pub table_parts: Vec<TablePart>,
856}
857
858/// Individual table part reference.
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
860pub struct TablePart {
861    #[serde(rename = "@r:id", alias = "@id")]
862    pub r_id: String,
863}
864
865/// Conditional formatting container.
866#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
867pub struct ConditionalFormatting {
868    #[serde(rename = "@sqref")]
869    pub sqref: String,
870
871    #[serde(rename = "cfRule", default)]
872    pub cf_rules: Vec<CfRule>,
873}
874
875/// Conditional formatting rule.
876#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
877pub struct CfRule {
878    #[serde(rename = "@type")]
879    pub rule_type: String,
880
881    #[serde(rename = "@dxfId", skip_serializing_if = "Option::is_none")]
882    pub dxf_id: Option<u32>,
883
884    #[serde(rename = "@priority")]
885    pub priority: u32,
886
887    #[serde(rename = "@operator", skip_serializing_if = "Option::is_none")]
888    pub operator: Option<String>,
889
890    #[serde(rename = "@text", skip_serializing_if = "Option::is_none")]
891    pub text: Option<String>,
892
893    #[serde(rename = "@stopIfTrue", skip_serializing_if = "Option::is_none")]
894    pub stop_if_true: Option<bool>,
895
896    #[serde(rename = "@aboveAverage", skip_serializing_if = "Option::is_none")]
897    pub above_average: Option<bool>,
898
899    #[serde(rename = "@equalAverage", skip_serializing_if = "Option::is_none")]
900    pub equal_average: Option<bool>,
901
902    #[serde(rename = "@percent", skip_serializing_if = "Option::is_none")]
903    pub percent: Option<bool>,
904
905    #[serde(rename = "@rank", skip_serializing_if = "Option::is_none")]
906    pub rank: Option<u32>,
907
908    #[serde(rename = "@bottom", skip_serializing_if = "Option::is_none")]
909    pub bottom: Option<bool>,
910
911    #[serde(rename = "formula", default, skip_serializing_if = "Vec::is_empty")]
912    pub formulas: Vec<String>,
913
914    #[serde(rename = "colorScale", skip_serializing_if = "Option::is_none")]
915    pub color_scale: Option<CfColorScale>,
916
917    #[serde(rename = "dataBar", skip_serializing_if = "Option::is_none")]
918    pub data_bar: Option<CfDataBar>,
919
920    #[serde(rename = "iconSet", skip_serializing_if = "Option::is_none")]
921    pub icon_set: Option<CfIconSet>,
922}
923
924/// Color scale definition for conditional formatting.
925#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
926pub struct CfColorScale {
927    #[serde(rename = "cfvo", default)]
928    pub cfvos: Vec<CfVo>,
929
930    #[serde(rename = "color", default)]
931    pub colors: Vec<CfColor>,
932}
933
934/// Data bar definition for conditional formatting.
935#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
936pub struct CfDataBar {
937    #[serde(rename = "@showValue", skip_serializing_if = "Option::is_none")]
938    pub show_value: Option<bool>,
939
940    #[serde(rename = "cfvo", default)]
941    pub cfvos: Vec<CfVo>,
942
943    #[serde(rename = "color", skip_serializing_if = "Option::is_none")]
944    pub color: Option<CfColor>,
945}
946
947/// Icon set definition for conditional formatting.
948#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
949pub struct CfIconSet {
950    #[serde(rename = "@iconSet", skip_serializing_if = "Option::is_none")]
951    pub icon_set: Option<String>,
952
953    #[serde(rename = "cfvo", default)]
954    pub cfvos: Vec<CfVo>,
955}
956
957/// Conditional formatting value object.
958#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
959pub struct CfVo {
960    #[serde(rename = "@type")]
961    pub value_type: String,
962
963    #[serde(rename = "@val", skip_serializing_if = "Option::is_none")]
964    pub val: Option<String>,
965}
966
967/// Color reference for conditional formatting.
968#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
969pub struct CfColor {
970    #[serde(rename = "@rgb", skip_serializing_if = "Option::is_none")]
971    pub rgb: Option<String>,
972
973    #[serde(rename = "@theme", skip_serializing_if = "Option::is_none")]
974    pub theme: Option<u32>,
975
976    #[serde(rename = "@tint", skip_serializing_if = "Option::is_none")]
977    pub tint: Option<f64>,
978}
979
980impl Default for WorksheetXml {
981    fn default() -> Self {
982        Self {
983            xmlns: namespaces::SPREADSHEET_ML.to_string(),
984            xmlns_r: namespaces::RELATIONSHIPS.to_string(),
985            sheet_pr: None,
986            dimension: None,
987            sheet_views: None,
988            sheet_format_pr: None,
989            cols: None,
990            sheet_data: SheetData { rows: vec![] },
991            sheet_protection: None,
992            auto_filter: None,
993            merge_cells: None,
994            conditional_formatting: vec![],
995            data_validations: None,
996            hyperlinks: None,
997            print_options: None,
998            page_margins: None,
999            page_setup: None,
1000            header_footer: None,
1001            row_breaks: None,
1002            drawing: None,
1003            legacy_drawing: None,
1004            table_parts: None,
1005        }
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012
1013    #[test]
1014    fn test_worksheet_default() {
1015        let ws = WorksheetXml::default();
1016        assert_eq!(ws.xmlns, namespaces::SPREADSHEET_ML);
1017        assert_eq!(ws.xmlns_r, namespaces::RELATIONSHIPS);
1018        assert!(ws.sheet_data.rows.is_empty());
1019        assert!(ws.dimension.is_none());
1020        assert!(ws.sheet_views.is_none());
1021        assert!(ws.cols.is_none());
1022        assert!(ws.merge_cells.is_none());
1023        assert!(ws.page_margins.is_none());
1024        assert!(ws.sheet_pr.is_none());
1025        assert!(ws.sheet_protection.is_none());
1026    }
1027
1028    #[test]
1029    fn test_worksheet_roundtrip() {
1030        let ws = WorksheetXml::default();
1031        let xml = quick_xml::se::to_string(&ws).unwrap();
1032        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1033        assert_eq!(ws.xmlns, parsed.xmlns);
1034        assert_eq!(ws.xmlns_r, parsed.xmlns_r);
1035        assert_eq!(ws.sheet_data.rows.len(), parsed.sheet_data.rows.len());
1036    }
1037
1038    #[test]
1039    fn test_worksheet_with_data() {
1040        let ws = WorksheetXml {
1041            sheet_data: SheetData {
1042                rows: vec![Row {
1043                    r: 1,
1044                    spans: Some("1:3".to_string()),
1045                    s: None,
1046                    custom_format: None,
1047                    ht: None,
1048                    hidden: None,
1049                    custom_height: None,
1050                    outline_level: None,
1051                    cells: vec![
1052                        Cell {
1053                            r: CompactCellRef::new("A1"),
1054                            col: 1,
1055                            s: None,
1056                            t: CellTypeTag::SharedString,
1057                            v: Some("0".to_string()),
1058                            f: None,
1059                            is: None,
1060                        },
1061                        Cell {
1062                            r: CompactCellRef::new("B1"),
1063                            col: 2,
1064                            s: None,
1065                            t: CellTypeTag::None,
1066                            v: Some("42".to_string()),
1067                            f: None,
1068                            is: None,
1069                        },
1070                    ],
1071                }],
1072            },
1073            ..WorksheetXml::default()
1074        };
1075
1076        let xml = quick_xml::se::to_string(&ws).unwrap();
1077        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1078        assert_eq!(parsed.sheet_data.rows.len(), 1);
1079        assert_eq!(parsed.sheet_data.rows[0].r, 1);
1080        assert_eq!(parsed.sheet_data.rows[0].cells.len(), 2);
1081        assert_eq!(parsed.sheet_data.rows[0].cells[0].r, "A1");
1082        assert_eq!(
1083            parsed.sheet_data.rows[0].cells[0].t,
1084            CellTypeTag::SharedString
1085        );
1086        assert_eq!(parsed.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1087        assert_eq!(parsed.sheet_data.rows[0].cells[1].r, "B1");
1088        assert_eq!(parsed.sheet_data.rows[0].cells[1].v, Some("42".to_string()));
1089    }
1090
1091    #[test]
1092    fn test_cell_with_formula() {
1093        let cell = Cell {
1094            r: CompactCellRef::new("C1"),
1095            col: 3,
1096            s: None,
1097            t: CellTypeTag::None,
1098            v: Some("84".to_string()),
1099            f: Some(CellFormula {
1100                t: None,
1101                reference: None,
1102                si: None,
1103                value: Some("A1+B1".to_string()),
1104            }),
1105            is: None,
1106        };
1107        let xml = quick_xml::se::to_string(&cell).unwrap();
1108        assert!(xml.contains("A1+B1"));
1109        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1110        assert!(parsed.f.is_some());
1111        assert_eq!(parsed.f.unwrap().value, Some("A1+B1".to_string()));
1112    }
1113
1114    #[test]
1115    fn test_cell_with_inline_string() {
1116        let cell = Cell {
1117            r: CompactCellRef::new("A1"),
1118            col: 1,
1119            s: None,
1120            t: CellTypeTag::InlineString,
1121            v: None,
1122            f: None,
1123            is: Some(InlineString {
1124                t: Some("Hello World".to_string()),
1125            }),
1126        };
1127        let xml = quick_xml::se::to_string(&cell).unwrap();
1128        assert!(xml.contains("Hello World"));
1129        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1130        assert_eq!(parsed.t, CellTypeTag::InlineString);
1131        assert!(parsed.is.is_some());
1132        assert_eq!(parsed.is.unwrap().t, Some("Hello World".to_string()));
1133    }
1134
1135    #[test]
1136    fn test_parse_real_excel_worksheet() {
1137        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1138<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1139  <dimension ref="A1:B2"/>
1140  <sheetData>
1141    <row r="1" spans="1:2">
1142      <c r="A1" t="s"><v>0</v></c>
1143      <c r="B1" t="s"><v>1</v></c>
1144    </row>
1145    <row r="2" spans="1:2">
1146      <c r="A2"><v>100</v></c>
1147      <c r="B2"><v>200</v></c>
1148    </row>
1149  </sheetData>
1150</worksheet>"#;
1151
1152        let parsed: WorksheetXml = quick_xml::de::from_str(xml).unwrap();
1153        assert_eq!(parsed.dimension.as_ref().unwrap().reference, "A1:B2");
1154        assert_eq!(parsed.sheet_data.rows.len(), 2);
1155        assert_eq!(parsed.sheet_data.rows[0].cells.len(), 2);
1156        assert_eq!(parsed.sheet_data.rows[0].cells[0].r, "A1");
1157        assert_eq!(
1158            parsed.sheet_data.rows[0].cells[0].t,
1159            CellTypeTag::SharedString
1160        );
1161        assert_eq!(parsed.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1162        assert_eq!(parsed.sheet_data.rows[1].cells[0].r, "A2");
1163        assert_eq!(
1164            parsed.sheet_data.rows[1].cells[0].v,
1165            Some("100".to_string())
1166        );
1167    }
1168
1169    #[test]
1170    fn test_worksheet_with_merge_cells() {
1171        let ws = WorksheetXml {
1172            merge_cells: Some(MergeCells {
1173                count: Some(1),
1174                merge_cells: vec![MergeCell {
1175                    reference: "A1:B2".to_string(),
1176                }],
1177            }),
1178            ..WorksheetXml::default()
1179        };
1180        let xml = quick_xml::se::to_string(&ws).unwrap();
1181        assert!(xml.contains("mergeCells"));
1182        assert!(xml.contains("A1:B2"));
1183        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1184        assert!(parsed.merge_cells.is_some());
1185        assert_eq!(parsed.merge_cells.as_ref().unwrap().merge_cells.len(), 1);
1186    }
1187
1188    #[test]
1189    fn test_empty_sheet_data_serialization() {
1190        let sd = SheetData { rows: vec![] };
1191        let xml = quick_xml::se::to_string(&sd).unwrap();
1192        // Empty SheetData should still be serializable
1193        let parsed: SheetData = quick_xml::de::from_str(&xml).unwrap();
1194        assert!(parsed.rows.is_empty());
1195    }
1196
1197    #[test]
1198    fn test_row_optional_fields_not_serialized() {
1199        let row = Row {
1200            r: 1,
1201            spans: None,
1202            s: None,
1203            custom_format: None,
1204            ht: None,
1205            hidden: None,
1206            custom_height: None,
1207            outline_level: None,
1208            cells: vec![],
1209        };
1210        let xml = quick_xml::se::to_string(&row).unwrap();
1211        assert!(!xml.contains("spans"));
1212        assert!(!xml.contains("ht"));
1213        assert!(!xml.contains("hidden"));
1214    }
1215
1216    #[test]
1217    fn test_cell_type_tag_as_str() {
1218        assert_eq!(CellTypeTag::Boolean.as_str(), Some("b"));
1219        assert_eq!(CellTypeTag::Date.as_str(), Some("d"));
1220        assert_eq!(CellTypeTag::Error.as_str(), Some("e"));
1221        assert_eq!(CellTypeTag::InlineString.as_str(), Some("inlineStr"));
1222        assert_eq!(CellTypeTag::Number.as_str(), Some("n"));
1223        assert_eq!(CellTypeTag::SharedString.as_str(), Some("s"));
1224        assert_eq!(CellTypeTag::FormulaString.as_str(), Some("str"));
1225        assert_eq!(CellTypeTag::None.as_str(), Option::None);
1226    }
1227
1228    #[test]
1229    fn test_cell_type_tag_default_is_none() {
1230        assert_eq!(CellTypeTag::default(), CellTypeTag::None);
1231        assert!(CellTypeTag::None.is_none());
1232        assert!(!CellTypeTag::SharedString.is_none());
1233    }
1234
1235    #[test]
1236    fn test_cell_type_tag_serde_round_trip() {
1237        let variants = [
1238            (CellTypeTag::SharedString, "s"),
1239            (CellTypeTag::Number, "n"),
1240            (CellTypeTag::Boolean, "b"),
1241            (CellTypeTag::Error, "e"),
1242            (CellTypeTag::InlineString, "inlineStr"),
1243            (CellTypeTag::FormulaString, "str"),
1244            (CellTypeTag::Date, "d"),
1245        ];
1246        for (tag, expected_str) in &variants {
1247            let cell = Cell {
1248                r: CompactCellRef::new("A1"),
1249                col: 1,
1250                s: None,
1251                t: *tag,
1252                v: Some("0".to_string()),
1253                f: None,
1254                is: None,
1255            };
1256            let xml = quick_xml::se::to_string(&cell).unwrap();
1257            assert!(
1258                xml.contains(&format!("t=\"{expected_str}\"")),
1259                "expected t=\"{expected_str}\" in: {xml}"
1260            );
1261            let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1262            assert_eq!(parsed.t, *tag);
1263        }
1264
1265        let cell_none = Cell {
1266            r: CompactCellRef::new("A1"),
1267            col: 1,
1268            s: None,
1269            t: CellTypeTag::None,
1270            v: Some("42".to_string()),
1271            f: None,
1272            is: None,
1273        };
1274        let xml = quick_xml::se::to_string(&cell_none).unwrap();
1275        assert!(
1276            !xml.contains("t="),
1277            "None variant should not emit t attribute: {xml}"
1278        );
1279        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1280        assert_eq!(parsed.t, CellTypeTag::None);
1281    }
1282
1283    #[test]
1284    fn test_worksheet_with_cols() {
1285        let ws = WorksheetXml {
1286            cols: Some(Cols {
1287                cols: vec![Col {
1288                    min: 1,
1289                    max: 1,
1290                    width: Some(15.0),
1291                    style: None,
1292                    hidden: None,
1293                    custom_width: Some(true),
1294                    outline_level: None,
1295                }],
1296            }),
1297            ..WorksheetXml::default()
1298        };
1299        let xml = quick_xml::se::to_string(&ws).unwrap();
1300        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1301        assert!(parsed.cols.is_some());
1302        let cols = parsed.cols.unwrap();
1303        assert_eq!(cols.cols.len(), 1);
1304        assert_eq!(cols.cols[0].min, 1);
1305        assert_eq!(cols.cols[0].width, Some(15.0));
1306        assert_eq!(cols.cols[0].custom_width, Some(true));
1307    }
1308
1309    #[test]
1310    fn test_sheet_protection_roundtrip() {
1311        let prot = SheetProtection {
1312            password: Some("ABCD".to_string()),
1313            sheet: Some(true),
1314            objects: Some(true),
1315            scenarios: Some(true),
1316            format_cells: Some(false),
1317            ..SheetProtection::default()
1318        };
1319        let xml = quick_xml::se::to_string(&prot).unwrap();
1320        let parsed: SheetProtection = quick_xml::de::from_str(&xml).unwrap();
1321        assert_eq!(parsed.password, Some("ABCD".to_string()));
1322        assert_eq!(parsed.sheet, Some(true));
1323        assert_eq!(parsed.objects, Some(true));
1324        assert_eq!(parsed.scenarios, Some(true));
1325        assert_eq!(parsed.format_cells, Some(false));
1326        assert!(parsed.sort.is_none());
1327    }
1328
1329    #[test]
1330    fn test_sheet_pr_roundtrip() {
1331        let pr = SheetPr {
1332            code_name: Some("Sheet1".to_string()),
1333            tab_color: Some(TabColor {
1334                rgb: Some("FF0000".to_string()),
1335                theme: None,
1336                indexed: None,
1337            }),
1338            ..SheetPr::default()
1339        };
1340        let xml = quick_xml::se::to_string(&pr).unwrap();
1341        let parsed: SheetPr = quick_xml::de::from_str(&xml).unwrap();
1342        assert_eq!(parsed.code_name, Some("Sheet1".to_string()));
1343        assert!(parsed.tab_color.is_some());
1344        assert_eq!(parsed.tab_color.unwrap().rgb, Some("FF0000".to_string()));
1345    }
1346
1347    #[test]
1348    fn test_sheet_format_pr_extended_fields() {
1349        let fmt = SheetFormatPr {
1350            default_row_height: 15.0,
1351            default_col_width: Some(10.0),
1352            custom_height: Some(true),
1353            outline_level_row: Some(2),
1354            outline_level_col: Some(1),
1355        };
1356        let xml = quick_xml::se::to_string(&fmt).unwrap();
1357        let parsed: SheetFormatPr = quick_xml::de::from_str(&xml).unwrap();
1358        assert_eq!(parsed.default_row_height, 15.0);
1359        assert_eq!(parsed.default_col_width, Some(10.0));
1360        assert_eq!(parsed.custom_height, Some(true));
1361        assert_eq!(parsed.outline_level_row, Some(2));
1362        assert_eq!(parsed.outline_level_col, Some(1));
1363    }
1364
1365    #[test]
1366    fn test_compact_cell_ref_basic() {
1367        let r = CompactCellRef::new("A1");
1368        assert_eq!(r.as_str(), "A1");
1369        assert_eq!(r.len, 2);
1370    }
1371
1372    #[test]
1373    fn test_compact_cell_ref_max_length() {
1374        let r = CompactCellRef::new("XFD1048576");
1375        assert_eq!(r.as_str(), "XFD1048576");
1376        assert_eq!(r.len, 10);
1377    }
1378
1379    #[test]
1380    fn test_compact_cell_ref_various_lengths() {
1381        for s in &["A1", "B5", "Z99", "AA100", "XFD1048576"] {
1382            let r = CompactCellRef::new(s);
1383            assert_eq!(r.as_str(), *s);
1384        }
1385    }
1386
1387    #[test]
1388    fn test_compact_cell_ref_display() {
1389        let r = CompactCellRef::new("C3");
1390        assert_eq!(format!("{r}"), "C3");
1391    }
1392
1393    #[test]
1394    fn test_compact_cell_ref_debug() {
1395        let r = CompactCellRef::new("C3");
1396        let dbg = format!("{r:?}");
1397        assert!(dbg.contains("CompactCellRef"));
1398        assert!(dbg.contains("C3"));
1399    }
1400
1401    #[test]
1402    fn test_compact_cell_ref_default() {
1403        let r = CompactCellRef::default();
1404        assert_eq!(r.as_str(), "");
1405        assert_eq!(r.len, 0);
1406    }
1407
1408    #[test]
1409    fn test_compact_cell_ref_from_str() {
1410        let r: CompactCellRef = "D4".into();
1411        assert_eq!(r.as_str(), "D4");
1412    }
1413
1414    #[test]
1415    fn test_compact_cell_ref_from_string() {
1416        let r: CompactCellRef = String::from("E5").into();
1417        assert_eq!(r.as_str(), "E5");
1418    }
1419
1420    #[test]
1421    fn test_compact_cell_ref_as_ref_str() {
1422        let r = CompactCellRef::new("F6");
1423        let s: &str = r.as_ref();
1424        assert_eq!(s, "F6");
1425    }
1426
1427    #[test]
1428    fn test_compact_cell_ref_partial_eq_str() {
1429        let r = CompactCellRef::new("G7");
1430        assert_eq!(r, "G7");
1431        assert!(r == "G7");
1432        assert!(r != "H8");
1433    }
1434
1435    #[test]
1436    fn test_compact_cell_ref_copy() {
1437        let r1 = CompactCellRef::new("A1");
1438        let r2 = r1;
1439        assert_eq!(r1.as_str(), "A1");
1440        assert_eq!(r2.as_str(), "A1");
1441    }
1442
1443    #[test]
1444    fn test_compact_cell_ref_serde_roundtrip() {
1445        let cell = Cell {
1446            r: CompactCellRef::new("XFD1048576"),
1447            col: 16384,
1448            s: None,
1449            t: CellTypeTag::None,
1450            v: Some("42".to_string()),
1451            f: None,
1452            is: None,
1453        };
1454        let xml = quick_xml::se::to_string(&cell).unwrap();
1455        assert!(xml.contains("XFD1048576"));
1456        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1457        assert_eq!(parsed.r, "XFD1048576");
1458        assert_eq!(parsed.v, Some("42".to_string()));
1459    }
1460
1461    #[test]
1462    #[should_panic(expected = "cell reference too long")]
1463    fn test_compact_cell_ref_panics_on_overflow() {
1464        CompactCellRef::new("ABCDEFGHIJK");
1465    }
1466
1467    #[test]
1468    fn test_compact_cell_ref_from_coordinates_a1() {
1469        let r = CompactCellRef::from_coordinates(1, 1);
1470        assert_eq!(r.as_str(), "A1");
1471    }
1472
1473    #[test]
1474    fn test_compact_cell_ref_from_coordinates_z26() {
1475        let r = CompactCellRef::from_coordinates(26, 26);
1476        assert_eq!(r.as_str(), "Z26");
1477    }
1478
1479    #[test]
1480    fn test_compact_cell_ref_from_coordinates_aa1() {
1481        let r = CompactCellRef::from_coordinates(27, 1);
1482        assert_eq!(r.as_str(), "AA1");
1483    }
1484
1485    #[test]
1486    fn test_compact_cell_ref_from_coordinates_max() {
1487        let r = CompactCellRef::from_coordinates(16384, 1048576);
1488        assert_eq!(r.as_str(), "XFD1048576");
1489    }
1490
1491    #[test]
1492    fn test_compact_cell_ref_from_coordinates_zz1() {
1493        let r = CompactCellRef::from_coordinates(702, 1);
1494        assert_eq!(r.as_str(), "ZZ1");
1495    }
1496
1497    #[test]
1498    fn test_compact_cell_ref_from_coordinates_roundtrip() {
1499        fn col_to_name(mut col: u32) -> String {
1500            let mut result = String::new();
1501            while col > 0 {
1502                col -= 1;
1503                result.insert(0, (b'A' + (col % 26) as u8) as char);
1504                col /= 26;
1505            }
1506            result
1507        }
1508        for c in [1, 2, 26, 27, 52, 100, 256, 702, 703, 16384] {
1509            let r = CompactCellRef::from_coordinates(c, 1);
1510            let expected = format!("{}1", col_to_name(c));
1511            assert_eq!(r.as_str(), expected, "mismatch for col={c}");
1512        }
1513    }
1514}