Skip to main content

ooxml_sml/
ext.rs

1//! Extension traits for generated OOXML types.
2//!
3//! This module provides convenience methods for the generated types via extension traits.
4//! See ADR-003 for the architectural rationale.
5//!
6//! # Design
7//!
8//! Extension traits are split into two categories:
9//!
10//! - **Pure traits** (`CellExt`, `RowExt`): Methods that don't need external context
11//! - **Resolve traits** (`CellResolveExt`): Methods that need `ResolveContext` for
12//!   shared strings, styles, etc.
13//!
14//! # Example
15//!
16//! ```ignore
17//! use ooxml_sml::ext::{CellExt, CellResolveExt, ResolveContext};
18//! use ooxml_sml::types::Cell;
19//!
20//! let cell: &Cell = /* ... */;
21//!
22//! // Pure methods - no context needed
23//! let col = cell.column_number();
24//! let row = cell.row_number();
25//!
26//! // Resolved methods - context required
27//! let ctx = ResolveContext::new(shared_strings, stylesheet);
28//! let value = cell.value_as_string(&ctx);
29//! ```
30
31use crate::parsers::{FromXml, ParseError};
32use crate::types::{Cell, CellType, Row, SheetData, Worksheet};
33use quick_xml::Reader;
34use quick_xml::events::Event;
35use std::io::Cursor;
36
37/// Resolved cell value (typed).
38#[derive(Debug, Clone, PartialEq)]
39pub enum CellValue {
40    /// Empty cell
41    Empty,
42    /// String value (from shared strings or inline)
43    String(String),
44    /// Numeric value
45    Number(f64),
46    /// Boolean value
47    Boolean(bool),
48    /// Error value (e.g., "#REF!", "#VALUE!")
49    Error(String),
50}
51
52impl CellValue {
53    /// Check if the value is empty.
54    pub fn is_empty(&self) -> bool {
55        matches!(self, CellValue::Empty)
56    }
57
58    /// Get as string for display.
59    pub fn to_display_string(&self) -> String {
60        match self {
61            CellValue::Empty => String::new(),
62            CellValue::String(s) => s.clone(),
63            CellValue::Number(n) => n.to_string(),
64            CellValue::Boolean(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
65            CellValue::Error(e) => e.clone(),
66        }
67    }
68
69    /// Try to get as number.
70    pub fn as_number(&self) -> Option<f64> {
71        match self {
72            CellValue::Number(n) => Some(*n),
73            CellValue::String(s) => s.parse().ok(),
74            _ => None,
75        }
76    }
77
78    /// Try to get as boolean.
79    pub fn as_bool(&self) -> Option<bool> {
80        match self {
81            CellValue::Boolean(b) => Some(*b),
82            CellValue::Number(n) => Some(*n != 0.0),
83            CellValue::String(s) => match s.to_lowercase().as_str() {
84                "true" | "1" => Some(true),
85                "false" | "0" => Some(false),
86                _ => None,
87            },
88            _ => None,
89        }
90    }
91}
92
93/// Context for resolving cell values.
94///
95/// Contains shared strings table and stylesheet needed to convert
96/// raw XML values into typed `CellValue`s.
97#[derive(Debug, Clone, Default)]
98pub struct ResolveContext {
99    /// Shared string table (index -> string)
100    pub shared_strings: Vec<String>,
101    // Future: stylesheet, themes, etc.
102}
103
104impl ResolveContext {
105    /// Create a new resolve context.
106    pub fn new(shared_strings: Vec<String>) -> Self {
107        Self { shared_strings }
108    }
109
110    /// Get a shared string by index.
111    pub fn shared_string(&self, index: usize) -> Option<&str> {
112        self.shared_strings.get(index).map(|s| s.as_str())
113    }
114}
115
116// =============================================================================
117// Cell Extension Traits
118// =============================================================================
119
120/// Pure extension methods for `Cell` (no context needed).
121pub trait CellExt {
122    /// Get the cell reference string (e.g., "A1", "B5").
123    fn reference_str(&self) -> Option<&str>;
124
125    /// Parse column number from reference (1-based, e.g., "B5" -> 2).
126    fn column_number(&self) -> Option<u32>;
127
128    /// Parse row number from reference (1-based, e.g., "B5" -> 5).
129    fn row_number(&self) -> Option<u32>;
130
131    /// Check if cell has a formula.
132    fn has_formula(&self) -> bool;
133
134    /// Get the formula text (if any).
135    fn formula_text(&self) -> Option<&str>;
136
137    /// Get the raw value string (before resolution).
138    fn raw_value(&self) -> Option<&str>;
139
140    /// Get the cell type.
141    fn cell_type(&self) -> Option<CellType>;
142
143    /// Check if this is a shared string cell.
144    fn is_shared_string(&self) -> bool;
145
146    /// Check if this is a number cell.
147    fn is_number(&self) -> bool;
148
149    /// Check if this is a boolean cell.
150    fn is_boolean(&self) -> bool;
151
152    /// Check if this is an error cell.
153    fn is_error(&self) -> bool;
154}
155
156impl CellExt for Cell {
157    fn reference_str(&self) -> Option<&str> {
158        self.reference.as_deref()
159    }
160
161    fn column_number(&self) -> Option<u32> {
162        let reference = self.reference.as_ref()?;
163        parse_column(reference)
164    }
165
166    fn row_number(&self) -> Option<u32> {
167        let reference = self.reference.as_ref()?;
168        parse_row(reference)
169    }
170
171    fn has_formula(&self) -> bool {
172        self.formula.is_some()
173    }
174
175    fn formula_text(&self) -> Option<&str> {
176        self.formula.as_ref().and_then(|f| f.text.as_deref())
177    }
178
179    fn raw_value(&self) -> Option<&str> {
180        self.value.as_deref()
181    }
182
183    fn cell_type(&self) -> Option<CellType> {
184        self.cell_type
185    }
186
187    fn is_shared_string(&self) -> bool {
188        matches!(self.cell_type, Some(CellType::SharedString))
189    }
190
191    fn is_number(&self) -> bool {
192        matches!(self.cell_type, Some(CellType::Number)) || self.cell_type.is_none()
193    }
194
195    fn is_boolean(&self) -> bool {
196        matches!(self.cell_type, Some(CellType::Boolean))
197    }
198
199    fn is_error(&self) -> bool {
200        matches!(self.cell_type, Some(CellType::Error))
201    }
202}
203
204/// Extension methods for `Cell` that require a [`ResolveContext`] to dereference
205/// shared strings and interpret cell types.
206pub trait CellResolveExt {
207    /// Resolve the cell value to a typed `CellValue`.
208    fn resolved_value(&self, ctx: &ResolveContext) -> CellValue;
209
210    /// Get value as display string.
211    fn value_as_string(&self, ctx: &ResolveContext) -> String;
212
213    /// Try to get value as number.
214    fn value_as_number(&self, ctx: &ResolveContext) -> Option<f64>;
215
216    /// Try to get value as boolean.
217    fn value_as_bool(&self, ctx: &ResolveContext) -> Option<bool>;
218}
219
220impl CellResolveExt for Cell {
221    fn resolved_value(&self, ctx: &ResolveContext) -> CellValue {
222        let raw = match &self.value {
223            Some(v) => v.as_str(),
224            None => return CellValue::Empty,
225        };
226
227        match &self.cell_type {
228            Some(CellType::SharedString) => {
229                // Shared string - raw value is index
230                if let Ok(idx) = raw.parse::<usize>()
231                    && let Some(s) = ctx.shared_string(idx)
232                {
233                    return CellValue::String(s.to_string());
234                }
235                CellValue::Error(format!("#REF! (invalid shared string index: {})", raw))
236            }
237            Some(CellType::Boolean) => {
238                // Boolean
239                CellValue::Boolean(raw == "1" || raw.eq_ignore_ascii_case("true"))
240            }
241            Some(CellType::Error) => {
242                // Error
243                CellValue::Error(raw.to_string())
244            }
245            Some(CellType::String) | Some(CellType::InlineString) => {
246                // Inline string
247                CellValue::String(raw.to_string())
248            }
249            Some(CellType::Number) | None => {
250                // Number (or default, which is number)
251                if raw.is_empty() {
252                    CellValue::Empty
253                } else if let Ok(n) = raw.parse::<f64>() {
254                    CellValue::Number(n)
255                } else {
256                    // Fallback to string if not a valid number
257                    CellValue::String(raw.to_string())
258                }
259            }
260        }
261    }
262
263    fn value_as_string(&self, ctx: &ResolveContext) -> String {
264        self.resolved_value(ctx).to_display_string()
265    }
266
267    fn value_as_number(&self, ctx: &ResolveContext) -> Option<f64> {
268        self.resolved_value(ctx).as_number()
269    }
270
271    fn value_as_bool(&self, ctx: &ResolveContext) -> Option<bool> {
272        self.resolved_value(ctx).as_bool()
273    }
274}
275
276// =============================================================================
277// Row Extension Traits
278// =============================================================================
279
280/// Pure extension methods for `Row` that do not require external context.
281pub trait RowExt {
282    /// Get the 1-based row number.
283    fn row_number(&self) -> Option<u32>;
284
285    /// Get the number of cells in this row.
286    fn cell_count(&self) -> usize;
287
288    /// Check if row is empty (no cells).
289    fn is_empty(&self) -> bool;
290
291    /// Get a cell by column number (1-based).
292    fn cell_at_column(&self, col: u32) -> Option<&Cell>;
293
294    /// Iterate over cells.
295    fn cells_iter(&self) -> impl Iterator<Item = &Cell>;
296}
297
298impl RowExt for Row {
299    fn row_number(&self) -> Option<u32> {
300        self.reference
301    }
302
303    fn cell_count(&self) -> usize {
304        self.cells.len()
305    }
306
307    fn is_empty(&self) -> bool {
308        self.cells.is_empty()
309    }
310
311    fn cell_at_column(&self, col: u32) -> Option<&Cell> {
312        self.cells.iter().find(|c| {
313            c.reference
314                .as_ref()
315                .and_then(|r| parse_column(r))
316                .map(|c_col| c_col == col)
317                .unwrap_or(false)
318        })
319    }
320
321    fn cells_iter(&self) -> impl Iterator<Item = &Cell> {
322        self.cells.iter()
323    }
324}
325
326// =============================================================================
327// Worksheet Parsing
328// =============================================================================
329
330/// Parse a worksheet from XML bytes using the generated FromXml parser.
331///
332/// This is the recommended way to parse worksheet XML, as it uses the
333/// spec-compliant generated types and is faster than serde.
334pub fn parse_worksheet(xml: &[u8]) -> Result<Worksheet, ParseError> {
335    let mut reader = Reader::from_reader(Cursor::new(xml));
336    let mut buf = Vec::new();
337
338    loop {
339        match reader.read_event_into(&mut buf) {
340            Ok(Event::Start(e)) => return Worksheet::from_xml(&mut reader, &e, false),
341            Ok(Event::Empty(e)) => return Worksheet::from_xml(&mut reader, &e, true),
342            Ok(Event::Eof) => break,
343            Err(e) => return Err(ParseError::Xml(e)),
344            _ => {}
345        }
346        buf.clear();
347    }
348    Err(ParseError::UnexpectedElement(
349        "no worksheet element found".to_string(),
350    ))
351}
352
353// =============================================================================
354// Worksheet Extension Traits
355// =============================================================================
356
357/// Pure extension methods for `Worksheet` (no context needed).
358pub trait WorksheetExt {
359    /// Get the sheet data (rows and cells).
360    fn sheet_data(&self) -> &SheetData;
361
362    /// Get the number of rows.
363    fn row_count(&self) -> usize;
364
365    /// Check if the worksheet is empty.
366    fn is_empty(&self) -> bool;
367
368    /// Get a row by 1-based row number.
369    fn row(&self, row_num: u32) -> Option<&Row>;
370
371    /// Get a cell by reference (e.g., "A1", "B5").
372    fn cell(&self, reference: &str) -> Option<&Cell>;
373
374    /// Iterate over all rows.
375    fn rows(&self) -> impl Iterator<Item = &Row>;
376
377    /// Check if the worksheet has an auto-filter.
378    #[cfg(feature = "sml-filtering")]
379    fn has_auto_filter(&self) -> bool;
380
381    /// Check if the worksheet has merged cells.
382    fn has_merged_cells(&self) -> bool;
383
384    /// Check if the worksheet has conditional formatting.
385    #[cfg(feature = "sml-styling")]
386    fn has_conditional_formatting(&self) -> bool;
387
388    /// Check if the worksheet has data validations.
389    #[cfg(feature = "sml-validation")]
390    fn has_data_validations(&self) -> bool;
391
392    /// Check if the worksheet has freeze panes.
393    #[cfg(feature = "sml-structure")]
394    fn has_freeze_panes(&self) -> bool;
395
396    /// Get the height of a row in points (if specified).
397    ///
398    /// `row_num` is 1-based. Returns `None` if the row does not exist or has no
399    /// explicit height set. ECMA-376 Part 1, §18.3.1.73 (`@ht` attribute).
400    #[cfg(feature = "sml-styling")]
401    fn get_row_height(&self, row_num: u32) -> Option<f64>;
402
403    /// Get the width of a column in characters (if specified).
404    ///
405    /// `col_idx` is 1-based (A=1, B=2, …). Returns `None` if no column definition
406    /// covers this index or if the column has no explicit width.
407    /// ECMA-376 Part 1, §18.3.1.13 (`@width` attribute).
408    #[cfg(feature = "sml-styling")]
409    fn get_column_width(&self, col_idx: u32) -> Option<f64>;
410}
411
412impl WorksheetExt for Worksheet {
413    fn sheet_data(&self) -> &SheetData {
414        &self.sheet_data
415    }
416
417    fn row_count(&self) -> usize {
418        self.sheet_data.row.len()
419    }
420
421    fn is_empty(&self) -> bool {
422        self.sheet_data.row.is_empty()
423    }
424
425    fn row(&self, row_num: u32) -> Option<&Row> {
426        self.sheet_data
427            .row
428            .iter()
429            .find(|r| r.reference == Some(row_num))
430    }
431
432    fn cell(&self, reference: &str) -> Option<&Cell> {
433        let col = parse_column(reference)?;
434        let row_num = parse_row(reference)?;
435        let row = self.row(row_num)?;
436        row.cells.iter().find(|c| {
437            c.reference
438                .as_ref()
439                .and_then(|r| parse_column(r))
440                .map(|c_col| c_col == col)
441                .unwrap_or(false)
442        })
443    }
444
445    fn rows(&self) -> impl Iterator<Item = &Row> {
446        self.sheet_data.row.iter()
447    }
448
449    #[cfg(feature = "sml-filtering")]
450    fn has_auto_filter(&self) -> bool {
451        self.auto_filter.is_some()
452    }
453
454    fn has_merged_cells(&self) -> bool {
455        self.merged_cells.is_some()
456    }
457
458    #[cfg(feature = "sml-styling")]
459    fn has_conditional_formatting(&self) -> bool {
460        !self.conditional_formatting.is_empty()
461    }
462
463    #[cfg(feature = "sml-validation")]
464    fn has_data_validations(&self) -> bool {
465        self.data_validations.is_some()
466    }
467
468    #[cfg(feature = "sml-structure")]
469    fn has_freeze_panes(&self) -> bool {
470        // Check if any sheet view has a pane with frozen state
471        self.sheet_views
472            .as_ref()
473            .is_some_and(|views| views.sheet_view.iter().any(|sv| sv.pane.is_some()))
474    }
475
476    #[cfg(feature = "sml-styling")]
477    fn get_row_height(&self, row_num: u32) -> Option<f64> {
478        self.sheet_data
479            .row
480            .iter()
481            .find(|r| r.reference == Some(row_num))
482            .and_then(|r| r.height)
483    }
484
485    #[cfg(feature = "sml-styling")]
486    fn get_column_width(&self, col_idx: u32) -> Option<f64> {
487        self.cols
488            .iter()
489            .flat_map(|cols| &cols.col)
490            .find(|c| c.start_column <= col_idx && col_idx <= c.end_column)
491            .and_then(|c| c.width)
492    }
493}
494
495/// Extension methods for `SheetData`, providing row lookup and iteration.
496pub trait SheetDataExt {
497    /// Get a row by 1-based row number.
498    fn row(&self, row_num: u32) -> Option<&Row>;
499
500    /// Iterate over rows.
501    fn rows(&self) -> impl Iterator<Item = &Row>;
502}
503
504impl SheetDataExt for SheetData {
505    fn row(&self, row_num: u32) -> Option<&Row> {
506        self.row.iter().find(|r| r.reference == Some(row_num))
507    }
508
509    fn rows(&self) -> impl Iterator<Item = &Row> {
510        self.row.iter()
511    }
512}
513
514// =============================================================================
515// ResolvedSheet - High-level wrapper with automatic value resolution
516// =============================================================================
517
518/// A worksheet with bound resolution context for convenient value access.
519///
520/// This is the high-level API for reading worksheets. It wraps a generated
521/// `types::Worksheet` and provides methods that automatically resolve values
522/// using the shared string table.
523///
524/// # Example
525///
526/// ```ignore
527/// let sheet = ResolvedSheet::new(name, worksheet, shared_strings);
528///
529/// // Iterate rows and get resolved values
530/// for row in sheet.rows() {
531///     for cell in row.cells_iter() {
532///         println!("{}", sheet.cell_value_string(cell));
533///     }
534/// }
535///
536/// // Direct cell access
537/// if let Some(cell) = sheet.cell("A1") {
538///     println!("A1 = {}", sheet.cell_value_string(cell));
539/// }
540/// ```
541#[derive(Debug, Clone)]
542pub struct ResolvedSheet {
543    /// Sheet name
544    name: String,
545    /// The underlying worksheet data (generated type)
546    worksheet: Worksheet,
547    /// Resolution context for shared strings
548    context: ResolveContext,
549    /// Comments (loaded separately from comments.xml)
550    comments: Vec<Comment>,
551    /// Charts (loaded separately via drawing relationships)
552    charts: Vec<Chart>,
553    /// Pivot tables (loaded from sheet relationships)
554    #[cfg(feature = "sml-pivot")]
555    pivot_tables: Vec<crate::types::CTPivotTableDefinition>,
556}
557
558/// A comment (note) attached to a cell, as returned by [`ResolvedSheet::comments`].
559#[derive(Debug, Clone)]
560pub struct Comment {
561    /// Cell reference (e.g., "A1")
562    pub reference: String,
563    /// Comment author (if available)
564    pub author: Option<String>,
565    /// Comment text
566    pub text: String,
567}
568
569/// A chart embedded in the worksheet, as returned by [`ResolvedSheet::charts`].
570#[derive(Debug, Clone)]
571pub struct Chart {
572    /// Chart title (if available)
573    pub title: Option<String>,
574    /// Chart type
575    pub chart_type: ChartType,
576}
577
578/// The type of an embedded chart as exposed by [`ResolvedSheet`].
579#[derive(Debug, Clone, Copy, PartialEq, Eq)]
580pub enum ChartType {
581    /// Bar chart (horizontal bars).
582    Bar,
583    /// Column chart (vertical bars).
584    Column,
585    /// Line chart.
586    Line,
587    /// Pie chart.
588    Pie,
589    /// Area chart.
590    Area,
591    /// Scatter (XY) chart.
592    Scatter,
593    /// Doughnut chart.
594    Doughnut,
595    /// Radar (spider) chart.
596    Radar,
597    /// Surface chart.
598    Surface,
599    /// Bubble chart.
600    Bubble,
601    /// Stock (OHLC) chart.
602    Stock,
603    /// Unrecognized chart type.
604    Unknown,
605}
606
607impl ResolvedSheet {
608    /// Create a new resolved sheet.
609    pub fn new(name: String, worksheet: Worksheet, shared_strings: Vec<String>) -> Self {
610        Self {
611            name,
612            worksheet,
613            context: ResolveContext::new(shared_strings),
614            comments: Vec::new(),
615            charts: Vec::new(),
616            #[cfg(feature = "sml-pivot")]
617            pivot_tables: Vec::new(),
618        }
619    }
620
621    /// Create a resolved sheet with comments, charts, and pivot tables.
622    pub fn with_extras(
623        name: String,
624        worksheet: Worksheet,
625        shared_strings: Vec<String>,
626        comments: Vec<Comment>,
627        charts: Vec<Chart>,
628        #[cfg(feature = "sml-pivot")] pivot_tables: Vec<crate::types::CTPivotTableDefinition>,
629    ) -> Self {
630        Self {
631            name,
632            worksheet,
633            context: ResolveContext::new(shared_strings),
634            comments,
635            charts,
636            #[cfg(feature = "sml-pivot")]
637            pivot_tables,
638        }
639    }
640
641    /// Get the sheet name.
642    pub fn name(&self) -> &str {
643        &self.name
644    }
645
646    /// Get the underlying worksheet (generated type).
647    pub fn worksheet(&self) -> &Worksheet {
648        &self.worksheet
649    }
650
651    /// Get the resolution context.
652    pub fn context(&self) -> &ResolveContext {
653        &self.context
654    }
655
656    // -------------------------------------------------------------------------
657    // Row/Cell Access (delegating to WorksheetExt)
658    // -------------------------------------------------------------------------
659
660    /// Get the number of rows.
661    pub fn row_count(&self) -> usize {
662        self.worksheet.row_count()
663    }
664
665    /// Check if the sheet is empty.
666    pub fn is_empty(&self) -> bool {
667        self.worksheet.is_empty()
668    }
669
670    /// Get a row by 1-based row number.
671    pub fn row(&self, row_num: u32) -> Option<&Row> {
672        self.worksheet.row(row_num)
673    }
674
675    /// Iterate over all rows.
676    pub fn rows(&self) -> impl Iterator<Item = &Row> {
677        self.worksheet.rows()
678    }
679
680    /// Get a cell by reference (e.g., "A1").
681    pub fn cell(&self, reference: &str) -> Option<&Cell> {
682        self.worksheet.cell(reference)
683    }
684
685    // -------------------------------------------------------------------------
686    // Value Resolution (convenience methods)
687    // -------------------------------------------------------------------------
688
689    /// Get a cell's resolved value.
690    pub fn cell_value(&self, cell: &Cell) -> CellValue {
691        cell.resolved_value(&self.context)
692    }
693
694    /// Get a cell's value as a display string.
695    pub fn cell_value_string(&self, cell: &Cell) -> String {
696        cell.value_as_string(&self.context)
697    }
698
699    /// Get a cell's value as a number (if applicable).
700    pub fn cell_value_number(&self, cell: &Cell) -> Option<f64> {
701        cell.value_as_number(&self.context)
702    }
703
704    /// Get a cell's value as a boolean (if applicable).
705    pub fn cell_value_bool(&self, cell: &Cell) -> Option<bool> {
706        cell.value_as_bool(&self.context)
707    }
708
709    /// Get the value at a cell reference as a string.
710    pub fn value_at(&self, reference: &str) -> Option<String> {
711        self.cell(reference).map(|c| self.cell_value_string(c))
712    }
713
714    /// Get the value at a cell reference as a number.
715    pub fn number_at(&self, reference: &str) -> Option<f64> {
716        self.cell(reference).and_then(|c| self.cell_value_number(c))
717    }
718
719    // -------------------------------------------------------------------------
720    // Sheet Features
721    // -------------------------------------------------------------------------
722
723    /// Check if the sheet has an auto-filter.
724    #[cfg(feature = "sml-filtering")]
725    pub fn has_auto_filter(&self) -> bool {
726        self.worksheet.has_auto_filter()
727    }
728
729    /// Check if the sheet has merged cells.
730    pub fn has_merged_cells(&self) -> bool {
731        self.worksheet.has_merged_cells()
732    }
733
734    /// Check if the sheet has conditional formatting.
735    #[cfg(feature = "sml-styling")]
736    pub fn has_conditional_formatting(&self) -> bool {
737        self.worksheet.has_conditional_formatting()
738    }
739
740    /// Check if the sheet has data validations.
741    #[cfg(feature = "sml-validation")]
742    pub fn has_data_validations(&self) -> bool {
743        self.worksheet.has_data_validations()
744    }
745
746    /// Check if the sheet has freeze panes.
747    #[cfg(feature = "sml-structure")]
748    pub fn has_freeze_panes(&self) -> bool {
749        self.worksheet.has_freeze_panes()
750    }
751
752    // -------------------------------------------------------------------------
753    // Comments
754    // -------------------------------------------------------------------------
755
756    /// Get all comments.
757    pub fn comments(&self) -> &[Comment] {
758        &self.comments
759    }
760
761    /// Get the comment for a specific cell.
762    pub fn comment(&self, reference: &str) -> Option<&Comment> {
763        self.comments.iter().find(|c| c.reference == reference)
764    }
765
766    /// Check if a cell has a comment.
767    pub fn has_comment(&self, reference: &str) -> bool {
768        self.comment(reference).is_some()
769    }
770
771    // -------------------------------------------------------------------------
772    // Charts
773    // -------------------------------------------------------------------------
774
775    /// Get all charts.
776    pub fn charts(&self) -> &[Chart] {
777        &self.charts
778    }
779
780    // -------------------------------------------------------------------------
781    // Pivot Tables
782    // -------------------------------------------------------------------------
783
784    /// Get all pivot tables on this sheet.
785    ///
786    /// Pivot tables are loaded from the sheet's relationships (pivotTable parts).
787    /// Requires the `sml-pivot` feature.
788    ///
789    /// ECMA-376 Part 1, Section 18.10 (PivotTable).
790    #[cfg(feature = "sml-pivot")]
791    pub fn pivot_tables(&self) -> &[crate::types::CTPivotTableDefinition] {
792        &self.pivot_tables
793    }
794
795    // -------------------------------------------------------------------------
796    // Dimensions & Structure
797    // -------------------------------------------------------------------------
798
799    /// Get the used range dimensions: (min_row, min_col, max_row, max_col).
800    ///
801    /// Returns None if the sheet is empty.
802    pub fn dimensions(&self) -> Option<(u32, u32, u32, u32)> {
803        if self.worksheet.sheet_data.row.is_empty() {
804            return None;
805        }
806
807        let mut min_row = u32::MAX;
808        let mut max_row = 0u32;
809        let mut min_col = u32::MAX;
810        let mut max_col = 0u32;
811
812        for row in &self.worksheet.sheet_data.row {
813            if let Some(row_num) = row.reference {
814                min_row = min_row.min(row_num);
815                max_row = max_row.max(row_num);
816            }
817            for cell in &row.cells {
818                if let Some(col) = cell.column_number() {
819                    min_col = min_col.min(col);
820                    max_col = max_col.max(col);
821                }
822            }
823        }
824
825        if min_row == u32::MAX {
826            None
827        } else {
828            Some((min_row, min_col, max_row, max_col))
829        }
830    }
831
832    /// Get merged cell ranges (raw data).
833    pub fn merged_cells(&self) -> Option<&crate::types::MergedCells> {
834        self.worksheet.merged_cells.as_deref()
835    }
836
837    /// Get conditional formatting rules (raw data).
838    #[cfg(feature = "sml-styling")]
839    pub fn conditional_formatting(&self) -> &[crate::types::ConditionalFormatting] {
840        &self.worksheet.conditional_formatting
841    }
842
843    /// Get data validations (raw data).
844    #[cfg(feature = "sml-validation")]
845    pub fn data_validations(&self) -> Option<&crate::types::DataValidations> {
846        self.worksheet.data_validations.as_deref()
847    }
848
849    /// Get the auto-filter configuration (raw data).
850    #[cfg(feature = "sml-filtering")]
851    pub fn auto_filter(&self) -> Option<&crate::types::AutoFilter> {
852        self.worksheet.auto_filter.as_deref()
853    }
854
855    /// Get the sheet views (contains freeze pane info).
856    pub fn sheet_views(&self) -> Option<&crate::types::SheetViews> {
857        self.worksheet.sheet_views.as_deref()
858    }
859
860    /// Get the freeze pane configuration (if any).
861    #[cfg(feature = "sml-structure")]
862    pub fn freeze_pane(&self) -> Option<&crate::types::Pane> {
863        self.worksheet
864            .sheet_views
865            .as_ref()
866            .and_then(|views| views.sheet_view.first())
867            .and_then(|view| view.pane.as_deref())
868    }
869
870    /// Get column definitions.
871    #[cfg(feature = "sml-styling")]
872    pub fn columns(&self) -> &[crate::types::Columns] {
873        &self.worksheet.cols
874    }
875}
876
877// =============================================================================
878// Helpers
879// =============================================================================
880
881/// Parse column letters from a cell reference (e.g., "AB5" -> 28).
882fn parse_column(reference: &str) -> Option<u32> {
883    let mut col: u32 = 0;
884    for ch in reference.chars() {
885        if ch.is_ascii_alphabetic() {
886            col = col * 26 + (ch.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
887        } else {
888            break;
889        }
890    }
891    if col > 0 { Some(col) } else { None }
892}
893
894/// Parse row number from a cell reference (e.g., "AB5" -> 5).
895fn parse_row(reference: &str) -> Option<u32> {
896    let digits: String = reference.chars().filter(|c| c.is_ascii_digit()).collect();
897    digits.parse().ok()
898}
899
900// =============================================================================
901// Pivot Table Extension Traits
902// =============================================================================
903
904/// Extension methods for `types::CTPivotTableDefinition`.
905///
906/// Provides convenient access to pivot table name, location, and field indices.
907/// Gated on the `sml-pivot` feature.
908///
909/// ECMA-376 Part 1, Section 18.10.1.73 (pivotTableDefinition).
910#[cfg(feature = "sml-pivot")]
911pub trait PivotTableExt {
912    /// Get the pivot table name.
913    fn name(&self) -> &str;
914
915    /// Get the cell range for this pivot table (e.g., "A1:D10").
916    ///
917    /// This is the `ref` attribute of the `location` element.
918    fn location_reference(&self) -> &str;
919
920    /// Get the names of all data fields (value fields).
921    ///
922    /// Returns each data field's `name` attribute when present, or an empty
923    /// string for unnamed data fields.
924    fn data_field_names(&self) -> Vec<&str>;
925
926    /// Get the field indices used as row fields.
927    ///
928    /// Each value is the `x` attribute of a `<field>` element in `<rowFields>`.
929    /// Negative values (e.g., `-2`) represent the special "data" axis field.
930    fn row_field_indices(&self) -> Vec<i32>;
931
932    /// Get the field indices used as column fields.
933    ///
934    /// Each value is the `x` attribute of a `<field>` element in `<colFields>`.
935    /// Negative values (e.g., `-2`) represent the special "data" axis field.
936    fn col_field_indices(&self) -> Vec<i32>;
937}
938
939#[cfg(feature = "sml-pivot")]
940impl PivotTableExt for crate::types::CTPivotTableDefinition {
941    fn name(&self) -> &str {
942        &self.name
943    }
944
945    fn location_reference(&self) -> &str {
946        &self.location.reference
947    }
948
949    fn data_field_names(&self) -> Vec<&str> {
950        self.data_fields
951            .as_ref()
952            .map(|df| {
953                df.data_field
954                    .iter()
955                    .map(|f| f.name.as_deref().unwrap_or(""))
956                    .collect()
957            })
958            .unwrap_or_default()
959    }
960
961    fn row_field_indices(&self) -> Vec<i32> {
962        self.row_fields
963            .as_ref()
964            .map(|rf| rf.field.iter().map(|f| f.x).collect())
965            .unwrap_or_default()
966    }
967
968    fn col_field_indices(&self) -> Vec<i32> {
969        self.col_fields
970            .as_ref()
971            .map(|cf| cf.field.iter().map(|f| f.x).collect())
972            .unwrap_or_default()
973    }
974}
975
976// =============================================================================
977// Conditional Formatting Extension Traits
978// =============================================================================
979
980/// Extension methods for `types::ConditionalFormatting`.
981///
982/// Provides convenient access to cell range and contained rules.
983/// Gated on the `sml-styling` feature.
984///
985/// ECMA-376 Part 1, Section 18.3.1.18 (conditionalFormatting).
986#[cfg(feature = "sml-styling")]
987pub trait ConditionalFormattingExt {
988    /// Get the cell range this formatting applies to (sqref attribute).
989    ///
990    /// For example, `"A1:B10"` or `"A1:A10 C1:C10"` (space-separated ranges).
991    fn cell_range(&self) -> Option<&str>;
992
993    /// Get the conditional formatting rules.
994    fn rules(&self) -> &[crate::types::ConditionalRule];
995
996    /// Get the number of rules.
997    fn rule_count(&self) -> usize;
998}
999
1000#[cfg(feature = "sml-styling")]
1001impl ConditionalFormattingExt for crate::types::ConditionalFormatting {
1002    fn cell_range(&self) -> Option<&str> {
1003        self.square_reference.as_deref()
1004    }
1005
1006    fn rules(&self) -> &[crate::types::ConditionalRule] {
1007        &self.cf_rule
1008    }
1009
1010    fn rule_count(&self) -> usize {
1011        self.cf_rule.len()
1012    }
1013}
1014
1015/// Extension methods for `types::ConditionalRule`.
1016///
1017/// Provides convenient access to rule type and visualization sub-elements.
1018/// Gated on the `sml-styling` feature.
1019///
1020/// ECMA-376 Part 1, Section 18.3.1.10 (cfRule).
1021#[cfg(feature = "sml-styling")]
1022pub trait ConditionalRuleExt {
1023    /// Get the rule type (e.g., ColorScale, DataBar, CellIs, Expression).
1024    fn rule_type(&self) -> Option<&crate::types::ConditionalType>;
1025
1026    /// Get the rule priority (lower number = higher priority).
1027    fn priority(&self) -> i32;
1028
1029    /// Check if this rule uses a color scale visualization.
1030    fn has_color_scale(&self) -> bool;
1031
1032    /// Check if this rule uses a data bar visualization.
1033    fn has_data_bar(&self) -> bool;
1034
1035    /// Check if this rule uses an icon set visualization.
1036    fn has_icon_set(&self) -> bool;
1037
1038    /// Get the formula expressions associated with this rule.
1039    ///
1040    /// For `cellIs` rules, contains 1-2 formulas (operands).
1041    /// For `expression` rules, contains 1 formula.
1042    /// For visualization rules (colorScale, dataBar, iconSet), empty.
1043    fn formulas(&self) -> &[crate::types::STFormula];
1044}
1045
1046#[cfg(feature = "sml-styling")]
1047impl ConditionalRuleExt for crate::types::ConditionalRule {
1048    fn rule_type(&self) -> Option<&crate::types::ConditionalType> {
1049        self.r#type.as_ref()
1050    }
1051
1052    fn priority(&self) -> i32 {
1053        self.priority
1054    }
1055
1056    fn has_color_scale(&self) -> bool {
1057        self.color_scale.is_some()
1058    }
1059
1060    fn has_data_bar(&self) -> bool {
1061        self.data_bar.is_some()
1062    }
1063
1064    fn has_icon_set(&self) -> bool {
1065        self.icon_set.is_some()
1066    }
1067
1068    fn formulas(&self) -> &[crate::types::STFormula] {
1069        &self.formula
1070    }
1071}
1072
1073// =============================================================================
1074// Font / Fill / Border / Format Extension Traits
1075// =============================================================================
1076
1077/// Extension methods for `types::Font` (ECMA-376 §18.8.22, CT_Font).
1078///
1079/// Provides convenient access to font properties such as bold, italic, name,
1080/// and size. All accessors are gated on the `sml-styling` feature because the
1081/// underlying fields are only present when that feature is enabled.
1082#[cfg(feature = "sml-styling")]
1083pub trait FontExt {
1084    /// Check if bold is set.
1085    fn is_bold(&self) -> bool;
1086    /// Check if italic is set.
1087    fn is_italic(&self) -> bool;
1088    /// Check if strikethrough is set.
1089    fn is_strikethrough(&self) -> bool;
1090    /// Check if outline is set.
1091    fn is_outline(&self) -> bool;
1092    /// Check if shadow is set.
1093    fn is_shadow(&self) -> bool;
1094    /// Check if condense is set.
1095    fn is_condense(&self) -> bool;
1096    /// Check if extend is set.
1097    fn is_extend(&self) -> bool;
1098    /// Get the typeface name (e.g. `"Calibri"`).
1099    fn font_name(&self) -> Option<&str>;
1100    /// Get the font size in points (e.g. `11.0`).
1101    fn font_size(&self) -> Option<f64>;
1102    /// Get the font colour.
1103    fn font_color(&self) -> Option<&crate::types::Color>;
1104    /// Get the vertical alignment (baseline / superscript / subscript).
1105    fn vertical_align(&self) -> Option<crate::types::VerticalAlignRun>;
1106    /// Get the font scheme (none / major / minor).
1107    fn font_scheme(&self) -> Option<crate::types::FontScheme>;
1108}
1109
1110#[cfg(feature = "sml-styling")]
1111impl FontExt for crate::types::Font {
1112    fn is_bold(&self) -> bool {
1113        self.b.as_ref().is_some_and(|v| v.value.unwrap_or(false))
1114    }
1115
1116    fn is_italic(&self) -> bool {
1117        self.i.as_ref().is_some_and(|v| v.value.unwrap_or(false))
1118    }
1119
1120    fn is_strikethrough(&self) -> bool {
1121        self.strike
1122            .as_ref()
1123            .is_some_and(|v| v.value.unwrap_or(false))
1124    }
1125
1126    fn is_outline(&self) -> bool {
1127        self.outline
1128            .as_ref()
1129            .is_some_and(|v| v.value.unwrap_or(false))
1130    }
1131
1132    fn is_shadow(&self) -> bool {
1133        self.shadow
1134            .as_ref()
1135            .is_some_and(|v| v.value.unwrap_or(false))
1136    }
1137
1138    fn is_condense(&self) -> bool {
1139        self.condense
1140            .as_ref()
1141            .is_some_and(|v| v.value.unwrap_or(false))
1142    }
1143
1144    fn is_extend(&self) -> bool {
1145        self.extend
1146            .as_ref()
1147            .is_some_and(|v| v.value.unwrap_or(false))
1148    }
1149
1150    fn font_name(&self) -> Option<&str> {
1151        self.name.as_deref().map(|n| n.value.as_str())
1152    }
1153
1154    fn font_size(&self) -> Option<f64> {
1155        self.sz.as_deref().map(|s| s.value)
1156    }
1157
1158    fn font_color(&self) -> Option<&crate::types::Color> {
1159        self.color.as_deref()
1160    }
1161
1162    fn vertical_align(&self) -> Option<crate::types::VerticalAlignRun> {
1163        self.vert_align.as_deref().map(|v| v.value)
1164    }
1165
1166    fn font_scheme(&self) -> Option<crate::types::FontScheme> {
1167        self.scheme.as_deref().map(|s| s.value)
1168    }
1169}
1170
1171/// Extension methods for `types::Fill` (ECMA-376 §18.8.20, CT_Fill).
1172///
1173/// Provides access to the fill sub-type (pattern or gradient).
1174/// Gated on the `sml-styling` feature.
1175#[cfg(feature = "sml-styling")]
1176pub trait FillExt {
1177    /// Get the pattern fill definition, if this fill uses a pattern.
1178    fn pattern_fill(&self) -> Option<&crate::types::PatternFill>;
1179    /// Get the gradient fill definition, if this fill uses a gradient.
1180    fn gradient_fill(&self) -> Option<&crate::types::GradientFill>;
1181    /// Check if this fill has any fill type set (pattern or gradient).
1182    fn has_fill(&self) -> bool;
1183}
1184
1185#[cfg(feature = "sml-styling")]
1186impl FillExt for crate::types::Fill {
1187    fn pattern_fill(&self) -> Option<&crate::types::PatternFill> {
1188        self.pattern_fill.as_deref()
1189    }
1190
1191    fn gradient_fill(&self) -> Option<&crate::types::GradientFill> {
1192        self.gradient_fill.as_deref()
1193    }
1194
1195    fn has_fill(&self) -> bool {
1196        self.pattern_fill.is_some() || self.gradient_fill.is_some()
1197    }
1198}
1199
1200/// Extension methods for `types::PatternFill` (ECMA-376 §18.8.32, CT_PatternFill).
1201///
1202/// Provides access to the pattern type and foreground/background colours.
1203pub trait PatternFillExt {
1204    /// Get the pattern type (solid, dark-grid, etc.).
1205    fn pattern_type(&self) -> Option<crate::types::PatternType>;
1206    /// Get the foreground colour of the pattern.
1207    fn foreground_color(&self) -> Option<&crate::types::Color>;
1208    /// Get the background colour of the pattern.
1209    fn background_color(&self) -> Option<&crate::types::Color>;
1210}
1211
1212impl PatternFillExt for crate::types::PatternFill {
1213    fn pattern_type(&self) -> Option<crate::types::PatternType> {
1214        self.pattern_type
1215    }
1216
1217    fn foreground_color(&self) -> Option<&crate::types::Color> {
1218        self.fg_color.as_deref()
1219    }
1220
1221    fn background_color(&self) -> Option<&crate::types::Color> {
1222        self.bg_color.as_deref()
1223    }
1224}
1225
1226/// Extension methods for `types::Border` (ECMA-376 §18.8.4, CT_Border).
1227///
1228/// Provides access to individual border sides and diagonal flags.
1229/// Gated on the `sml-styling` feature.
1230#[cfg(feature = "sml-styling")]
1231pub trait BorderExt {
1232    /// Get the left border properties.
1233    fn left_border(&self) -> Option<&crate::types::BorderProperties>;
1234    /// Get the right border properties.
1235    fn right_border(&self) -> Option<&crate::types::BorderProperties>;
1236    /// Get the top border properties.
1237    fn top_border(&self) -> Option<&crate::types::BorderProperties>;
1238    /// Get the bottom border properties.
1239    fn bottom_border(&self) -> Option<&crate::types::BorderProperties>;
1240    /// Get the diagonal border properties.
1241    fn diagonal_border(&self) -> Option<&crate::types::BorderProperties>;
1242    /// Check if the diagonal-up line is shown.
1243    fn is_diagonal_up(&self) -> bool;
1244    /// Check if the diagonal-down line is shown.
1245    fn is_diagonal_down(&self) -> bool;
1246    /// Check if the outline border is applied.
1247    fn is_outline_applied(&self) -> bool;
1248}
1249
1250#[cfg(feature = "sml-styling")]
1251impl BorderExt for crate::types::Border {
1252    fn left_border(&self) -> Option<&crate::types::BorderProperties> {
1253        self.left.as_deref()
1254    }
1255
1256    fn right_border(&self) -> Option<&crate::types::BorderProperties> {
1257        self.right.as_deref()
1258    }
1259
1260    fn top_border(&self) -> Option<&crate::types::BorderProperties> {
1261        self.top.as_deref()
1262    }
1263
1264    fn bottom_border(&self) -> Option<&crate::types::BorderProperties> {
1265        self.bottom.as_deref()
1266    }
1267
1268    fn diagonal_border(&self) -> Option<&crate::types::BorderProperties> {
1269        self.diagonal.as_deref()
1270    }
1271
1272    fn is_diagonal_up(&self) -> bool {
1273        self.diagonal_up.unwrap_or(false)
1274    }
1275
1276    fn is_diagonal_down(&self) -> bool {
1277        self.diagonal_down.unwrap_or(false)
1278    }
1279
1280    fn is_outline_applied(&self) -> bool {
1281        self.outline.unwrap_or(false)
1282    }
1283}
1284
1285/// Extension methods for `types::BorderProperties` (ECMA-376 §18.8.3, CT_BorderPr).
1286///
1287/// Provides access to border line style and colour.
1288pub trait BorderPropertiesExt {
1289    /// Get the border line style.
1290    fn border_style(&self) -> Option<crate::types::BorderStyle>;
1291    /// Get the border colour.
1292    fn border_color(&self) -> Option<&crate::types::Color>;
1293    /// Check if a border style is set.
1294    fn has_style(&self) -> bool;
1295}
1296
1297impl BorderPropertiesExt for crate::types::BorderProperties {
1298    fn border_style(&self) -> Option<crate::types::BorderStyle> {
1299        self.style
1300    }
1301
1302    fn border_color(&self) -> Option<&crate::types::Color> {
1303        self.color.as_deref()
1304    }
1305
1306    fn has_style(&self) -> bool {
1307        self.style.is_some()
1308    }
1309}
1310
1311/// Extension methods for `types::CellAlignment` (ECMA-376 §18.8.1, CT_CellAlignment).
1312///
1313/// Provides access to horizontal/vertical alignment, text rotation, and wrapping.
1314/// Gated on the `sml-styling` feature.
1315#[cfg(feature = "sml-styling")]
1316pub trait CellAlignmentExt {
1317    /// Get the horizontal alignment.
1318    fn horizontal_alignment(&self) -> Option<crate::types::HorizontalAlignment>;
1319    /// Get the vertical alignment.
1320    fn vertical_alignment(&self) -> Option<crate::types::VerticalAlignment>;
1321    /// Get the text rotation in degrees (0–180 or 255 for vertical text).
1322    fn text_rotation(&self) -> Option<u32>;
1323    /// Check if text wrapping is enabled.
1324    fn is_wrap_text(&self) -> bool;
1325    /// Check if shrink-to-fit is enabled.
1326    fn is_shrink_to_fit(&self) -> bool;
1327    /// Get the indent level.
1328    fn indent(&self) -> Option<u32>;
1329}
1330
1331#[cfg(feature = "sml-styling")]
1332impl CellAlignmentExt for crate::types::CellAlignment {
1333    fn horizontal_alignment(&self) -> Option<crate::types::HorizontalAlignment> {
1334        self.horizontal
1335    }
1336
1337    fn vertical_alignment(&self) -> Option<crate::types::VerticalAlignment> {
1338        self.vertical
1339    }
1340
1341    fn text_rotation(&self) -> Option<u32> {
1342        self.text_rotation.as_deref().and_then(|r| r.parse().ok())
1343    }
1344
1345    fn is_wrap_text(&self) -> bool {
1346        self.wrap_text.unwrap_or(false)
1347    }
1348
1349    fn is_shrink_to_fit(&self) -> bool {
1350        self.shrink_to_fit.unwrap_or(false)
1351    }
1352
1353    fn indent(&self) -> Option<u32> {
1354        self.indent
1355    }
1356}
1357
1358/// Extension methods for `types::CellProtection` (ECMA-376 §18.8.13, CT_CellProtection).
1359///
1360/// Provides access to cell locking and formula-hiding flags.
1361/// Gated on the `sml-protection` feature.
1362#[cfg(feature = "sml-protection")]
1363pub trait CellProtectionExt {
1364    /// Check if the cell is locked (default: `true` per OOXML spec).
1365    fn is_locked(&self) -> bool;
1366    /// Check if the formula is hidden from the formula bar.
1367    fn is_formula_hidden(&self) -> bool;
1368}
1369
1370#[cfg(feature = "sml-protection")]
1371impl CellProtectionExt for crate::types::CellProtection {
1372    fn is_locked(&self) -> bool {
1373        // Per ECMA-376, the default value is `true` — cells are locked unless
1374        // explicitly set to `false`.
1375        self.locked.unwrap_or(true)
1376    }
1377
1378    fn is_formula_hidden(&self) -> bool {
1379        self.hidden.unwrap_or(false)
1380    }
1381}
1382
1383/// Extension methods for `types::Format` (ECMA-376 §18.8.45, CT_Xf).
1384///
1385/// The `xf` element is used in both `cellXfs` (cell formats) and
1386/// `cellStyleXfs` (named style formats). It carries index references into
1387/// the font, fill, border, and numFmt tables, plus optional alignment and
1388/// protection overrides.
1389pub trait FormatExt {
1390    /// Get the number format ID (index into `numFmts`).
1391    #[cfg(feature = "sml-styling")]
1392    fn number_format_id(&self) -> u32;
1393    /// Get the font ID (index into `fonts`).
1394    #[cfg(feature = "sml-styling")]
1395    fn font_id(&self) -> u32;
1396    /// Get the fill ID (index into `fills`).
1397    #[cfg(feature = "sml-styling")]
1398    fn fill_id(&self) -> u32;
1399    /// Get the border ID (index into `borders`).
1400    #[cfg(feature = "sml-styling")]
1401    fn border_id(&self) -> u32;
1402    /// Check if alignment properties are applied from this format.
1403    #[cfg(feature = "sml-styling")]
1404    fn apply_alignment(&self) -> bool;
1405    /// Check if font properties are applied from this format.
1406    #[cfg(feature = "sml-styling")]
1407    fn apply_font(&self) -> bool;
1408    /// Check if fill properties are applied from this format.
1409    #[cfg(feature = "sml-styling")]
1410    fn apply_fill(&self) -> bool;
1411    /// Check if border properties are applied from this format.
1412    #[cfg(feature = "sml-styling")]
1413    fn apply_border(&self) -> bool;
1414    /// Get the cell alignment override (if any).
1415    #[cfg(feature = "sml-styling")]
1416    fn alignment(&self) -> Option<&crate::types::CellAlignment>;
1417    /// Get the cell protection override (if any).
1418    #[cfg(feature = "sml-protection")]
1419    fn protection(&self) -> Option<&crate::types::CellProtection>;
1420}
1421
1422impl FormatExt for crate::types::Format {
1423    #[cfg(feature = "sml-styling")]
1424    fn number_format_id(&self) -> u32 {
1425        self.number_format_id.unwrap_or(0)
1426    }
1427
1428    #[cfg(feature = "sml-styling")]
1429    fn font_id(&self) -> u32 {
1430        self.font_id.unwrap_or(0)
1431    }
1432
1433    #[cfg(feature = "sml-styling")]
1434    fn fill_id(&self) -> u32 {
1435        self.fill_id.unwrap_or(0)
1436    }
1437
1438    #[cfg(feature = "sml-styling")]
1439    fn border_id(&self) -> u32 {
1440        self.border_id.unwrap_or(0)
1441    }
1442
1443    #[cfg(feature = "sml-styling")]
1444    fn apply_alignment(&self) -> bool {
1445        self.apply_alignment.unwrap_or(false)
1446    }
1447
1448    #[cfg(feature = "sml-styling")]
1449    fn apply_font(&self) -> bool {
1450        self.apply_font.unwrap_or(false)
1451    }
1452
1453    #[cfg(feature = "sml-styling")]
1454    fn apply_fill(&self) -> bool {
1455        self.apply_fill.unwrap_or(false)
1456    }
1457
1458    #[cfg(feature = "sml-styling")]
1459    fn apply_border(&self) -> bool {
1460        self.apply_border.unwrap_or(false)
1461    }
1462
1463    #[cfg(feature = "sml-styling")]
1464    fn alignment(&self) -> Option<&crate::types::CellAlignment> {
1465        self.alignment.as_deref()
1466    }
1467
1468    #[cfg(feature = "sml-protection")]
1469    fn protection(&self) -> Option<&crate::types::CellProtection> {
1470        self.protection.as_deref()
1471    }
1472}
1473
1474/// Extension for `types::Worksheet` to access conditional formatting.
1475///
1476/// Gated on the `sml-styling` feature.
1477#[cfg(feature = "sml-styling")]
1478pub trait WorksheetConditionalFormattingExt {
1479    /// Get all conditional formatting rules on this worksheet.
1480    fn conditional_formattings(&self) -> &[crate::types::ConditionalFormatting];
1481}
1482
1483#[cfg(feature = "sml-styling")]
1484impl WorksheetConditionalFormattingExt for crate::types::Worksheet {
1485    fn conditional_formattings(&self) -> &[crate::types::ConditionalFormatting] {
1486        &self.conditional_formatting
1487    }
1488}
1489
1490#[cfg(test)]
1491mod tests {
1492    use super::*;
1493
1494    #[test]
1495    fn test_parse_column() {
1496        assert_eq!(parse_column("A1"), Some(1));
1497        assert_eq!(parse_column("B5"), Some(2));
1498        assert_eq!(parse_column("Z1"), Some(26));
1499        assert_eq!(parse_column("AA1"), Some(27));
1500        assert_eq!(parse_column("AB1"), Some(28));
1501        assert_eq!(parse_column("AZ1"), Some(52));
1502        assert_eq!(parse_column("BA1"), Some(53));
1503    }
1504
1505    #[test]
1506    fn test_parse_row() {
1507        assert_eq!(parse_row("A1"), Some(1));
1508        assert_eq!(parse_row("B5"), Some(5));
1509        assert_eq!(parse_row("AA100"), Some(100));
1510        assert_eq!(parse_row("ZZ9999"), Some(9999));
1511    }
1512
1513    #[test]
1514    #[cfg(feature = "full")]
1515    fn test_cell_ext() {
1516        let cell = Cell {
1517            reference: Some("B5".to_string()),
1518            cell_type: Some(CellType::Number),
1519            value: Some("42.5".to_string()),
1520            formula: None,
1521            style_index: None,
1522            cm: None,
1523            vm: None,
1524            placeholder: None,
1525            is: None,
1526            extension_list: None,
1527            #[cfg(feature = "extra-attrs")]
1528            extra_attrs: Default::default(),
1529            #[cfg(feature = "extra-children")]
1530            extra_children: Default::default(),
1531        };
1532
1533        assert_eq!(cell.column_number(), Some(2));
1534        assert_eq!(cell.row_number(), Some(5));
1535        assert!(!cell.has_formula());
1536        assert!(cell.is_number());
1537        assert!(!cell.is_shared_string());
1538    }
1539
1540    #[test]
1541    #[cfg(feature = "full")]
1542    fn test_cell_resolve_number() {
1543        let cell = Cell {
1544            reference: Some("A1".to_string()),
1545            cell_type: Some(CellType::Number),
1546            value: Some("123.45".to_string()),
1547            formula: None,
1548            style_index: None,
1549            cm: None,
1550            vm: None,
1551            placeholder: None,
1552            is: None,
1553            extension_list: None,
1554            #[cfg(feature = "extra-attrs")]
1555            extra_attrs: Default::default(),
1556            #[cfg(feature = "extra-children")]
1557            extra_children: Default::default(),
1558        };
1559
1560        let ctx = ResolveContext::default();
1561        assert_eq!(cell.resolved_value(&ctx), CellValue::Number(123.45));
1562        assert_eq!(cell.value_as_string(&ctx), "123.45");
1563        assert_eq!(cell.value_as_number(&ctx), Some(123.45));
1564    }
1565
1566    #[test]
1567    #[cfg(feature = "full")]
1568    fn test_cell_resolve_shared_string() {
1569        let cell = Cell {
1570            reference: Some("A1".to_string()),
1571            cell_type: Some(CellType::SharedString),
1572            value: Some("0".to_string()), // Index into shared strings
1573            formula: None,
1574            style_index: None,
1575            cm: None,
1576            vm: None,
1577            placeholder: None,
1578            is: None,
1579            extension_list: None,
1580            #[cfg(feature = "extra-attrs")]
1581            extra_attrs: Default::default(),
1582            #[cfg(feature = "extra-children")]
1583            extra_children: Default::default(),
1584        };
1585
1586        let ctx = ResolveContext::new(vec!["Hello".to_string(), "World".to_string()]);
1587        assert_eq!(
1588            cell.resolved_value(&ctx),
1589            CellValue::String("Hello".to_string())
1590        );
1591        assert_eq!(cell.value_as_string(&ctx), "Hello");
1592    }
1593
1594    #[test]
1595    #[cfg(feature = "full")]
1596    fn test_cell_resolve_boolean() {
1597        let cell = Cell {
1598            reference: Some("A1".to_string()),
1599            cell_type: Some(CellType::Boolean),
1600            value: Some("1".to_string()),
1601            formula: None,
1602            style_index: None,
1603            cm: None,
1604            vm: None,
1605            placeholder: None,
1606            is: None,
1607            extension_list: None,
1608            #[cfg(feature = "extra-attrs")]
1609            extra_attrs: Default::default(),
1610            #[cfg(feature = "extra-children")]
1611            extra_children: Default::default(),
1612        };
1613
1614        let ctx = ResolveContext::default();
1615        assert_eq!(cell.resolved_value(&ctx), CellValue::Boolean(true));
1616        assert_eq!(cell.value_as_string(&ctx), "TRUE");
1617        assert_eq!(cell.value_as_bool(&ctx), Some(true));
1618    }
1619
1620    #[test]
1621    fn test_parse_worksheet() {
1622        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1623        <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1624            <sheetData>
1625                <row r="1" spans="1:3">
1626                    <c r="A1" t="s"><v>0</v></c>
1627                    <c r="B1"><v>42.5</v></c>
1628                    <c r="C1" t="b"><v>1</v></c>
1629                </row>
1630                <row r="2">
1631                    <c r="A2"><v>100</v></c>
1632                </row>
1633            </sheetData>
1634        </worksheet>"#;
1635
1636        let worksheet = parse_worksheet(xml).expect("parse failed");
1637
1638        assert_eq!(worksheet.row_count(), 2);
1639        assert!(!worksheet.is_empty());
1640
1641        // Test row access
1642        let row1 = worksheet.row(1).expect("row 1 should exist");
1643        assert_eq!(row1.cells.len(), 3);
1644
1645        let row2 = worksheet.row(2).expect("row 2 should exist");
1646        assert_eq!(row2.cells.len(), 1);
1647
1648        // Test cell access by reference
1649        let cell_a1 = worksheet.cell("A1").expect("A1 should exist");
1650        assert_eq!(cell_a1.value.as_deref(), Some("0"));
1651        assert!(cell_a1.is_shared_string());
1652
1653        let cell_b1 = worksheet.cell("B1").expect("B1 should exist");
1654        assert_eq!(cell_b1.value.as_deref(), Some("42.5"));
1655        assert!(cell_b1.is_number());
1656
1657        // Test non-existent cell
1658        assert!(worksheet.cell("Z99").is_none());
1659    }
1660
1661    #[test]
1662    fn test_worksheet_ext_with_resolve() {
1663        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1664        <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1665            <sheetData>
1666                <row r="1">
1667                    <c r="A1" t="s"><v>0</v></c>
1668                    <c r="B1" t="s"><v>1</v></c>
1669                </row>
1670            </sheetData>
1671        </worksheet>"#;
1672
1673        let worksheet = parse_worksheet(xml).expect("parse failed");
1674        let ctx = ResolveContext::new(vec!["Hello".to_string(), "World".to_string()]);
1675
1676        let cell_a1 = worksheet.cell("A1").expect("A1 should exist");
1677        assert_eq!(cell_a1.value_as_string(&ctx), "Hello");
1678
1679        let cell_b1 = worksheet.cell("B1").expect("B1 should exist");
1680        assert_eq!(cell_b1.value_as_string(&ctx), "World");
1681    }
1682
1683    #[test]
1684    fn test_resolved_sheet() {
1685        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1686        <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1687            <sheetData>
1688                <row r="1">
1689                    <c r="A1" t="s"><v>0</v></c>
1690                    <c r="B1"><v>42.5</v></c>
1691                    <c r="C1" t="b"><v>1</v></c>
1692                </row>
1693                <row r="2">
1694                    <c r="A2" t="s"><v>1</v></c>
1695                    <c r="B2"><v>100</v></c>
1696                </row>
1697            </sheetData>
1698        </worksheet>"#;
1699
1700        let worksheet = parse_worksheet(xml).expect("parse failed");
1701        let shared_strings = vec!["Hello".to_string(), "World".to_string()];
1702        let sheet = ResolvedSheet::new("Sheet1".to_string(), worksheet, shared_strings);
1703
1704        // Basic info
1705        assert_eq!(sheet.name(), "Sheet1");
1706        assert_eq!(sheet.row_count(), 2);
1707        assert!(!sheet.is_empty());
1708
1709        // Cell access with auto-resolution
1710        let cell_a1 = sheet.cell("A1").expect("A1");
1711        assert_eq!(sheet.cell_value_string(cell_a1), "Hello");
1712
1713        let cell_b1 = sheet.cell("B1").expect("B1");
1714        assert_eq!(sheet.cell_value_number(cell_b1), Some(42.5));
1715
1716        let cell_c1 = sheet.cell("C1").expect("C1");
1717        assert_eq!(sheet.cell_value_bool(cell_c1), Some(true));
1718
1719        // Convenience methods
1720        assert_eq!(sheet.value_at("A1"), Some("Hello".to_string()));
1721        assert_eq!(sheet.value_at("A2"), Some("World".to_string()));
1722        assert_eq!(sheet.number_at("B1"), Some(42.5));
1723        assert_eq!(sheet.number_at("B2"), Some(100.0));
1724
1725        // Non-existent cell
1726        assert!(sheet.value_at("Z99").is_none());
1727    }
1728
1729    #[test]
1730    #[cfg(feature = "sml-styling")]
1731    fn test_conditional_formatting_cell_range() {
1732        use crate::workbook::bootstrap;
1733        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1734        <conditionalFormatting sqref="A1:B5" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1735        </conditionalFormatting>"#;
1736        let cf: crate::types::ConditionalFormatting = bootstrap(xml).expect("parse failed");
1737        assert_eq!(cf.cell_range(), Some("A1:B5"));
1738        assert_eq!(cf.rule_count(), 0);
1739    }
1740
1741    #[test]
1742    #[cfg(feature = "sml-styling")]
1743    fn test_conditional_rule_type() {
1744        use crate::workbook::bootstrap;
1745        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1746        <cfRule type="colorScale" priority="1" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1747            <colorScale>
1748                <cfvo type="min"/>
1749                <cfvo type="max"/>
1750                <color rgb="FF0000"/>
1751                <color rgb="00FF00"/>
1752            </colorScale>
1753        </cfRule>"#;
1754        let rule: crate::types::ConditionalRule = bootstrap(xml).expect("parse failed");
1755        assert_eq!(
1756            rule.rule_type(),
1757            Some(&crate::types::ConditionalType::ColorScale)
1758        );
1759        assert_eq!(rule.priority(), 1);
1760    }
1761
1762    #[test]
1763    #[cfg(feature = "sml-styling")]
1764    fn test_conditional_rule_has_color_scale() {
1765        use crate::workbook::bootstrap;
1766        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1767        <cfRule type="colorScale" priority="1" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1768            <colorScale>
1769                <cfvo type="min"/>
1770                <cfvo type="max"/>
1771                <color rgb="FF0000"/>
1772                <color rgb="00FF00"/>
1773            </colorScale>
1774        </cfRule>"#;
1775        let rule: crate::types::ConditionalRule = bootstrap(xml).expect("parse failed");
1776        assert!(rule.has_color_scale());
1777        assert!(!rule.has_data_bar());
1778        assert!(!rule.has_icon_set());
1779    }
1780
1781    #[test]
1782    #[cfg(feature = "sml-pivot")]
1783    fn test_pivot_table_name() {
1784        use crate::workbook::bootstrap;
1785        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1786        <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
1787            name="PivotTable1" cacheId="1" dataCaption="Values">
1788            <location ref="A1:D10" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>
1789        </pivotTableDefinition>"#;
1790        let pt: crate::types::CTPivotTableDefinition = bootstrap(xml).expect("parse failed");
1791        assert_eq!(pt.name(), "PivotTable1");
1792    }
1793
1794    #[test]
1795    #[cfg(feature = "sml-pivot")]
1796    fn test_pivot_table_location() {
1797        use crate::workbook::bootstrap;
1798        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1799        <pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
1800            name="PivotTable1" cacheId="1" dataCaption="Values">
1801            <location ref="A1:D10" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>
1802        </pivotTableDefinition>"#;
1803        let pt: crate::types::CTPivotTableDefinition = bootstrap(xml).expect("parse failed");
1804        assert_eq!(pt.location_reference(), "A1:D10");
1805    }
1806
1807    // -------------------------------------------------------------------------
1808    // FontExt tests
1809    // -------------------------------------------------------------------------
1810
1811    #[cfg(feature = "sml-styling")]
1812    #[test]
1813    fn test_font_ext_bold_italic() {
1814        use crate::workbook::bootstrap;
1815        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1816        <font xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1817            <b val="1"/>
1818            <i val="0"/>
1819            <name val="Calibri"/>
1820            <sz val="11"/>
1821        </font>"#;
1822        let font: crate::types::Font = bootstrap(xml).expect("parse failed");
1823        assert!(font.is_bold());
1824        assert!(!font.is_italic());
1825        assert_eq!(font.font_name(), Some("Calibri"));
1826        assert_eq!(font.font_size(), Some(11.0));
1827    }
1828
1829    #[cfg(feature = "sml-styling")]
1830    #[test]
1831    fn test_font_ext_defaults() {
1832        use crate::workbook::bootstrap;
1833        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1834        <font xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1835        "#;
1836        let font: crate::types::Font = bootstrap(xml).expect("parse failed");
1837        assert!(!font.is_bold());
1838        assert!(!font.is_italic());
1839        assert!(!font.is_strikethrough());
1840        assert!(font.font_name().is_none());
1841        assert!(font.font_size().is_none());
1842        assert!(font.font_color().is_none());
1843        assert!(font.vertical_align().is_none());
1844        assert!(font.font_scheme().is_none());
1845    }
1846
1847    // -------------------------------------------------------------------------
1848    // FillExt / PatternFillExt tests
1849    // -------------------------------------------------------------------------
1850
1851    #[cfg(feature = "sml-styling")]
1852    #[test]
1853    fn test_fill_ext_pattern() {
1854        use crate::workbook::bootstrap;
1855        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1856        <fill xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1857            <patternFill patternType="solid"/>
1858        </fill>"#;
1859        let fill: crate::types::Fill = bootstrap(xml).expect("parse failed");
1860        assert!(fill.has_fill());
1861        assert!(fill.pattern_fill().is_some());
1862        assert!(fill.gradient_fill().is_none());
1863    }
1864
1865    #[cfg(feature = "sml-styling")]
1866    #[test]
1867    fn test_fill_ext_no_fill() {
1868        use crate::workbook::bootstrap;
1869        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1870        <fill xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1871            <patternFill patternType="none"/>
1872        </fill>"#;
1873        let fill: crate::types::Fill = bootstrap(xml).expect("parse failed");
1874        assert!(fill.has_fill()); // has a pattern fill element, even if "none"
1875        let pf = fill.pattern_fill().unwrap();
1876        use crate::ext::PatternFillExt;
1877        use crate::types::PatternType;
1878        assert_eq!(pf.pattern_type(), Some(PatternType::None));
1879    }
1880
1881    // -------------------------------------------------------------------------
1882    // BorderExt tests
1883    // -------------------------------------------------------------------------
1884
1885    #[cfg(feature = "sml-styling")]
1886    #[test]
1887    fn test_border_ext_diagonal_flags() {
1888        use crate::workbook::bootstrap;
1889        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1890        <border diagonalUp="1" diagonalDown="0"
1891                xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1892        </border>"#;
1893        let border: crate::types::Border = bootstrap(xml).expect("parse failed");
1894        assert!(border.is_diagonal_up());
1895        assert!(!border.is_diagonal_down());
1896        assert!(border.left_border().is_none());
1897        assert!(border.diagonal_border().is_none());
1898    }
1899
1900    // -------------------------------------------------------------------------
1901    // CellAlignmentExt tests
1902    // -------------------------------------------------------------------------
1903
1904    #[cfg(feature = "sml-styling")]
1905    #[test]
1906    fn test_cell_alignment_ext() {
1907        use crate::workbook::bootstrap;
1908        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1909        <alignment horizontal="center" vertical="bottom" wrapText="1" indent="2"
1910                   xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1911        "#;
1912        let align: crate::types::CellAlignment = bootstrap(xml).expect("parse failed");
1913        use crate::types::{HorizontalAlignment, VerticalAlignment};
1914        assert_eq!(
1915            align.horizontal_alignment(),
1916            Some(HorizontalAlignment::Center)
1917        );
1918        assert_eq!(align.vertical_alignment(), Some(VerticalAlignment::Bottom));
1919        assert!(align.is_wrap_text());
1920        assert!(!align.is_shrink_to_fit());
1921        assert_eq!(align.indent(), Some(2));
1922    }
1923
1924    // -------------------------------------------------------------------------
1925    // FormatExt tests
1926    // -------------------------------------------------------------------------
1927
1928    #[cfg(feature = "sml-styling")]
1929    #[test]
1930    fn test_format_ext_ids() {
1931        use crate::workbook::bootstrap;
1932        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1933        <xf numFmtId="4" fontId="1" fillId="2" borderId="3" applyFont="1" applyFill="0"
1934            xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"/>
1935        "#;
1936        let fmt: crate::types::Format = bootstrap(xml).expect("parse failed");
1937        use crate::ext::FormatExt;
1938        assert_eq!(fmt.number_format_id(), 4);
1939        assert_eq!(fmt.font_id(), 1);
1940        assert_eq!(fmt.fill_id(), 2);
1941        assert_eq!(fmt.border_id(), 3);
1942        assert!(fmt.apply_font());
1943        assert!(!fmt.apply_fill());
1944        assert!(fmt.alignment().is_none());
1945    }
1946
1947    // -------------------------------------------------------------------------
1948    // WorksheetExt row/column dimension tests
1949    // -------------------------------------------------------------------------
1950
1951    #[cfg(feature = "sml-styling")]
1952    #[test]
1953    fn test_worksheet_get_row_height() {
1954        let xml = br#"<?xml version="1.0" encoding="UTF-8"?>
1955        <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
1956            <sheetData>
1957                <row r="1" ht="20" customHeight="1">
1958                    <c r="A1"><v>1</v></c>
1959                </row>
1960                <row r="2">
1961                    <c r="A2"><v>2</v></c>
1962                </row>
1963            </sheetData>
1964        </worksheet>"#;
1965        let ws = parse_worksheet(xml).expect("parse failed");
1966        assert_eq!(ws.get_row_height(1), Some(20.0));
1967        assert_eq!(ws.get_row_height(2), None); // no explicit height
1968        assert_eq!(ws.get_row_height(99), None); // row doesn't exist
1969    }
1970}