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