Skip to main content

sheetkit_xml/
pivot_table.rs

1//! Pivot table XML schema structures.
2//!
3//! Represents `xl/pivotTables/pivotTable{N}.xml` in the OOXML package.
4
5use serde::{Deserialize, Serialize};
6
7/// Root element for a pivot table definition.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename = "pivotTableDefinition")]
10pub struct PivotTableDefinition {
11    #[serde(rename = "@xmlns")]
12    pub xmlns: String,
13
14    #[serde(rename = "@name")]
15    pub name: String,
16
17    #[serde(rename = "@cacheId")]
18    pub cache_id: u32,
19
20    #[serde(rename = "@dataOnRows", skip_serializing_if = "Option::is_none")]
21    pub data_on_rows: Option<bool>,
22
23    #[serde(
24        rename = "@applyNumberFormats",
25        skip_serializing_if = "Option::is_none"
26    )]
27    pub apply_number_formats: Option<bool>,
28
29    #[serde(
30        rename = "@applyBorderFormats",
31        skip_serializing_if = "Option::is_none"
32    )]
33    pub apply_border_formats: Option<bool>,
34
35    #[serde(rename = "@applyFontFormats", skip_serializing_if = "Option::is_none")]
36    pub apply_font_formats: Option<bool>,
37
38    #[serde(
39        rename = "@applyPatternFormats",
40        skip_serializing_if = "Option::is_none"
41    )]
42    pub apply_pattern_formats: Option<bool>,
43
44    #[serde(
45        rename = "@applyAlignmentFormats",
46        skip_serializing_if = "Option::is_none"
47    )]
48    pub apply_alignment_formats: Option<bool>,
49
50    #[serde(
51        rename = "@applyWidthHeightFormats",
52        skip_serializing_if = "Option::is_none"
53    )]
54    pub apply_width_height_formats: Option<bool>,
55
56    #[serde(rename = "location")]
57    pub location: PivotLocation,
58
59    #[serde(rename = "pivotFields")]
60    pub pivot_fields: PivotFields,
61
62    #[serde(rename = "rowFields", skip_serializing_if = "Option::is_none")]
63    pub row_fields: Option<FieldList>,
64
65    #[serde(rename = "colFields", skip_serializing_if = "Option::is_none")]
66    pub col_fields: Option<FieldList>,
67
68    #[serde(rename = "dataFields", skip_serializing_if = "Option::is_none")]
69    pub data_fields: Option<DataFields>,
70}
71
72/// Location of the pivot table within the worksheet.
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct PivotLocation {
75    #[serde(rename = "@ref")]
76    pub reference: String,
77
78    #[serde(rename = "@firstHeaderRow")]
79    pub first_header_row: u32,
80
81    #[serde(rename = "@firstDataRow")]
82    pub first_data_row: u32,
83
84    #[serde(rename = "@firstDataCol")]
85    pub first_data_col: u32,
86}
87
88/// Container for pivot field definitions.
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct PivotFields {
91    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
92    pub count: Option<u32>,
93
94    #[serde(rename = "pivotField", default)]
95    pub fields: Vec<PivotFieldDef>,
96}
97
98/// Individual pivot field definition.
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct PivotFieldDef {
101    #[serde(rename = "@axis", skip_serializing_if = "Option::is_none")]
102    pub axis: Option<String>,
103
104    #[serde(rename = "@dataField", skip_serializing_if = "Option::is_none")]
105    pub data_field: Option<bool>,
106
107    #[serde(rename = "@showAll", skip_serializing_if = "Option::is_none")]
108    pub show_all: Option<bool>,
109
110    #[serde(rename = "items", skip_serializing_if = "Option::is_none")]
111    pub items: Option<FieldItems>,
112}
113
114/// Container for field items.
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
116pub struct FieldItems {
117    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
118    pub count: Option<u32>,
119
120    #[serde(rename = "item", default)]
121    pub items: Vec<FieldItem>,
122}
123
124/// Individual field item entry.
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct FieldItem {
127    #[serde(rename = "@t", skip_serializing_if = "Option::is_none")]
128    pub item_type: Option<String>,
129
130    #[serde(rename = "@x", skip_serializing_if = "Option::is_none")]
131    pub index: Option<u32>,
132}
133
134/// List of field references (used for row and column fields).
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct FieldList {
137    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
138    pub count: Option<u32>,
139
140    #[serde(rename = "field", default)]
141    pub fields: Vec<FieldRef>,
142}
143
144/// Reference to a pivot field by index.
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct FieldRef {
147    #[serde(rename = "@x")]
148    pub index: i32,
149}
150
151/// Container for data fields.
152#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct DataFields {
154    #[serde(rename = "@count", skip_serializing_if = "Option::is_none")]
155    pub count: Option<u32>,
156
157    #[serde(rename = "dataField", default)]
158    pub fields: Vec<DataFieldDef>,
159}
160
161/// Individual data field definition.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct DataFieldDef {
164    #[serde(rename = "@name", skip_serializing_if = "Option::is_none")]
165    pub name: Option<String>,
166
167    #[serde(rename = "@fld")]
168    pub field_index: u32,
169
170    #[serde(rename = "@subtotal", skip_serializing_if = "Option::is_none")]
171    pub subtotal: Option<String>,
172
173    #[serde(rename = "@baseField", skip_serializing_if = "Option::is_none")]
174    pub base_field: Option<i32>,
175
176    #[serde(rename = "@baseItem", skip_serializing_if = "Option::is_none")]
177    pub base_item: Option<u32>,
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_pivot_location_roundtrip() {
186        let loc = PivotLocation {
187            reference: "A3:D20".to_string(),
188            first_header_row: 1,
189            first_data_row: 2,
190            first_data_col: 1,
191        };
192        let xml = quick_xml::se::to_string(&loc).unwrap();
193        let parsed: PivotLocation = quick_xml::de::from_str(&xml).unwrap();
194        assert_eq!(loc, parsed);
195    }
196
197    #[test]
198    fn test_field_item_roundtrip() {
199        let item = FieldItem {
200            item_type: Some("default".to_string()),
201            index: Some(0),
202        };
203        let xml = quick_xml::se::to_string(&item).unwrap();
204        let parsed: FieldItem = quick_xml::de::from_str(&xml).unwrap();
205        assert_eq!(item, parsed);
206    }
207
208    #[test]
209    fn test_field_item_optional_fields_skipped() {
210        let item = FieldItem {
211            item_type: None,
212            index: Some(3),
213        };
214        let xml = quick_xml::se::to_string(&item).unwrap();
215        assert!(!xml.contains("t="));
216        assert!(xml.contains("x=\"3\""));
217    }
218
219    #[test]
220    fn test_field_items_roundtrip() {
221        let items = FieldItems {
222            count: Some(2),
223            items: vec![
224                FieldItem {
225                    item_type: None,
226                    index: Some(0),
227                },
228                FieldItem {
229                    item_type: Some("default".to_string()),
230                    index: None,
231                },
232            ],
233        };
234        let xml = quick_xml::se::to_string(&items).unwrap();
235        let parsed: FieldItems = quick_xml::de::from_str(&xml).unwrap();
236        assert_eq!(items, parsed);
237    }
238
239    #[test]
240    fn test_pivot_field_def_roundtrip() {
241        let field = PivotFieldDef {
242            axis: Some("axisRow".to_string()),
243            data_field: None,
244            show_all: Some(false),
245            items: Some(FieldItems {
246                count: Some(1),
247                items: vec![FieldItem {
248                    item_type: Some("default".to_string()),
249                    index: None,
250                }],
251            }),
252        };
253        let xml = quick_xml::se::to_string(&field).unwrap();
254        let parsed: PivotFieldDef = quick_xml::de::from_str(&xml).unwrap();
255        assert_eq!(field, parsed);
256    }
257
258    #[test]
259    fn test_pivot_field_def_no_axis() {
260        let field = PivotFieldDef {
261            axis: None,
262            data_field: Some(true),
263            show_all: Some(false),
264            items: None,
265        };
266        let xml = quick_xml::se::to_string(&field).unwrap();
267        assert!(!xml.contains("axis="));
268        assert!(xml.contains("dataField=\"true\""));
269    }
270
271    #[test]
272    fn test_pivot_fields_roundtrip() {
273        let fields = PivotFields {
274            count: Some(2),
275            fields: vec![
276                PivotFieldDef {
277                    axis: Some("axisRow".to_string()),
278                    data_field: None,
279                    show_all: Some(false),
280                    items: None,
281                },
282                PivotFieldDef {
283                    axis: None,
284                    data_field: Some(true),
285                    show_all: Some(false),
286                    items: None,
287                },
288            ],
289        };
290        let xml = quick_xml::se::to_string(&fields).unwrap();
291        let parsed: PivotFields = quick_xml::de::from_str(&xml).unwrap();
292        assert_eq!(fields, parsed);
293    }
294
295    #[test]
296    fn test_field_ref_roundtrip() {
297        let field_ref = FieldRef { index: 2 };
298        let xml = quick_xml::se::to_string(&field_ref).unwrap();
299        let parsed: FieldRef = quick_xml::de::from_str(&xml).unwrap();
300        assert_eq!(field_ref, parsed);
301    }
302
303    #[test]
304    fn test_field_ref_negative_index() {
305        let field_ref = FieldRef { index: -2 };
306        let xml = quick_xml::se::to_string(&field_ref).unwrap();
307        let parsed: FieldRef = quick_xml::de::from_str(&xml).unwrap();
308        assert_eq!(parsed.index, -2);
309    }
310
311    #[test]
312    fn test_field_list_roundtrip() {
313        let list = FieldList {
314            count: Some(2),
315            fields: vec![FieldRef { index: 0 }, FieldRef { index: 3 }],
316        };
317        let xml = quick_xml::se::to_string(&list).unwrap();
318        let parsed: FieldList = quick_xml::de::from_str(&xml).unwrap();
319        assert_eq!(list, parsed);
320    }
321
322    #[test]
323    fn test_data_field_def_roundtrip() {
324        let data_field = DataFieldDef {
325            name: Some("Sum of Sales".to_string()),
326            field_index: 2,
327            subtotal: Some("sum".to_string()),
328            base_field: Some(0),
329            base_item: Some(0),
330        };
331        let xml = quick_xml::se::to_string(&data_field).unwrap();
332        let parsed: DataFieldDef = quick_xml::de::from_str(&xml).unwrap();
333        assert_eq!(data_field, parsed);
334    }
335
336    #[test]
337    fn test_data_field_def_optional_fields_skipped() {
338        let data_field = DataFieldDef {
339            name: None,
340            field_index: 1,
341            subtotal: None,
342            base_field: None,
343            base_item: None,
344        };
345        let xml = quick_xml::se::to_string(&data_field).unwrap();
346        assert!(!xml.contains("name="));
347        assert!(!xml.contains("subtotal="));
348        assert!(!xml.contains("baseField="));
349        assert!(!xml.contains("baseItem="));
350        assert!(xml.contains("fld=\"1\""));
351    }
352
353    #[test]
354    fn test_data_fields_roundtrip() {
355        let data_fields = DataFields {
356            count: Some(1),
357            fields: vec![DataFieldDef {
358                name: Some("Count of Items".to_string()),
359                field_index: 0,
360                subtotal: Some("count".to_string()),
361                base_field: Some(0),
362                base_item: Some(0),
363            }],
364        };
365        let xml = quick_xml::se::to_string(&data_fields).unwrap();
366        let parsed: DataFields = quick_xml::de::from_str(&xml).unwrap();
367        assert_eq!(data_fields, parsed);
368    }
369
370    #[test]
371    fn test_pivot_table_definition_minimal_roundtrip() {
372        let def = PivotTableDefinition {
373            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
374            name: "PivotTable1".to_string(),
375            cache_id: 0,
376            data_on_rows: None,
377            apply_number_formats: None,
378            apply_border_formats: None,
379            apply_font_formats: None,
380            apply_pattern_formats: None,
381            apply_alignment_formats: None,
382            apply_width_height_formats: None,
383            location: PivotLocation {
384                reference: "A3:C20".to_string(),
385                first_header_row: 1,
386                first_data_row: 1,
387                first_data_col: 1,
388            },
389            pivot_fields: PivotFields {
390                count: Some(2),
391                fields: vec![
392                    PivotFieldDef {
393                        axis: Some("axisRow".to_string()),
394                        data_field: None,
395                        show_all: Some(false),
396                        items: None,
397                    },
398                    PivotFieldDef {
399                        axis: None,
400                        data_field: Some(true),
401                        show_all: Some(false),
402                        items: None,
403                    },
404                ],
405            },
406            row_fields: Some(FieldList {
407                count: Some(1),
408                fields: vec![FieldRef { index: 0 }],
409            }),
410            col_fields: None,
411            data_fields: Some(DataFields {
412                count: Some(1),
413                fields: vec![DataFieldDef {
414                    name: Some("Sum of Amount".to_string()),
415                    field_index: 1,
416                    subtotal: Some("sum".to_string()),
417                    base_field: Some(0),
418                    base_item: Some(0),
419                }],
420            }),
421        };
422        let xml = quick_xml::se::to_string(&def).unwrap();
423        let parsed: PivotTableDefinition = quick_xml::de::from_str(&xml).unwrap();
424        assert_eq!(def, parsed);
425    }
426
427    #[test]
428    fn test_pivot_table_definition_full_roundtrip() {
429        let def = PivotTableDefinition {
430            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
431            name: "SalesReport".to_string(),
432            cache_id: 1,
433            data_on_rows: Some(false),
434            apply_number_formats: Some(false),
435            apply_border_formats: Some(false),
436            apply_font_formats: Some(false),
437            apply_pattern_formats: Some(false),
438            apply_alignment_formats: Some(false),
439            apply_width_height_formats: Some(true),
440            location: PivotLocation {
441                reference: "A1:E30".to_string(),
442                first_header_row: 1,
443                first_data_row: 2,
444                first_data_col: 1,
445            },
446            pivot_fields: PivotFields {
447                count: Some(3),
448                fields: vec![
449                    PivotFieldDef {
450                        axis: Some("axisRow".to_string()),
451                        data_field: None,
452                        show_all: Some(false),
453                        items: Some(FieldItems {
454                            count: Some(2),
455                            items: vec![
456                                FieldItem {
457                                    item_type: None,
458                                    index: Some(0),
459                                },
460                                FieldItem {
461                                    item_type: Some("default".to_string()),
462                                    index: None,
463                                },
464                            ],
465                        }),
466                    },
467                    PivotFieldDef {
468                        axis: Some("axisCol".to_string()),
469                        data_field: None,
470                        show_all: Some(false),
471                        items: None,
472                    },
473                    PivotFieldDef {
474                        axis: None,
475                        data_field: Some(true),
476                        show_all: Some(false),
477                        items: None,
478                    },
479                ],
480            },
481            row_fields: Some(FieldList {
482                count: Some(1),
483                fields: vec![FieldRef { index: 0 }],
484            }),
485            col_fields: Some(FieldList {
486                count: Some(1),
487                fields: vec![FieldRef { index: 1 }],
488            }),
489            data_fields: Some(DataFields {
490                count: Some(1),
491                fields: vec![DataFieldDef {
492                    name: Some("Sum of Revenue".to_string()),
493                    field_index: 2,
494                    subtotal: Some("sum".to_string()),
495                    base_field: Some(0),
496                    base_item: Some(0),
497                }],
498            }),
499        };
500        let xml = quick_xml::se::to_string(&def).unwrap();
501        let parsed: PivotTableDefinition = quick_xml::de::from_str(&xml).unwrap();
502        assert_eq!(def, parsed);
503    }
504
505    #[test]
506    fn test_pivot_table_definition_serialization_structure() {
507        let def = PivotTableDefinition {
508            xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main".to_string(),
509            name: "TestPivot".to_string(),
510            cache_id: 0,
511            data_on_rows: Some(false),
512            apply_number_formats: None,
513            apply_border_formats: None,
514            apply_font_formats: None,
515            apply_pattern_formats: None,
516            apply_alignment_formats: None,
517            apply_width_height_formats: None,
518            location: PivotLocation {
519                reference: "A1".to_string(),
520                first_header_row: 1,
521                first_data_row: 1,
522                first_data_col: 1,
523            },
524            pivot_fields: PivotFields {
525                count: Some(0),
526                fields: vec![],
527            },
528            row_fields: None,
529            col_fields: None,
530            data_fields: None,
531        };
532        let xml = quick_xml::se::to_string(&def).unwrap();
533        assert!(xml.contains("<pivotTableDefinition"));
534        assert!(xml.contains("name=\"TestPivot\""));
535        assert!(xml.contains("cacheId=\"0\""));
536        assert!(xml.contains("<location"));
537        assert!(xml.contains("<pivotFields"));
538        assert!(!xml.contains("<rowFields"));
539        assert!(!xml.contains("<colFields"));
540        assert!(!xml.contains("<dataFields"));
541    }
542
543    #[test]
544    fn test_empty_pivot_fields() {
545        let fields = PivotFields {
546            count: Some(0),
547            fields: vec![],
548        };
549        let xml = quick_xml::se::to_string(&fields).unwrap();
550        let parsed: PivotFields = quick_xml::de::from_str(&xml).unwrap();
551        assert_eq!(parsed.count, Some(0));
552        assert!(parsed.fields.is_empty());
553    }
554
555    #[test]
556    fn test_empty_field_list() {
557        let list = FieldList {
558            count: Some(0),
559            fields: vec![],
560        };
561        let xml = quick_xml::se::to_string(&list).unwrap();
562        let parsed: FieldList = quick_xml::de::from_str(&xml).unwrap();
563        assert_eq!(parsed.count, Some(0));
564        assert!(parsed.fields.is_empty());
565    }
566
567    #[test]
568    fn test_empty_data_fields() {
569        let data_fields = DataFields {
570            count: Some(0),
571            fields: vec![],
572        };
573        let xml = quick_xml::se::to_string(&data_fields).unwrap();
574        let parsed: DataFields = quick_xml::de::from_str(&xml).unwrap();
575        assert_eq!(parsed.count, Some(0));
576        assert!(parsed.fields.is_empty());
577    }
578}