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        struct CompactCellRefVisitor;
479
480        impl serde::de::Visitor<'_> for CompactCellRefVisitor {
481            type Value = CompactCellRef;
482
483            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
484                formatter.write_str("a cell reference string (e.g. \"A1\")")
485            }
486
487            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<CompactCellRef, E> {
488                if v.len() > 10 {
489                    return Err(E::custom(format!(
490                        "cell reference too long ({} bytes): {v}",
491                        v.len()
492                    )));
493                }
494                Ok(CompactCellRef::new(v))
495            }
496
497            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<CompactCellRef, E> {
498                self.visit_str(&v)
499            }
500        }
501
502        deserializer.deserialize_str(CompactCellRefVisitor)
503    }
504}
505
506/// A single cell.
507#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
508pub struct Cell {
509    /// Cell reference (e.g., "A1").
510    #[serde(rename = "@r")]
511    pub r: CompactCellRef,
512
513    /// Cached 1-based column number parsed from `r`. Populated at load time
514    /// or at creation time to avoid repeated string parsing.
515    #[serde(skip)]
516    pub col: u32,
517
518    /// Style index.
519    #[serde(rename = "@s", skip_serializing_if = "Option::is_none")]
520    pub s: Option<u32>,
521
522    /// Cell data type.
523    #[serde(rename = "@t", default, skip_serializing_if = "CellTypeTag::is_none")]
524    pub t: CellTypeTag,
525
526    /// Cell value.
527    #[serde(rename = "v", skip_serializing_if = "Option::is_none")]
528    pub v: Option<String>,
529
530    /// Cell formula.
531    #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
532    pub f: Option<Box<CellFormula>>,
533
534    /// Inline string.
535    #[serde(rename = "is", skip_serializing_if = "Option::is_none")]
536    pub is: Option<Box<InlineString>>,
537}
538
539/// Cell data type tag, replacing `Option<String>` for zero-allocation matching.
540#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
541pub enum CellTypeTag {
542    /// No type attribute (typically means Number).
543    #[default]
544    None,
545    /// Shared string ("s").
546    SharedString,
547    /// Explicit number ("n").
548    Number,
549    /// Boolean ("b").
550    Boolean,
551    /// Error ("e").
552    Error,
553    /// Inline string ("inlineStr").
554    InlineString,
555    /// Formula string result ("str").
556    FormulaString,
557    /// Date ("d").
558    Date,
559}
560
561impl CellTypeTag {
562    /// Returns `true` when the tag is `None`, used for `skip_serializing_if`.
563    pub fn is_none(&self) -> bool {
564        matches!(self, CellTypeTag::None)
565    }
566
567    /// Returns the XML string representation, or `None` for the default variant.
568    pub fn as_str(&self) -> Option<&'static str> {
569        match self {
570            CellTypeTag::None => Option::None,
571            CellTypeTag::SharedString => Some("s"),
572            CellTypeTag::Number => Some("n"),
573            CellTypeTag::Boolean => Some("b"),
574            CellTypeTag::Error => Some("e"),
575            CellTypeTag::InlineString => Some("inlineStr"),
576            CellTypeTag::FormulaString => Some("str"),
577            CellTypeTag::Date => Some("d"),
578        }
579    }
580}
581
582impl Serialize for CellTypeTag {
583    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
584        match self.as_str() {
585            Some(s) => serializer.serialize_str(s),
586            Option::None => serializer.serialize_none(),
587        }
588    }
589}
590
591impl<'de> Deserialize<'de> for CellTypeTag {
592    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
593        struct CellTypeTagVisitor;
594
595        impl serde::de::Visitor<'_> for CellTypeTagVisitor {
596            type Value = CellTypeTag;
597
598            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
599                formatter.write_str("a cell type string (e.g. \"s\", \"n\", \"b\")")
600            }
601
602            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<CellTypeTag, E> {
603                Ok(match v {
604                    "s" => CellTypeTag::SharedString,
605                    "n" => CellTypeTag::Number,
606                    "b" => CellTypeTag::Boolean,
607                    "e" => CellTypeTag::Error,
608                    "inlineStr" => CellTypeTag::InlineString,
609                    "str" => CellTypeTag::FormulaString,
610                    "d" => CellTypeTag::Date,
611                    _ => CellTypeTag::None,
612                })
613            }
614
615            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<CellTypeTag, E> {
616                self.visit_str(&v)
617            }
618        }
619
620        deserializer.deserialize_str(CellTypeTagVisitor)
621    }
622}
623
624/// Cell formula.
625#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
626pub struct CellFormula {
627    #[serde(rename = "@t", skip_serializing_if = "Option::is_none")]
628    pub t: Option<String>,
629
630    #[serde(rename = "@ref", skip_serializing_if = "Option::is_none")]
631    pub reference: Option<String>,
632
633    #[serde(rename = "@si", skip_serializing_if = "Option::is_none")]
634    pub si: Option<u32>,
635
636    #[serde(rename = "$value", skip_serializing_if = "Option::is_none")]
637    pub value: Option<String>,
638}
639
640/// Inline string within a cell.
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
642pub struct InlineString {
643    #[serde(rename = "t", skip_serializing_if = "Option::is_none")]
644    pub t: Option<String>,
645}
646
647/// Auto filter.
648#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
649pub struct AutoFilter {
650    #[serde(rename = "@ref")]
651    pub reference: String,
652}
653
654/// Data validations container.
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
656pub struct DataValidations {
657    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
658    pub count: Option<u32>,
659
660    #[serde(
661        rename = "@disablePrompts",
662        skip_serializing_if = "Option::is_none",
663        default
664    )]
665    pub disable_prompts: Option<bool>,
666
667    #[serde(rename = "@xWindow", skip_serializing_if = "Option::is_none", default)]
668    pub x_window: Option<u32>,
669
670    #[serde(rename = "@yWindow", skip_serializing_if = "Option::is_none", default)]
671    pub y_window: Option<u32>,
672
673    #[serde(rename = "dataValidation", default)]
674    pub data_validations: Vec<DataValidation>,
675}
676
677/// Individual data validation rule.
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
679pub struct DataValidation {
680    #[serde(rename = "@type", skip_serializing_if = "Option::is_none")]
681    pub validation_type: Option<String>,
682
683    #[serde(rename = "@operator", skip_serializing_if = "Option::is_none")]
684    pub operator: Option<String>,
685
686    #[serde(rename = "@allowBlank", skip_serializing_if = "Option::is_none")]
687    pub allow_blank: Option<bool>,
688
689    #[serde(
690        rename = "@showDropDown",
691        skip_serializing_if = "Option::is_none",
692        default
693    )]
694    pub show_drop_down: Option<bool>,
695
696    #[serde(rename = "@showInputMessage", skip_serializing_if = "Option::is_none")]
697    pub show_input_message: Option<bool>,
698
699    #[serde(rename = "@showErrorMessage", skip_serializing_if = "Option::is_none")]
700    pub show_error_message: Option<bool>,
701
702    #[serde(rename = "@errorStyle", skip_serializing_if = "Option::is_none")]
703    pub error_style: Option<String>,
704
705    #[serde(rename = "@imeMode", skip_serializing_if = "Option::is_none", default)]
706    pub ime_mode: Option<String>,
707
708    #[serde(rename = "@errorTitle", skip_serializing_if = "Option::is_none")]
709    pub error_title: Option<String>,
710
711    #[serde(rename = "@error", skip_serializing_if = "Option::is_none")]
712    pub error: Option<String>,
713
714    #[serde(rename = "@promptTitle", skip_serializing_if = "Option::is_none")]
715    pub prompt_title: Option<String>,
716
717    #[serde(rename = "@prompt", skip_serializing_if = "Option::is_none")]
718    pub prompt: Option<String>,
719
720    #[serde(rename = "@sqref")]
721    pub sqref: String,
722
723    #[serde(rename = "formula1", skip_serializing_if = "Option::is_none")]
724    pub formula1: Option<String>,
725
726    #[serde(rename = "formula2", skip_serializing_if = "Option::is_none")]
727    pub formula2: Option<String>,
728}
729
730/// Merge cells container.
731///
732/// `PartialEq` is implemented manually to exclude `cached_coords`, which is a
733/// transient performance cache. Two values with the same `count` and
734/// `merge_cells` are semantically equal regardless of cache state.
735#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct MergeCells {
737    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
738    pub count: Option<u32>,
739
740    #[serde(rename = "mergeCell", default)]
741    pub merge_cells: Vec<MergeCell>,
742
743    /// Cached parsed coordinates `(min_col, min_row, max_col, max_row)` for each
744    /// merge region, kept in parallel with `merge_cells`. Populated lazily on
745    /// first overlap check and maintained during add/remove. Not serialized.
746    #[serde(skip)]
747    pub cached_coords: Vec<(u32, u32, u32, u32)>,
748}
749
750impl PartialEq for MergeCells {
751    fn eq(&self, other: &Self) -> bool {
752        self.count == other.count && self.merge_cells == other.merge_cells
753    }
754}
755
756/// Individual merge cell reference.
757#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
758pub struct MergeCell {
759    #[serde(rename = "@ref")]
760    pub reference: String,
761}
762
763/// Hyperlinks container.
764#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
765pub struct Hyperlinks {
766    #[serde(rename = "hyperlink", default)]
767    pub hyperlinks: Vec<Hyperlink>,
768}
769
770/// Individual hyperlink.
771#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
772pub struct Hyperlink {
773    #[serde(rename = "@ref")]
774    pub reference: String,
775
776    #[serde(
777        rename = "@r:id",
778        alias = "@id",
779        skip_serializing_if = "Option::is_none"
780    )]
781    pub r_id: Option<String>,
782
783    #[serde(rename = "@location", skip_serializing_if = "Option::is_none")]
784    pub location: Option<String>,
785
786    #[serde(rename = "@display", skip_serializing_if = "Option::is_none")]
787    pub display: Option<String>,
788
789    #[serde(rename = "@tooltip", skip_serializing_if = "Option::is_none")]
790    pub tooltip: Option<String>,
791}
792
793/// Page margins.
794#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
795pub struct PageMargins {
796    #[serde(rename = "@left")]
797    pub left: f64,
798
799    #[serde(rename = "@right")]
800    pub right: f64,
801
802    #[serde(rename = "@top")]
803    pub top: f64,
804
805    #[serde(rename = "@bottom")]
806    pub bottom: f64,
807
808    #[serde(rename = "@header")]
809    pub header: f64,
810
811    #[serde(rename = "@footer")]
812    pub footer: f64,
813}
814
815/// Page setup.
816#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
817pub struct PageSetup {
818    #[serde(rename = "@paperSize", skip_serializing_if = "Option::is_none")]
819    pub paper_size: Option<u32>,
820
821    #[serde(rename = "@orientation", skip_serializing_if = "Option::is_none")]
822    pub orientation: Option<String>,
823
824    #[serde(rename = "@scale", skip_serializing_if = "Option::is_none")]
825    pub scale: Option<u32>,
826
827    #[serde(rename = "@fitToWidth", skip_serializing_if = "Option::is_none")]
828    pub fit_to_width: Option<u32>,
829
830    #[serde(rename = "@fitToHeight", skip_serializing_if = "Option::is_none")]
831    pub fit_to_height: Option<u32>,
832
833    #[serde(rename = "@firstPageNumber", skip_serializing_if = "Option::is_none")]
834    pub first_page_number: Option<u32>,
835
836    #[serde(rename = "@horizontalDpi", skip_serializing_if = "Option::is_none")]
837    pub horizontal_dpi: Option<u32>,
838
839    #[serde(rename = "@verticalDpi", skip_serializing_if = "Option::is_none")]
840    pub vertical_dpi: Option<u32>,
841
842    #[serde(
843        rename = "@r:id",
844        alias = "@id",
845        skip_serializing_if = "Option::is_none"
846    )]
847    pub r_id: Option<String>,
848}
849
850/// Header and footer for printing.
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
852pub struct HeaderFooter {
853    #[serde(rename = "oddHeader", skip_serializing_if = "Option::is_none")]
854    pub odd_header: Option<String>,
855
856    #[serde(rename = "oddFooter", skip_serializing_if = "Option::is_none")]
857    pub odd_footer: Option<String>,
858}
859
860/// Print options.
861#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
862pub struct PrintOptions {
863    #[serde(rename = "@gridLines", skip_serializing_if = "Option::is_none")]
864    pub grid_lines: Option<bool>,
865
866    #[serde(rename = "@headings", skip_serializing_if = "Option::is_none")]
867    pub headings: Option<bool>,
868
869    #[serde(
870        rename = "@horizontalCentered",
871        skip_serializing_if = "Option::is_none"
872    )]
873    pub horizontal_centered: Option<bool>,
874
875    #[serde(rename = "@verticalCentered", skip_serializing_if = "Option::is_none")]
876    pub vertical_centered: Option<bool>,
877}
878
879/// Row page breaks container.
880#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
881pub struct RowBreaks {
882    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
883    pub count: Option<u32>,
884
885    #[serde(rename = "@manualBreakCount", skip_serializing_if = "Option::is_none")]
886    pub manual_break_count: Option<u32>,
887
888    #[serde(rename = "brk", default)]
889    pub brk: Vec<Break>,
890}
891
892/// Individual page break entry.
893#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
894pub struct Break {
895    #[serde(rename = "@id")]
896    pub id: u32,
897
898    #[serde(rename = "@max", skip_serializing_if = "Option::is_none")]
899    pub max: Option<u32>,
900
901    #[serde(rename = "@man", skip_serializing_if = "Option::is_none")]
902    pub man: Option<bool>,
903}
904
905/// Drawing reference.
906#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
907pub struct DrawingRef {
908    #[serde(rename = "@r:id", alias = "@id")]
909    pub r_id: String,
910}
911
912/// Legacy drawing reference (VML).
913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
914pub struct LegacyDrawingRef {
915    #[serde(rename = "@r:id", alias = "@id")]
916    pub r_id: String,
917}
918
919/// Table parts container.
920#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
921pub struct TableParts {
922    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
923    pub count: Option<u32>,
924
925    #[serde(rename = "tablePart", default)]
926    pub table_parts: Vec<TablePart>,
927}
928
929/// Individual table part reference.
930#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
931pub struct TablePart {
932    #[serde(rename = "@r:id", alias = "@id")]
933    pub r_id: String,
934}
935
936/// Conditional formatting container.
937#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
938pub struct ConditionalFormatting {
939    #[serde(rename = "@sqref")]
940    pub sqref: String,
941
942    #[serde(rename = "cfRule", default)]
943    pub cf_rules: Vec<CfRule>,
944}
945
946/// Conditional formatting rule.
947#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
948pub struct CfRule {
949    #[serde(rename = "@type")]
950    pub rule_type: String,
951
952    #[serde(rename = "@dxfId", skip_serializing_if = "Option::is_none")]
953    pub dxf_id: Option<u32>,
954
955    #[serde(rename = "@priority")]
956    pub priority: u32,
957
958    #[serde(rename = "@operator", skip_serializing_if = "Option::is_none")]
959    pub operator: Option<String>,
960
961    #[serde(rename = "@text", skip_serializing_if = "Option::is_none")]
962    pub text: Option<String>,
963
964    #[serde(rename = "@stopIfTrue", skip_serializing_if = "Option::is_none")]
965    pub stop_if_true: Option<bool>,
966
967    #[serde(rename = "@aboveAverage", skip_serializing_if = "Option::is_none")]
968    pub above_average: Option<bool>,
969
970    #[serde(rename = "@equalAverage", skip_serializing_if = "Option::is_none")]
971    pub equal_average: Option<bool>,
972
973    #[serde(rename = "@percent", skip_serializing_if = "Option::is_none")]
974    pub percent: Option<bool>,
975
976    #[serde(rename = "@rank", skip_serializing_if = "Option::is_none")]
977    pub rank: Option<u32>,
978
979    #[serde(rename = "@bottom", skip_serializing_if = "Option::is_none")]
980    pub bottom: Option<bool>,
981
982    #[serde(rename = "formula", default, skip_serializing_if = "Vec::is_empty")]
983    pub formulas: Vec<String>,
984
985    #[serde(rename = "colorScale", skip_serializing_if = "Option::is_none")]
986    pub color_scale: Option<CfColorScale>,
987
988    #[serde(rename = "dataBar", skip_serializing_if = "Option::is_none")]
989    pub data_bar: Option<CfDataBar>,
990
991    #[serde(rename = "iconSet", skip_serializing_if = "Option::is_none")]
992    pub icon_set: Option<CfIconSet>,
993}
994
995/// Color scale definition for conditional formatting.
996#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
997pub struct CfColorScale {
998    #[serde(rename = "cfvo", default)]
999    pub cfvos: Vec<CfVo>,
1000
1001    #[serde(rename = "color", default)]
1002    pub colors: Vec<CfColor>,
1003}
1004
1005/// Data bar definition for conditional formatting.
1006#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1007pub struct CfDataBar {
1008    #[serde(rename = "@showValue", skip_serializing_if = "Option::is_none")]
1009    pub show_value: Option<bool>,
1010
1011    #[serde(rename = "cfvo", default)]
1012    pub cfvos: Vec<CfVo>,
1013
1014    #[serde(rename = "color", skip_serializing_if = "Option::is_none")]
1015    pub color: Option<CfColor>,
1016}
1017
1018/// Icon set definition for conditional formatting.
1019#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1020pub struct CfIconSet {
1021    #[serde(rename = "@iconSet", skip_serializing_if = "Option::is_none")]
1022    pub icon_set: Option<String>,
1023
1024    #[serde(rename = "cfvo", default)]
1025    pub cfvos: Vec<CfVo>,
1026}
1027
1028/// Conditional formatting value object.
1029#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1030pub struct CfVo {
1031    #[serde(rename = "@type")]
1032    pub value_type: String,
1033
1034    #[serde(rename = "@val", skip_serializing_if = "Option::is_none")]
1035    pub val: Option<String>,
1036}
1037
1038/// Color reference for conditional formatting.
1039#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1040pub struct CfColor {
1041    #[serde(rename = "@rgb", skip_serializing_if = "Option::is_none")]
1042    pub rgb: Option<String>,
1043
1044    #[serde(rename = "@theme", skip_serializing_if = "Option::is_none")]
1045    pub theme: Option<u32>,
1046
1047    #[serde(rename = "@tint", skip_serializing_if = "Option::is_none")]
1048    pub tint: Option<f64>,
1049}
1050
1051impl Default for WorksheetXml {
1052    fn default() -> Self {
1053        Self {
1054            xmlns: namespaces::SPREADSHEET_ML.to_string(),
1055            xmlns_r: namespaces::RELATIONSHIPS.to_string(),
1056            sheet_pr: None,
1057            dimension: None,
1058            sheet_views: None,
1059            sheet_format_pr: None,
1060            cols: None,
1061            sheet_data: SheetData { rows: vec![] },
1062            sheet_protection: None,
1063            auto_filter: None,
1064            merge_cells: None,
1065            conditional_formatting: vec![],
1066            data_validations: None,
1067            hyperlinks: None,
1068            print_options: None,
1069            page_margins: None,
1070            page_setup: None,
1071            header_footer: None,
1072            row_breaks: None,
1073            drawing: None,
1074            legacy_drawing: None,
1075            table_parts: None,
1076        }
1077    }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082    use super::*;
1083
1084    #[test]
1085    fn test_worksheet_default() {
1086        let ws = WorksheetXml::default();
1087        assert_eq!(ws.xmlns, namespaces::SPREADSHEET_ML);
1088        assert_eq!(ws.xmlns_r, namespaces::RELATIONSHIPS);
1089        assert!(ws.sheet_data.rows.is_empty());
1090        assert!(ws.dimension.is_none());
1091        assert!(ws.sheet_views.is_none());
1092        assert!(ws.cols.is_none());
1093        assert!(ws.merge_cells.is_none());
1094        assert!(ws.page_margins.is_none());
1095        assert!(ws.sheet_pr.is_none());
1096        assert!(ws.sheet_protection.is_none());
1097    }
1098
1099    #[test]
1100    fn test_worksheet_roundtrip() {
1101        let ws = WorksheetXml::default();
1102        let xml = quick_xml::se::to_string(&ws).unwrap();
1103        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1104        assert_eq!(ws.xmlns, parsed.xmlns);
1105        assert_eq!(ws.xmlns_r, parsed.xmlns_r);
1106        assert_eq!(ws.sheet_data.rows.len(), parsed.sheet_data.rows.len());
1107    }
1108
1109    #[test]
1110    fn test_worksheet_with_data() {
1111        let ws = WorksheetXml {
1112            sheet_data: SheetData {
1113                rows: vec![Row {
1114                    r: 1,
1115                    spans: Some("1:3".to_string()),
1116                    s: None,
1117                    custom_format: None,
1118                    ht: None,
1119                    hidden: None,
1120                    custom_height: None,
1121                    outline_level: None,
1122                    cells: vec![
1123                        Cell {
1124                            r: CompactCellRef::new("A1"),
1125                            col: 1,
1126                            s: None,
1127                            t: CellTypeTag::SharedString,
1128                            v: Some("0".to_string()),
1129                            f: None,
1130                            is: None,
1131                        },
1132                        Cell {
1133                            r: CompactCellRef::new("B1"),
1134                            col: 2,
1135                            s: None,
1136                            t: CellTypeTag::None,
1137                            v: Some("42".to_string()),
1138                            f: None,
1139                            is: None,
1140                        },
1141                    ],
1142                }],
1143            },
1144            ..WorksheetXml::default()
1145        };
1146
1147        let xml = quick_xml::se::to_string(&ws).unwrap();
1148        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1149        assert_eq!(parsed.sheet_data.rows.len(), 1);
1150        assert_eq!(parsed.sheet_data.rows[0].r, 1);
1151        assert_eq!(parsed.sheet_data.rows[0].cells.len(), 2);
1152        assert_eq!(parsed.sheet_data.rows[0].cells[0].r, "A1");
1153        assert_eq!(
1154            parsed.sheet_data.rows[0].cells[0].t,
1155            CellTypeTag::SharedString
1156        );
1157        assert_eq!(parsed.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1158        assert_eq!(parsed.sheet_data.rows[0].cells[1].r, "B1");
1159        assert_eq!(parsed.sheet_data.rows[0].cells[1].v, Some("42".to_string()));
1160    }
1161
1162    #[test]
1163    fn test_cell_with_formula() {
1164        let cell = Cell {
1165            r: CompactCellRef::new("C1"),
1166            col: 3,
1167            s: None,
1168            t: CellTypeTag::None,
1169            v: Some("84".to_string()),
1170            f: Some(Box::new(CellFormula {
1171                t: None,
1172                reference: None,
1173                si: None,
1174                value: Some("A1+B1".to_string()),
1175            })),
1176            is: None,
1177        };
1178        let xml = quick_xml::se::to_string(&cell).unwrap();
1179        assert!(xml.contains("A1+B1"));
1180        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1181        assert!(parsed.f.is_some());
1182        assert_eq!(parsed.f.unwrap().value, Some("A1+B1".to_string()));
1183    }
1184
1185    #[test]
1186    fn test_cell_with_inline_string() {
1187        let cell = Cell {
1188            r: CompactCellRef::new("A1"),
1189            col: 1,
1190            s: None,
1191            t: CellTypeTag::InlineString,
1192            v: None,
1193            f: None,
1194            is: Some(Box::new(InlineString {
1195                t: Some("Hello World".to_string()),
1196            })),
1197        };
1198        let xml = quick_xml::se::to_string(&cell).unwrap();
1199        assert!(xml.contains("Hello World"));
1200        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1201        assert_eq!(parsed.t, CellTypeTag::InlineString);
1202        assert!(parsed.is.is_some());
1203        assert_eq!(parsed.is.unwrap().t, Some("Hello World".to_string()));
1204    }
1205
1206    #[test]
1207    fn test_parse_real_excel_worksheet() {
1208        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1209<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
1210  <dimension ref="A1:B2"/>
1211  <sheetData>
1212    <row r="1" spans="1:2">
1213      <c r="A1" t="s"><v>0</v></c>
1214      <c r="B1" t="s"><v>1</v></c>
1215    </row>
1216    <row r="2" spans="1:2">
1217      <c r="A2"><v>100</v></c>
1218      <c r="B2"><v>200</v></c>
1219    </row>
1220  </sheetData>
1221</worksheet>"#;
1222
1223        let parsed: WorksheetXml = quick_xml::de::from_str(xml).unwrap();
1224        assert_eq!(parsed.dimension.as_ref().unwrap().reference, "A1:B2");
1225        assert_eq!(parsed.sheet_data.rows.len(), 2);
1226        assert_eq!(parsed.sheet_data.rows[0].cells.len(), 2);
1227        assert_eq!(parsed.sheet_data.rows[0].cells[0].r, "A1");
1228        assert_eq!(
1229            parsed.sheet_data.rows[0].cells[0].t,
1230            CellTypeTag::SharedString
1231        );
1232        assert_eq!(parsed.sheet_data.rows[0].cells[0].v, Some("0".to_string()));
1233        assert_eq!(parsed.sheet_data.rows[1].cells[0].r, "A2");
1234        assert_eq!(
1235            parsed.sheet_data.rows[1].cells[0].v,
1236            Some("100".to_string())
1237        );
1238    }
1239
1240    #[test]
1241    fn test_worksheet_with_merge_cells() {
1242        let ws = WorksheetXml {
1243            merge_cells: Some(MergeCells {
1244                count: Some(1),
1245                merge_cells: vec![MergeCell {
1246                    reference: "A1:B2".to_string(),
1247                }],
1248                cached_coords: Vec::new(),
1249            }),
1250            ..WorksheetXml::default()
1251        };
1252        let xml = quick_xml::se::to_string(&ws).unwrap();
1253        assert!(xml.contains("mergeCells"));
1254        assert!(xml.contains("A1:B2"));
1255        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1256        assert!(parsed.merge_cells.is_some());
1257        assert_eq!(parsed.merge_cells.as_ref().unwrap().merge_cells.len(), 1);
1258    }
1259
1260    #[test]
1261    fn test_empty_sheet_data_serialization() {
1262        let sd = SheetData { rows: vec![] };
1263        let xml = quick_xml::se::to_string(&sd).unwrap();
1264        // Empty SheetData should still be serializable
1265        let parsed: SheetData = quick_xml::de::from_str(&xml).unwrap();
1266        assert!(parsed.rows.is_empty());
1267    }
1268
1269    #[test]
1270    fn test_row_optional_fields_not_serialized() {
1271        let row = Row {
1272            r: 1,
1273            spans: None,
1274            s: None,
1275            custom_format: None,
1276            ht: None,
1277            hidden: None,
1278            custom_height: None,
1279            outline_level: None,
1280            cells: vec![],
1281        };
1282        let xml = quick_xml::se::to_string(&row).unwrap();
1283        assert!(!xml.contains("spans"));
1284        assert!(!xml.contains("ht"));
1285        assert!(!xml.contains("hidden"));
1286    }
1287
1288    #[test]
1289    fn test_cell_type_tag_as_str() {
1290        assert_eq!(CellTypeTag::Boolean.as_str(), Some("b"));
1291        assert_eq!(CellTypeTag::Date.as_str(), Some("d"));
1292        assert_eq!(CellTypeTag::Error.as_str(), Some("e"));
1293        assert_eq!(CellTypeTag::InlineString.as_str(), Some("inlineStr"));
1294        assert_eq!(CellTypeTag::Number.as_str(), Some("n"));
1295        assert_eq!(CellTypeTag::SharedString.as_str(), Some("s"));
1296        assert_eq!(CellTypeTag::FormulaString.as_str(), Some("str"));
1297        assert_eq!(CellTypeTag::None.as_str(), Option::None);
1298    }
1299
1300    #[test]
1301    fn test_cell_type_tag_default_is_none() {
1302        assert_eq!(CellTypeTag::default(), CellTypeTag::None);
1303        assert!(CellTypeTag::None.is_none());
1304        assert!(!CellTypeTag::SharedString.is_none());
1305    }
1306
1307    #[test]
1308    fn test_cell_type_tag_serde_round_trip() {
1309        let variants = [
1310            (CellTypeTag::SharedString, "s"),
1311            (CellTypeTag::Number, "n"),
1312            (CellTypeTag::Boolean, "b"),
1313            (CellTypeTag::Error, "e"),
1314            (CellTypeTag::InlineString, "inlineStr"),
1315            (CellTypeTag::FormulaString, "str"),
1316            (CellTypeTag::Date, "d"),
1317        ];
1318        for (tag, expected_str) in &variants {
1319            let cell = Cell {
1320                r: CompactCellRef::new("A1"),
1321                col: 1,
1322                s: None,
1323                t: *tag,
1324                v: Some("0".to_string()),
1325                f: None,
1326                is: None,
1327            };
1328            let xml = quick_xml::se::to_string(&cell).unwrap();
1329            assert!(
1330                xml.contains(&format!("t=\"{expected_str}\"")),
1331                "expected t=\"{expected_str}\" in: {xml}"
1332            );
1333            let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1334            assert_eq!(parsed.t, *tag);
1335        }
1336
1337        let cell_none = Cell {
1338            r: CompactCellRef::new("A1"),
1339            col: 1,
1340            s: None,
1341            t: CellTypeTag::None,
1342            v: Some("42".to_string()),
1343            f: None,
1344            is: None,
1345        };
1346        let xml = quick_xml::se::to_string(&cell_none).unwrap();
1347        assert!(
1348            !xml.contains("t="),
1349            "None variant should not emit t attribute: {xml}"
1350        );
1351        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1352        assert_eq!(parsed.t, CellTypeTag::None);
1353    }
1354
1355    #[test]
1356    fn test_worksheet_with_cols() {
1357        let ws = WorksheetXml {
1358            cols: Some(Cols {
1359                cols: vec![Col {
1360                    min: 1,
1361                    max: 1,
1362                    width: Some(15.0),
1363                    style: None,
1364                    hidden: None,
1365                    custom_width: Some(true),
1366                    outline_level: None,
1367                }],
1368            }),
1369            ..WorksheetXml::default()
1370        };
1371        let xml = quick_xml::se::to_string(&ws).unwrap();
1372        let parsed: WorksheetXml = quick_xml::de::from_str(&xml).unwrap();
1373        assert!(parsed.cols.is_some());
1374        let cols = parsed.cols.unwrap();
1375        assert_eq!(cols.cols.len(), 1);
1376        assert_eq!(cols.cols[0].min, 1);
1377        assert_eq!(cols.cols[0].width, Some(15.0));
1378        assert_eq!(cols.cols[0].custom_width, Some(true));
1379    }
1380
1381    #[test]
1382    fn test_sheet_protection_roundtrip() {
1383        let prot = SheetProtection {
1384            password: Some("ABCD".to_string()),
1385            sheet: Some(true),
1386            objects: Some(true),
1387            scenarios: Some(true),
1388            format_cells: Some(false),
1389            ..SheetProtection::default()
1390        };
1391        let xml = quick_xml::se::to_string(&prot).unwrap();
1392        let parsed: SheetProtection = quick_xml::de::from_str(&xml).unwrap();
1393        assert_eq!(parsed.password, Some("ABCD".to_string()));
1394        assert_eq!(parsed.sheet, Some(true));
1395        assert_eq!(parsed.objects, Some(true));
1396        assert_eq!(parsed.scenarios, Some(true));
1397        assert_eq!(parsed.format_cells, Some(false));
1398        assert!(parsed.sort.is_none());
1399    }
1400
1401    #[test]
1402    fn test_sheet_pr_roundtrip() {
1403        let pr = SheetPr {
1404            code_name: Some("Sheet1".to_string()),
1405            tab_color: Some(TabColor {
1406                rgb: Some("FF0000".to_string()),
1407                theme: None,
1408                indexed: None,
1409            }),
1410            ..SheetPr::default()
1411        };
1412        let xml = quick_xml::se::to_string(&pr).unwrap();
1413        let parsed: SheetPr = quick_xml::de::from_str(&xml).unwrap();
1414        assert_eq!(parsed.code_name, Some("Sheet1".to_string()));
1415        assert!(parsed.tab_color.is_some());
1416        assert_eq!(parsed.tab_color.unwrap().rgb, Some("FF0000".to_string()));
1417    }
1418
1419    #[test]
1420    fn test_sheet_format_pr_extended_fields() {
1421        let fmt = SheetFormatPr {
1422            default_row_height: 15.0,
1423            default_col_width: Some(10.0),
1424            custom_height: Some(true),
1425            outline_level_row: Some(2),
1426            outline_level_col: Some(1),
1427        };
1428        let xml = quick_xml::se::to_string(&fmt).unwrap();
1429        let parsed: SheetFormatPr = quick_xml::de::from_str(&xml).unwrap();
1430        assert_eq!(parsed.default_row_height, 15.0);
1431        assert_eq!(parsed.default_col_width, Some(10.0));
1432        assert_eq!(parsed.custom_height, Some(true));
1433        assert_eq!(parsed.outline_level_row, Some(2));
1434        assert_eq!(parsed.outline_level_col, Some(1));
1435    }
1436
1437    #[test]
1438    fn test_compact_cell_ref_basic() {
1439        let r = CompactCellRef::new("A1");
1440        assert_eq!(r.as_str(), "A1");
1441        assert_eq!(r.len, 2);
1442    }
1443
1444    #[test]
1445    fn test_compact_cell_ref_max_length() {
1446        let r = CompactCellRef::new("XFD1048576");
1447        assert_eq!(r.as_str(), "XFD1048576");
1448        assert_eq!(r.len, 10);
1449    }
1450
1451    #[test]
1452    fn test_compact_cell_ref_various_lengths() {
1453        for s in &["A1", "B5", "Z99", "AA100", "XFD1048576"] {
1454            let r = CompactCellRef::new(s);
1455            assert_eq!(r.as_str(), *s);
1456        }
1457    }
1458
1459    #[test]
1460    fn test_compact_cell_ref_display() {
1461        let r = CompactCellRef::new("C3");
1462        assert_eq!(format!("{r}"), "C3");
1463    }
1464
1465    #[test]
1466    fn test_compact_cell_ref_debug() {
1467        let r = CompactCellRef::new("C3");
1468        let dbg = format!("{r:?}");
1469        assert!(dbg.contains("CompactCellRef"));
1470        assert!(dbg.contains("C3"));
1471    }
1472
1473    #[test]
1474    fn test_compact_cell_ref_default() {
1475        let r = CompactCellRef::default();
1476        assert_eq!(r.as_str(), "");
1477        assert_eq!(r.len, 0);
1478    }
1479
1480    #[test]
1481    fn test_compact_cell_ref_from_str() {
1482        let r: CompactCellRef = "D4".into();
1483        assert_eq!(r.as_str(), "D4");
1484    }
1485
1486    #[test]
1487    fn test_compact_cell_ref_from_string() {
1488        let r: CompactCellRef = String::from("E5").into();
1489        assert_eq!(r.as_str(), "E5");
1490    }
1491
1492    #[test]
1493    fn test_compact_cell_ref_as_ref_str() {
1494        let r = CompactCellRef::new("F6");
1495        let s: &str = r.as_ref();
1496        assert_eq!(s, "F6");
1497    }
1498
1499    #[test]
1500    fn test_compact_cell_ref_partial_eq_str() {
1501        let r = CompactCellRef::new("G7");
1502        assert_eq!(r, "G7");
1503        assert!(r == "G7");
1504        assert!(r != "H8");
1505    }
1506
1507    #[test]
1508    fn test_compact_cell_ref_copy() {
1509        let r1 = CompactCellRef::new("A1");
1510        let r2 = r1;
1511        assert_eq!(r1.as_str(), "A1");
1512        assert_eq!(r2.as_str(), "A1");
1513    }
1514
1515    #[test]
1516    fn test_compact_cell_ref_serde_roundtrip() {
1517        let cell = Cell {
1518            r: CompactCellRef::new("XFD1048576"),
1519            col: 16384,
1520            s: None,
1521            t: CellTypeTag::None,
1522            v: Some("42".to_string()),
1523            f: None,
1524            is: None,
1525        };
1526        let xml = quick_xml::se::to_string(&cell).unwrap();
1527        assert!(xml.contains("XFD1048576"));
1528        let parsed: Cell = quick_xml::de::from_str(&xml).unwrap();
1529        assert_eq!(parsed.r, "XFD1048576");
1530        assert_eq!(parsed.v, Some("42".to_string()));
1531    }
1532
1533    #[test]
1534    #[should_panic(expected = "cell reference too long")]
1535    fn test_compact_cell_ref_panics_on_overflow() {
1536        CompactCellRef::new("ABCDEFGHIJK");
1537    }
1538
1539    #[test]
1540    fn test_compact_cell_ref_from_coordinates_a1() {
1541        let r = CompactCellRef::from_coordinates(1, 1);
1542        assert_eq!(r.as_str(), "A1");
1543    }
1544
1545    #[test]
1546    fn test_compact_cell_ref_from_coordinates_z26() {
1547        let r = CompactCellRef::from_coordinates(26, 26);
1548        assert_eq!(r.as_str(), "Z26");
1549    }
1550
1551    #[test]
1552    fn test_compact_cell_ref_from_coordinates_aa1() {
1553        let r = CompactCellRef::from_coordinates(27, 1);
1554        assert_eq!(r.as_str(), "AA1");
1555    }
1556
1557    #[test]
1558    fn test_compact_cell_ref_from_coordinates_max() {
1559        let r = CompactCellRef::from_coordinates(16384, 1048576);
1560        assert_eq!(r.as_str(), "XFD1048576");
1561    }
1562
1563    #[test]
1564    fn test_compact_cell_ref_from_coordinates_zz1() {
1565        let r = CompactCellRef::from_coordinates(702, 1);
1566        assert_eq!(r.as_str(), "ZZ1");
1567    }
1568
1569    #[test]
1570    fn test_compact_cell_ref_from_coordinates_roundtrip() {
1571        fn col_to_name(mut col: u32) -> String {
1572            let mut result = String::new();
1573            while col > 0 {
1574                col -= 1;
1575                result.insert(0, (b'A' + (col % 26) as u8) as char);
1576                col /= 26;
1577            }
1578            result
1579        }
1580        for c in [1, 2, 26, 27, 52, 100, 256, 702, 703, 16384] {
1581            let r = CompactCellRef::from_coordinates(c, 1);
1582            let expected = format!("{}1", col_to_name(c));
1583            assert_eq!(r.as_str(), expected, "mismatch for col={c}");
1584        }
1585    }
1586}