Skip to main content

sheetkit_core/
slicer.rs

1//! Slicer configuration and management.
2//!
3//! Provides types and logic for adding, retrieving, and deleting slicers
4//! on tables. Slicers are visual filter controls introduced in Excel 2010.
5
6use crate::error::Result;
7
8/// Configuration for adding a slicer to a table.
9#[derive(Debug, Clone)]
10pub struct SlicerConfig {
11    /// Unique slicer name.
12    pub name: String,
13    /// Anchor cell (top-left corner of the slicer, e.g. "F1").
14    pub cell: String,
15    /// Source table name.
16    pub table_name: String,
17    /// Column name from the table to filter.
18    pub column_name: String,
19    /// Caption displayed on the slicer header. Defaults to column_name.
20    pub caption: Option<String>,
21    /// Slicer visual style (e.g. "SlicerStyleLight1").
22    pub style: Option<String>,
23    /// Width in pixels. Defaults to 200.
24    pub width: Option<u32>,
25    /// Height in pixels. Defaults to 200.
26    pub height: Option<u32>,
27    /// Whether to show the caption header.
28    pub show_caption: Option<bool>,
29    /// Number of columns in the slicer item display.
30    pub column_count: Option<u32>,
31}
32
33/// Information about an existing slicer, returned by `get_slicers`.
34#[derive(Debug, Clone)]
35pub struct SlicerInfo {
36    /// The slicer's unique name.
37    pub name: String,
38    /// The display caption.
39    pub caption: String,
40    /// The source table name.
41    pub table_name: String,
42    /// The column name being filtered.
43    pub column_name: String,
44    /// The visual style name, if set.
45    pub style: Option<String>,
46}
47
48/// Default slicer row height in EMU (241300 EMU = approx 19pt).
49pub const DEFAULT_ROW_HEIGHT_EMU: u32 = 241300;
50
51/// Default slicer width in pixels.
52pub const DEFAULT_WIDTH_PX: u32 = 200;
53
54/// Default slicer height in pixels: u32.
55pub const DEFAULT_HEIGHT_PX: u32 = 200;
56
57/// Pixels to EMU conversion factor (1 pixel = 9525 EMU at 96 DPI).
58pub const PX_TO_EMU: u64 = 9525;
59
60/// Validate a slicer config.
61pub fn validate_slicer_config(config: &SlicerConfig) -> Result<()> {
62    if config.name.is_empty() {
63        return Err(crate::error::Error::InvalidArgument(
64            "slicer name cannot be empty".to_string(),
65        ));
66    }
67    if config.cell.is_empty() {
68        return Err(crate::error::Error::InvalidArgument(
69            "slicer anchor cell cannot be empty".to_string(),
70        ));
71    }
72    if config.table_name.is_empty() {
73        return Err(crate::error::Error::InvalidArgument(
74            "slicer table_name cannot be empty".to_string(),
75        ));
76    }
77    if config.column_name.is_empty() {
78        return Err(crate::error::Error::InvalidArgument(
79            "slicer column_name cannot be empty".to_string(),
80        ));
81    }
82    // Validate cell reference.
83    crate::utils::cell_ref::cell_name_to_coordinates(&config.cell)?;
84    Ok(())
85}
86
87/// Generate the sanitized cache name from a slicer name.
88pub fn slicer_cache_name(name: &str) -> String {
89    format!("Slicer_{}", name.replace(' ', "_"))
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_validate_slicer_config_valid() {
98        let config = SlicerConfig {
99            name: "StatusFilter".to_string(),
100            cell: "F1".to_string(),
101            table_name: "Table1".to_string(),
102            column_name: "Status".to_string(),
103            caption: None,
104            style: None,
105            width: None,
106            height: None,
107            show_caption: None,
108            column_count: None,
109        };
110        assert!(validate_slicer_config(&config).is_ok());
111    }
112
113    #[test]
114    fn test_validate_slicer_config_empty_name() {
115        let config = SlicerConfig {
116            name: "".to_string(),
117            cell: "A1".to_string(),
118            table_name: "T".to_string(),
119            column_name: "C".to_string(),
120            caption: None,
121            style: None,
122            width: None,
123            height: None,
124            show_caption: None,
125            column_count: None,
126        };
127        let err = validate_slicer_config(&config).unwrap_err();
128        assert!(err.to_string().contains("name cannot be empty"));
129    }
130
131    #[test]
132    fn test_validate_slicer_config_empty_cell() {
133        let config = SlicerConfig {
134            name: "S1".to_string(),
135            cell: "".to_string(),
136            table_name: "T".to_string(),
137            column_name: "C".to_string(),
138            caption: None,
139            style: None,
140            width: None,
141            height: None,
142            show_caption: None,
143            column_count: None,
144        };
145        let err = validate_slicer_config(&config).unwrap_err();
146        assert!(err.to_string().contains("cell cannot be empty"));
147    }
148
149    #[test]
150    fn test_validate_slicer_config_empty_table() {
151        let config = SlicerConfig {
152            name: "S1".to_string(),
153            cell: "A1".to_string(),
154            table_name: "".to_string(),
155            column_name: "C".to_string(),
156            caption: None,
157            style: None,
158            width: None,
159            height: None,
160            show_caption: None,
161            column_count: None,
162        };
163        let err = validate_slicer_config(&config).unwrap_err();
164        assert!(err.to_string().contains("table_name cannot be empty"));
165    }
166
167    #[test]
168    fn test_validate_slicer_config_empty_column() {
169        let config = SlicerConfig {
170            name: "S1".to_string(),
171            cell: "A1".to_string(),
172            table_name: "T".to_string(),
173            column_name: "".to_string(),
174            caption: None,
175            style: None,
176            width: None,
177            height: None,
178            show_caption: None,
179            column_count: None,
180        };
181        let err = validate_slicer_config(&config).unwrap_err();
182        assert!(err.to_string().contains("column_name cannot be empty"));
183    }
184
185    #[test]
186    fn test_validate_slicer_config_invalid_cell() {
187        let config = SlicerConfig {
188            name: "S1".to_string(),
189            cell: "XYZ0".to_string(),
190            table_name: "T".to_string(),
191            column_name: "C".to_string(),
192            caption: None,
193            style: None,
194            width: None,
195            height: None,
196            show_caption: None,
197            column_count: None,
198        };
199        assert!(validate_slicer_config(&config).is_err());
200    }
201
202    #[test]
203    fn test_slicer_cache_name() {
204        assert_eq!(slicer_cache_name("Category"), "Slicer_Category");
205        assert_eq!(slicer_cache_name("My Filter"), "Slicer_My_Filter");
206    }
207
208    #[test]
209    fn test_slicer_info_struct() {
210        let info = SlicerInfo {
211            name: "StatusFilter".to_string(),
212            caption: "Status".to_string(),
213            table_name: "Table1".to_string(),
214            column_name: "Status".to_string(),
215            style: Some("SlicerStyleLight1".to_string()),
216        };
217        assert_eq!(info.name, "StatusFilter");
218        assert_eq!(info.column_name, "Status");
219    }
220}