1use super::*;
2
3impl Workbook {
4 pub fn add_pivot_table(&mut self, config: &PivotTableConfig) -> Result<()> {
10 let _src_idx = self.sheet_index(&config.source_sheet)?;
12
13 let target_idx = self.sheet_index(&config.target_sheet)?;
15
16 if self
18 .pivot_tables
19 .iter()
20 .any(|(_, pt)| pt.name == config.name)
21 {
22 return Err(Error::PivotTableAlreadyExists {
23 name: config.name.clone(),
24 });
25 }
26
27 let field_names = self.read_header_row(&config.source_sheet, &config.source_range)?;
29 if field_names.is_empty() {
30 return Err(Error::InvalidSourceRange(
31 "source range header row is empty".to_string(),
32 ));
33 }
34
35 let cache_id = self
37 .pivot_tables
38 .iter()
39 .map(|(_, pt)| pt.cache_id)
40 .max()
41 .map(|m| m + 1)
42 .unwrap_or(0);
43
44 let pt_def = crate::pivot::build_pivot_table_xml(config, cache_id, &field_names)?;
46 let pcd = crate::pivot::build_pivot_cache_definition(
47 &config.source_sheet,
48 &config.source_range,
49 &field_names,
50 );
51 let pcr = sheetkit_xml::pivot_cache::PivotCacheRecords {
52 xmlns: sheetkit_xml::namespaces::SPREADSHEET_ML.to_string(),
53 xmlns_r: sheetkit_xml::namespaces::RELATIONSHIPS.to_string(),
54 count: Some(0),
55 records: vec![],
56 };
57
58 let pt_num = self.pivot_tables.len() + 1;
60 let cache_num = self.pivot_cache_defs.len() + 1;
61
62 let pt_path = format!("xl/pivotTables/pivotTable{}.xml", pt_num);
63 let pcd_path = format!("xl/pivotCache/pivotCacheDefinition{}.xml", cache_num);
64 let pcr_path = format!("xl/pivotCache/pivotCacheRecords{}.xml", cache_num);
65
66 self.pivot_tables.push((pt_path.clone(), pt_def));
68 self.pivot_cache_defs.push((pcd_path.clone(), pcd));
69 self.pivot_cache_records.push((pcr_path.clone(), pcr));
70
71 self.content_types.overrides.push(ContentTypeOverride {
73 part_name: format!("/{}", pt_path),
74 content_type: mime_types::PIVOT_TABLE.to_string(),
75 });
76 self.content_types.overrides.push(ContentTypeOverride {
77 part_name: format!("/{}", pcd_path),
78 content_type: mime_types::PIVOT_CACHE_DEFINITION.to_string(),
79 });
80 self.content_types.overrides.push(ContentTypeOverride {
81 part_name: format!("/{}", pcr_path),
82 content_type: mime_types::PIVOT_CACHE_RECORDS.to_string(),
83 });
84
85 let wb_rid = crate::sheet::next_rid(&self.workbook_rels.relationships);
87 self.workbook_rels.relationships.push(Relationship {
88 id: wb_rid.clone(),
89 rel_type: rel_types::PIVOT_CACHE_DEF.to_string(),
90 target: format!("pivotCache/pivotCacheDefinition{}.xml", cache_num),
91 target_mode: None,
92 });
93
94 let pivot_caches = self
96 .workbook_xml
97 .pivot_caches
98 .get_or_insert_with(|| sheetkit_xml::workbook::PivotCaches { caches: vec![] });
99 pivot_caches
100 .caches
101 .push(sheetkit_xml::workbook::PivotCacheEntry {
102 cache_id,
103 r_id: wb_rid,
104 });
105
106 let ws_rid = self.next_worksheet_rid(target_idx);
108 let ws_rels = self
109 .worksheet_rels
110 .entry(target_idx)
111 .or_insert_with(|| Relationships {
112 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
113 relationships: vec![],
114 });
115 ws_rels.relationships.push(Relationship {
116 id: ws_rid,
117 rel_type: rel_types::PIVOT_TABLE.to_string(),
118 target: format!("../pivotTables/pivotTable{}.xml", pt_num),
119 target_mode: None,
120 });
121
122 Ok(())
123 }
124
125 pub fn get_pivot_tables(&self) -> Vec<PivotTableInfo> {
127 self.pivot_tables
128 .iter()
129 .map(|(_path, pt)| {
130 let (source_sheet, source_range) = self
132 .pivot_cache_defs
133 .iter()
134 .enumerate()
135 .find(|(i, _)| {
136 self.workbook_xml
137 .pivot_caches
138 .as_ref()
139 .and_then(|pc| pc.caches.iter().find(|e| e.cache_id == pt.cache_id))
140 .is_some()
141 || *i == pt.cache_id as usize
142 })
143 .and_then(|(_, (_, pcd))| {
144 pcd.cache_source
145 .worksheet_source
146 .as_ref()
147 .map(|ws| (ws.sheet.clone(), ws.reference.clone()))
148 })
149 .unwrap_or_default();
150
151 let target_sheet = self.find_pivot_table_target_sheet(pt).unwrap_or_default();
153
154 PivotTableInfo {
155 name: pt.name.clone(),
156 source_sheet,
157 source_range,
158 target_sheet,
159 location: pt.location.reference.clone(),
160 }
161 })
162 .collect()
163 }
164
165 pub fn delete_pivot_table(&mut self, name: &str) -> Result<()> {
167 let pt_idx = self
169 .pivot_tables
170 .iter()
171 .position(|(_, pt)| pt.name == name)
172 .ok_or_else(|| Error::PivotTableNotFound {
173 name: name.to_string(),
174 })?;
175
176 let (pt_path, pt_def) = self.pivot_tables.remove(pt_idx);
177 let cache_id = pt_def.cache_id;
178
179 let mut wb_cache_rid = None;
182 if let Some(ref mut pivot_caches) = self.workbook_xml.pivot_caches {
183 if let Some(pos) = pivot_caches
184 .caches
185 .iter()
186 .position(|e| e.cache_id == cache_id)
187 {
188 wb_cache_rid = Some(pivot_caches.caches[pos].r_id.clone());
189 pivot_caches.caches.remove(pos);
190 }
191 if pivot_caches.caches.is_empty() {
192 self.workbook_xml.pivot_caches = None;
193 }
194 }
195
196 if let Some(ref rid) = wb_cache_rid {
198 if let Some(rel) = self
200 .workbook_rels
201 .relationships
202 .iter()
203 .find(|r| r.id == *rid)
204 {
205 let target_path = format!("xl/{}", rel.target);
206 self.pivot_cache_defs.retain(|(p, _)| *p != target_path);
207
208 let records_path = target_path.replace("pivotCacheDefinition", "pivotCacheRecords");
210 self.pivot_cache_records.retain(|(p, _)| *p != records_path);
211 }
212 self.workbook_rels.relationships.retain(|r| r.id != *rid);
213 }
214
215 let pt_part = format!("/{}", pt_path);
217 self.content_types
218 .overrides
219 .retain(|o| o.part_name != pt_part);
220
221 self.content_types.overrides.retain(|o| {
223 let p = o.part_name.trim_start_matches('/');
224 if o.content_type == mime_types::PIVOT_CACHE_DEFINITION {
226 return self.pivot_cache_defs.iter().any(|(path, _)| path == p);
227 }
228 if o.content_type == mime_types::PIVOT_CACHE_RECORDS {
229 return self.pivot_cache_records.iter().any(|(path, _)| path == p);
230 }
231 if o.content_type == mime_types::PIVOT_TABLE {
232 return self.pivot_tables.iter().any(|(path, _)| path == p);
233 }
234 true
235 });
236
237 for (_idx, rels) in self.worksheet_rels.iter_mut() {
239 rels.relationships.retain(|r| {
240 if r.rel_type != rel_types::PIVOT_TABLE {
241 return true;
242 }
243 let full_target = format!(
245 "xl/pivotTables/{}",
246 r.target.trim_start_matches("../pivotTables/")
247 );
248 full_target != pt_path
249 });
250 }
251
252 Ok(())
253 }
254
255 pub fn add_sparkline(
257 &mut self,
258 sheet: &str,
259 config: &crate::sparkline::SparklineConfig,
260 ) -> Result<()> {
261 let idx = self.sheet_index(sheet)?;
262 crate::sparkline::validate_sparkline_config(config)?;
263 while self.sheet_sparklines.len() <= idx {
264 self.sheet_sparklines.push(vec![]);
265 }
266 self.sheet_sparklines[idx].push(config.clone());
267 Ok(())
268 }
269
270 pub fn get_sparklines(&self, sheet: &str) -> Result<Vec<crate::sparkline::SparklineConfig>> {
272 let idx = self.sheet_index(sheet)?;
273 Ok(self.sheet_sparklines.get(idx).cloned().unwrap_or_default())
274 }
275
276 pub fn remove_sparkline(&mut self, sheet: &str, location: &str) -> Result<()> {
278 let idx = self.sheet_index(sheet)?;
279 let sparklines = self
280 .sheet_sparklines
281 .get_mut(idx)
282 .ok_or_else(|| Error::Internal(format!("no sparkline data for sheet '{sheet}'")))?;
283 let pos = sparklines
284 .iter()
285 .position(|s| s.location == location)
286 .ok_or_else(|| {
287 Error::Internal(format!(
288 "sparkline at location '{location}' not found on sheet '{sheet}'"
289 ))
290 })?;
291 sparklines.remove(pos);
292 Ok(())
293 }
294
295 pub fn evaluate_formula(&self, sheet: &str, formula: &str) -> Result<CellValue> {
300 let _ = self.sheet_index(sheet)?;
302 let parsed = crate::formula::parser::parse_formula(formula)?;
303 let snapshot = self.build_cell_snapshot(sheet)?;
304 crate::formula::eval::evaluate(&parsed, &snapshot)
305 }
306
307 pub fn calculate_all(&mut self) -> Result<()> {
311 use crate::formula::eval::{build_dependency_graph, topological_sort, CellCoord};
312
313 let sheet_names: Vec<String> = self.sheet_names().iter().map(|s| s.to_string()).collect();
314
315 let mut formula_cells: Vec<(CellCoord, String)> = Vec::new();
317 for sn in &sheet_names {
318 let ws = self
319 .worksheets
320 .iter()
321 .find(|(name, _)| name == sn)
322 .map(|(_, ws)| ws)
323 .ok_or_else(|| Error::SheetNotFound {
324 name: sn.to_string(),
325 })?;
326 for row in &ws.sheet_data.rows {
327 for cell in &row.cells {
328 if let Some(ref f) = cell.f {
329 let formula_str = f.value.clone().unwrap_or_default();
330 if !formula_str.is_empty() {
331 if let Ok((c, r)) = cell_name_to_coordinates(cell.r.as_str()) {
332 formula_cells.push((
333 CellCoord {
334 sheet: sn.clone(),
335 col: c,
336 row: r,
337 },
338 formula_str,
339 ));
340 }
341 }
342 }
343 }
344 }
345 }
346
347 if formula_cells.is_empty() {
348 return Ok(());
349 }
350
351 let deps = build_dependency_graph(&formula_cells)?;
353 let coords: Vec<CellCoord> = formula_cells.iter().map(|(c, _)| c.clone()).collect();
354 let eval_order = topological_sort(&coords, &deps)?;
355
356 let formula_map: HashMap<CellCoord, String> = formula_cells.into_iter().collect();
358
359 let first_sheet = sheet_names.first().cloned().unwrap_or_default();
361 let mut snapshot = self.build_cell_snapshot(&first_sheet)?;
362
363 let mut results: Vec<(CellCoord, String, CellValue)> = Vec::new();
366 for coord in &eval_order {
367 if let Some(formula_str) = formula_map.get(coord) {
368 snapshot.set_current_sheet(&coord.sheet);
369 let parsed = crate::formula::parser::parse_formula(formula_str)?;
370 let mut evaluator = crate::formula::eval::Evaluator::new(&snapshot);
371 let result = evaluator.eval_expr(&parsed)?;
372 snapshot.set_cell(&coord.sheet, coord.col, coord.row, result.clone());
373 results.push((coord.clone(), formula_str.clone(), result));
374 }
375 }
376
377 for (coord, _formula_str, result) in results {
380 let cell_ref = crate::utils::cell_ref::coordinates_to_cell_name(coord.col, coord.row)?;
381 if let Some((_, ws)) = self.worksheets.iter_mut().find(|(n, _)| *n == coord.sheet) {
382 if let Some(row) = ws.sheet_data.rows.iter_mut().find(|r| r.r == coord.row) {
383 if let Some(cell) = row.cells.iter_mut().find(|c| c.r == *cell_ref) {
384 match &result {
385 CellValue::Number(n) => {
386 cell.v = Some(n.to_string());
387 cell.t = CellTypeTag::None;
388 }
389 CellValue::String(s) => {
390 cell.v = Some(s.clone());
391 cell.t = CellTypeTag::FormulaString;
392 }
393 CellValue::Bool(b) => {
394 cell.v = Some(if *b { "1".to_string() } else { "0".to_string() });
395 cell.t = CellTypeTag::Boolean;
396 }
397 CellValue::Error(e) => {
398 cell.v = Some(e.clone());
399 cell.t = CellTypeTag::Error;
400 }
401 CellValue::Date(n) => {
402 cell.v = Some(n.to_string());
403 cell.t = CellTypeTag::None;
404 }
405 _ => {}
406 }
407 }
408 }
409 }
410 }
411
412 Ok(())
413 }
414
415 fn build_cell_snapshot(
418 &self,
419 current_sheet: &str,
420 ) -> Result<crate::formula::eval::CellSnapshot> {
421 let mut snapshot = crate::formula::eval::CellSnapshot::new(current_sheet.to_string());
422 for (sn, ws) in &self.worksheets {
423 for row in &ws.sheet_data.rows {
424 for cell in &row.cells {
425 if let Ok((c, r)) = cell_name_to_coordinates(cell.r.as_str()) {
426 let cv = self.xml_cell_to_value(cell)?;
427 snapshot.set_cell(sn, c, r, cv);
428 }
429 }
430 }
431 }
432 Ok(snapshot)
433 }
434
435 pub fn get_occupied_cells(&self, sheet: &str) -> Result<Vec<(u32, u32)>> {
437 let ws = self
438 .worksheets
439 .iter()
440 .find(|(name, _)| name == sheet)
441 .map(|(_, ws)| ws)
442 .ok_or_else(|| Error::SheetNotFound {
443 name: sheet.to_string(),
444 })?;
445 let mut cells = Vec::new();
446 for row in &ws.sheet_data.rows {
447 for cell in &row.cells {
448 if let Ok((c, r)) = cell_name_to_coordinates(cell.r.as_str()) {
449 cells.push((c, r));
450 }
451 }
452 }
453 Ok(cells)
454 }
455
456 fn read_header_row(&self, sheet: &str, range: &str) -> Result<Vec<String>> {
459 let parts: Vec<&str> = range.split(':').collect();
460 if parts.len() != 2 {
461 return Err(Error::InvalidSourceRange(range.to_string()));
462 }
463 let (start_col, start_row) = cell_name_to_coordinates(parts[0])
464 .map_err(|_| Error::InvalidSourceRange(range.to_string()))?;
465 let (end_col, _end_row) = cell_name_to_coordinates(parts[1])
466 .map_err(|_| Error::InvalidSourceRange(range.to_string()))?;
467
468 let mut headers = Vec::new();
469 for col in start_col..=end_col {
470 let cell_name = crate::utils::cell_ref::coordinates_to_cell_name(col, start_row)?;
471 let val = self.get_cell_value(sheet, &cell_name)?;
472 let s = match val {
473 CellValue::String(s) => s,
474 CellValue::Number(n) => n.to_string(),
475 CellValue::Bool(b) => b.to_string(),
476 CellValue::RichString(runs) => crate::rich_text::rich_text_to_plain(&runs),
477 _ => String::new(),
478 };
479 headers.push(s);
480 }
481 Ok(headers)
482 }
483
484 fn find_pivot_table_target_sheet(
487 &self,
488 pt: &sheetkit_xml::pivot_table::PivotTableDefinition,
489 ) -> Option<String> {
490 let pt_path = self
492 .pivot_tables
493 .iter()
494 .find(|(_, p)| p.name == pt.name)
495 .map(|(path, _)| path.as_str())?;
496
497 for (sheet_idx, rels) in &self.worksheet_rels {
499 for r in &rels.relationships {
500 if r.rel_type == rel_types::PIVOT_TABLE {
501 let full_target = format!(
502 "xl/pivotTables/{}",
503 r.target.trim_start_matches("../pivotTables/")
504 );
505 if full_target == pt_path {
506 return self
507 .worksheets
508 .get(*sheet_idx)
509 .map(|(name, _)| name.clone());
510 }
511 }
512 }
513 }
514 None
515 }
516
517 pub fn set_doc_props(&mut self, props: crate::doc_props::DocProperties) {
519 self.core_properties = Some(props.to_core_properties());
520 self.ensure_doc_props_content_types();
521 }
522
523 pub fn get_doc_props(&self) -> crate::doc_props::DocProperties {
525 self.core_properties
526 .as_ref()
527 .map(crate::doc_props::DocProperties::from)
528 .unwrap_or_default()
529 }
530
531 pub fn set_app_props(&mut self, props: crate::doc_props::AppProperties) {
533 self.app_properties = Some(props.to_extended_properties());
534 self.ensure_doc_props_content_types();
535 }
536
537 pub fn get_app_props(&self) -> crate::doc_props::AppProperties {
539 self.app_properties
540 .as_ref()
541 .map(crate::doc_props::AppProperties::from)
542 .unwrap_or_default()
543 }
544
545 pub fn set_custom_property(
548 &mut self,
549 name: &str,
550 value: crate::doc_props::CustomPropertyValue,
551 ) {
552 let props = self
553 .custom_properties
554 .get_or_insert_with(sheetkit_xml::doc_props::CustomProperties::default);
555 crate::doc_props::set_custom_property(props, name, value);
556 self.ensure_custom_props_content_types();
557 }
558
559 pub fn get_custom_property(&self, name: &str) -> Option<crate::doc_props::CustomPropertyValue> {
561 self.custom_properties
562 .as_ref()
563 .and_then(|p| crate::doc_props::find_custom_property(p, name))
564 }
565
566 pub fn delete_custom_property(&mut self, name: &str) -> bool {
569 if let Some(ref mut props) = self.custom_properties {
570 crate::doc_props::delete_custom_property(props, name)
571 } else {
572 false
573 }
574 }
575
576 fn ensure_doc_props_content_types(&mut self) {
578 let core_part = "/docProps/core.xml";
579 let app_part = "/docProps/app.xml";
580
581 let has_core = self
582 .content_types
583 .overrides
584 .iter()
585 .any(|o| o.part_name == core_part);
586 if !has_core {
587 self.content_types.overrides.push(ContentTypeOverride {
588 part_name: core_part.to_string(),
589 content_type: mime_types::CORE_PROPERTIES.to_string(),
590 });
591 }
592
593 let has_app = self
594 .content_types
595 .overrides
596 .iter()
597 .any(|o| o.part_name == app_part);
598 if !has_app {
599 self.content_types.overrides.push(ContentTypeOverride {
600 part_name: app_part.to_string(),
601 content_type: mime_types::EXTENDED_PROPERTIES.to_string(),
602 });
603 }
604 }
605
606 fn ensure_custom_props_content_types(&mut self) {
608 self.ensure_doc_props_content_types();
609
610 let custom_part = "/docProps/custom.xml";
611 let has_custom = self
612 .content_types
613 .overrides
614 .iter()
615 .any(|o| o.part_name == custom_part);
616 if !has_custom {
617 self.content_types.overrides.push(ContentTypeOverride {
618 part_name: custom_part.to_string(),
619 content_type: mime_types::CUSTOM_PROPERTIES.to_string(),
620 });
621 }
622
623 let has_custom_rel = self
624 .package_rels
625 .relationships
626 .iter()
627 .any(|r| r.rel_type == rel_types::CUSTOM_PROPERTIES);
628 if !has_custom_rel {
629 let next_id = self.package_rels.relationships.len() + 1;
630 self.package_rels.relationships.push(Relationship {
631 id: format!("rId{next_id}"),
632 rel_type: rel_types::CUSTOM_PROPERTIES.to_string(),
633 target: "docProps/custom.xml".to_string(),
634 target_mode: None,
635 });
636 }
637 }
638
639 fn find_table_by_name(
642 &self,
643 name: &str,
644 ) -> Option<(&String, &sheetkit_xml::table::TableXml, usize)> {
645 self.tables
646 .iter()
647 .find(|(_, t, _)| t.name == name)
648 .map(|(path, t, idx)| (path, t, *idx))
649 }
650
651 fn table_name_by_id(&self, table_id: u32) -> Option<&str> {
653 self.tables
654 .iter()
655 .find(|(_, t, _)| t.id == table_id)
656 .map(|(_, t, _)| t.name.as_str())
657 }
658
659 fn table_column_name_by_index(&self, table_id: u32, column_index: u32) -> Option<&str> {
661 self.tables
662 .iter()
663 .find(|(_, t, _)| t.id == table_id)
664 .and_then(|(_, t, _)| {
665 t.table_columns
666 .columns
667 .get((column_index - 1) as usize)
668 .map(|c| c.name.as_str())
669 })
670 }
671
672 pub fn add_slicer(&mut self, sheet: &str, config: &crate::slicer::SlicerConfig) -> Result<()> {
677 use sheetkit_xml::content_types::ContentTypeOverride;
678 use sheetkit_xml::slicer::{
679 SlicerCacheDefinition, SlicerDefinition, SlicerDefinitions, TableSlicerCache,
680 };
681
682 crate::slicer::validate_slicer_config(config)?;
683
684 let sheet_idx = self.sheet_index(sheet)?;
685
686 for (_, sd) in &self.slicer_defs {
688 for s in &sd.slicers {
689 if s.name == config.name {
690 return Err(Error::SlicerAlreadyExists {
691 name: config.name.clone(),
692 });
693 }
694 }
695 }
696
697 let cache_name = crate::slicer::slicer_cache_name(&config.name);
698 let caption = config
699 .caption
700 .clone()
701 .unwrap_or_else(|| config.column_name.clone());
702
703 let slicer_num = self.slicer_defs.len() + 1;
705 let cache_num = self.slicer_caches.len() + 1;
706
707 let slicer_path = format!("xl/slicers/slicer{}.xml", slicer_num);
708 let cache_path = format!("xl/slicerCaches/slicerCache{}.xml", cache_num);
709
710 let slicer_def = SlicerDefinition {
712 name: config.name.clone(),
713 cache: cache_name.clone(),
714 caption: Some(caption),
715 start_item: None,
716 column_count: config.column_count,
717 show_caption: config.show_caption,
718 style: config.style.clone(),
719 locked_position: None,
720 row_height: crate::slicer::DEFAULT_ROW_HEIGHT_EMU,
721 };
722
723 let slicer_defs = SlicerDefinitions {
724 xmlns: sheetkit_xml::namespaces::SLICER_2009.to_string(),
725 xmlns_mc: Some(sheetkit_xml::namespaces::MC.to_string()),
726 slicers: vec![slicer_def],
727 };
728
729 let (_path, table_xml, table_sheet_idx) = self
731 .find_table_by_name(&config.table_name)
732 .ok_or_else(|| Error::TableNotFound {
733 name: config.table_name.clone(),
734 })?;
735
736 if table_sheet_idx != sheet_idx {
737 return Err(Error::TableNotFound {
738 name: config.table_name.clone(),
739 });
740 }
741
742 let column_index = table_xml
744 .table_columns
745 .columns
746 .iter()
747 .position(|c| c.name == config.column_name)
748 .ok_or_else(|| Error::TableColumnNotFound {
749 table: config.table_name.clone(),
750 column: config.column_name.clone(),
751 })?;
752
753 let real_table_id = table_xml.id;
754 let real_column = (column_index + 1) as u32;
755
756 let slicer_cache = SlicerCacheDefinition {
758 name: cache_name.clone(),
759 source_name: config.column_name.clone(),
760 table_slicer_cache: Some(TableSlicerCache {
761 table_id: real_table_id,
762 column: real_column,
763 }),
764 };
765
766 self.slicer_defs.push((slicer_path.clone(), slicer_defs));
768 self.slicer_caches.push((cache_path.clone(), slicer_cache));
769
770 self.content_types.overrides.push(ContentTypeOverride {
772 part_name: format!("/{}", slicer_path),
773 content_type: mime_types::SLICER.to_string(),
774 });
775 self.content_types.overrides.push(ContentTypeOverride {
776 part_name: format!("/{}", cache_path),
777 content_type: mime_types::SLICER_CACHE.to_string(),
778 });
779
780 let wb_rid = crate::sheet::next_rid(&self.workbook_rels.relationships);
782 self.workbook_rels.relationships.push(Relationship {
783 id: wb_rid,
784 rel_type: rel_types::SLICER_CACHE.to_string(),
785 target: format!("slicerCaches/slicerCache{}.xml", cache_num),
786 target_mode: None,
787 });
788
789 let ws_rid = self.next_worksheet_rid(sheet_idx);
791 let ws_rels = self
792 .worksheet_rels
793 .entry(sheet_idx)
794 .or_insert_with(|| Relationships {
795 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
796 relationships: vec![],
797 });
798 ws_rels.relationships.push(Relationship {
799 id: ws_rid,
800 rel_type: rel_types::SLICER.to_string(),
801 target: format!("../slicers/slicer{}.xml", slicer_num),
802 target_mode: None,
803 });
804
805 Ok(())
806 }
807
808 pub fn get_slicers(&self, sheet: &str) -> Result<Vec<crate::slicer::SlicerInfo>> {
810 let sheet_idx = self.sheet_index(sheet)?;
811 let mut result = Vec::new();
812
813 let empty_rels = Relationships {
815 xmlns: sheetkit_xml::namespaces::PACKAGE_RELATIONSHIPS.to_string(),
816 relationships: vec![],
817 };
818 let rels = self.worksheet_rels.get(&sheet_idx).unwrap_or(&empty_rels);
819
820 let slicer_targets: Vec<String> = rels
821 .relationships
822 .iter()
823 .filter(|r| r.rel_type == rel_types::SLICER)
824 .map(|r| {
825 let sheet_path = self.sheet_part_path(sheet_idx);
826 crate::workbook_paths::resolve_relationship_target(&sheet_path, &r.target)
827 })
828 .collect();
829
830 for (path, sd) in &self.slicer_defs {
831 if !slicer_targets.contains(path) {
832 continue;
833 }
834 for slicer in &sd.slicers {
835 let cache = self
837 .slicer_caches
838 .iter()
839 .find(|(_, sc)| sc.name == slicer.cache);
840
841 let (table_name, column_name) = if let Some((_, sc)) = cache {
842 let tname = sc
843 .table_slicer_cache
844 .as_ref()
845 .and_then(|tsc| self.table_name_by_id(tsc.table_id))
846 .unwrap_or("")
847 .to_string();
848 let cname = sc
849 .table_slicer_cache
850 .as_ref()
851 .and_then(|tsc| self.table_column_name_by_index(tsc.table_id, tsc.column))
852 .unwrap_or(&sc.source_name)
853 .to_string();
854 (tname, cname)
855 } else {
856 (String::new(), String::new())
857 };
858
859 result.push(crate::slicer::SlicerInfo {
860 name: slicer.name.clone(),
861 caption: slicer
862 .caption
863 .clone()
864 .unwrap_or_else(|| slicer.name.clone()),
865 table_name,
866 column_name,
867 style: slicer.style.clone(),
868 });
869 }
870 }
871
872 Ok(result)
873 }
874
875 pub fn delete_slicer(&mut self, sheet: &str, name: &str) -> Result<()> {
879 let sheet_idx = self.sheet_index(sheet)?;
880
881 let sd_idx = self
883 .slicer_defs
884 .iter()
885 .position(|(_, sd)| sd.slicers.iter().any(|s| s.name == name))
886 .ok_or_else(|| Error::SlicerNotFound {
887 name: name.to_string(),
888 })?;
889
890 let (sd_path, sd) = &self.slicer_defs[sd_idx];
891
892 let cache_name = sd
894 .slicers
895 .iter()
896 .find(|s| s.name == name)
897 .map(|s| s.cache.clone())
898 .unwrap_or_default();
899
900 let remove_whole_part = sd.slicers.len() == 1;
902
903 if remove_whole_part {
904 let sd_path_clone = sd_path.clone();
905 self.slicer_defs.remove(sd_idx);
906
907 let sd_part = format!("/{}", sd_path_clone);
909 self.content_types
910 .overrides
911 .retain(|o| o.part_name != sd_part);
912
913 let ws_path = self.sheet_part_path(sheet_idx);
915 if let Some(rels) = self.worksheet_rels.get_mut(&sheet_idx) {
916 rels.relationships.retain(|r| {
917 if r.rel_type != rel_types::SLICER {
918 return true;
919 }
920 let target =
921 crate::workbook_paths::resolve_relationship_target(&ws_path, &r.target);
922 target != sd_path_clone
923 });
924 }
925 } else {
926 self.slicer_defs[sd_idx]
928 .1
929 .slicers
930 .retain(|s| s.name != name);
931 }
932
933 if !cache_name.is_empty() {
935 if let Some(sc_idx) = self
936 .slicer_caches
937 .iter()
938 .position(|(_, sc)| sc.name == cache_name)
939 {
940 let (sc_path, _) = self.slicer_caches.remove(sc_idx);
941 let sc_part = format!("/{}", sc_path);
942 self.content_types
943 .overrides
944 .retain(|o| o.part_name != sc_part);
945
946 self.workbook_rels.relationships.retain(|r| {
948 if r.rel_type != rel_types::SLICER_CACHE {
949 return true;
950 }
951 let full_target = format!("xl/{}", r.target);
952 full_target != sc_path
953 });
954 }
955 }
956
957 Ok(())
958 }
959}
960
961#[cfg(test)]
962mod tests {
963 use super::*;
964 use tempfile::TempDir;
965
966 fn make_pivot_workbook() -> Workbook {
967 let mut wb = Workbook::new();
968 wb.set_cell_value("Sheet1", "A1", "Name").unwrap();
969 wb.set_cell_value("Sheet1", "B1", "Region").unwrap();
970 wb.set_cell_value("Sheet1", "C1", "Sales").unwrap();
971 wb.set_cell_value("Sheet1", "A2", "Alice").unwrap();
972 wb.set_cell_value("Sheet1", "B2", "North").unwrap();
973 wb.set_cell_value("Sheet1", "C2", 100.0).unwrap();
974 wb.set_cell_value("Sheet1", "A3", "Bob").unwrap();
975 wb.set_cell_value("Sheet1", "B3", "South").unwrap();
976 wb.set_cell_value("Sheet1", "C3", 200.0).unwrap();
977 wb.set_cell_value("Sheet1", "A4", "Carol").unwrap();
978 wb.set_cell_value("Sheet1", "B4", "North").unwrap();
979 wb.set_cell_value("Sheet1", "C4", 150.0).unwrap();
980 wb
981 }
982
983 fn basic_pivot_config() -> PivotTableConfig {
984 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
985 PivotTableConfig {
986 name: "PivotTable1".to_string(),
987 source_sheet: "Sheet1".to_string(),
988 source_range: "A1:C4".to_string(),
989 target_sheet: "Sheet1".to_string(),
990 target_cell: "E1".to_string(),
991 rows: vec![PivotField {
992 name: "Name".to_string(),
993 }],
994 columns: vec![],
995 data: vec![PivotDataField {
996 name: "Sales".to_string(),
997 function: AggregateFunction::Sum,
998 display_name: None,
999 }],
1000 }
1001 }
1002
1003 #[test]
1004 fn test_add_pivot_table_basic() {
1005 let mut wb = make_pivot_workbook();
1006 let config = basic_pivot_config();
1007 wb.add_pivot_table(&config).unwrap();
1008
1009 assert_eq!(wb.pivot_tables.len(), 1);
1010 assert_eq!(wb.pivot_cache_defs.len(), 1);
1011 assert_eq!(wb.pivot_cache_records.len(), 1);
1012 assert_eq!(wb.pivot_tables[0].1.name, "PivotTable1");
1013 assert_eq!(wb.pivot_tables[0].1.cache_id, 0);
1014 }
1015
1016 #[test]
1017 fn test_add_pivot_table_with_columns() {
1018 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1019 let mut wb = make_pivot_workbook();
1020 let config = PivotTableConfig {
1021 name: "PT2".to_string(),
1022 source_sheet: "Sheet1".to_string(),
1023 source_range: "A1:C4".to_string(),
1024 target_sheet: "Sheet1".to_string(),
1025 target_cell: "E1".to_string(),
1026 rows: vec![PivotField {
1027 name: "Name".to_string(),
1028 }],
1029 columns: vec![PivotField {
1030 name: "Region".to_string(),
1031 }],
1032 data: vec![PivotDataField {
1033 name: "Sales".to_string(),
1034 function: AggregateFunction::Average,
1035 display_name: Some("Avg Sales".to_string()),
1036 }],
1037 };
1038 wb.add_pivot_table(&config).unwrap();
1039
1040 let pt = &wb.pivot_tables[0].1;
1041 assert!(pt.row_fields.is_some());
1042 assert!(pt.col_fields.is_some());
1043 assert!(pt.data_fields.is_some());
1044 }
1045
1046 #[test]
1047 fn test_add_pivot_table_source_sheet_not_found() {
1048 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1049 let mut wb = Workbook::new();
1050 let config = PivotTableConfig {
1051 name: "PT".to_string(),
1052 source_sheet: "NonExistent".to_string(),
1053 source_range: "A1:B2".to_string(),
1054 target_sheet: "Sheet1".to_string(),
1055 target_cell: "A1".to_string(),
1056 rows: vec![PivotField {
1057 name: "Col1".to_string(),
1058 }],
1059 columns: vec![],
1060 data: vec![PivotDataField {
1061 name: "Col2".to_string(),
1062 function: AggregateFunction::Sum,
1063 display_name: None,
1064 }],
1065 };
1066 let result = wb.add_pivot_table(&config);
1067 assert!(result.is_err());
1068 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1069 }
1070
1071 #[test]
1072 fn test_add_pivot_table_target_sheet_not_found() {
1073 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1074 let mut wb = make_pivot_workbook();
1075 let config = PivotTableConfig {
1076 name: "PT".to_string(),
1077 source_sheet: "Sheet1".to_string(),
1078 source_range: "A1:C4".to_string(),
1079 target_sheet: "Report".to_string(),
1080 target_cell: "A1".to_string(),
1081 rows: vec![PivotField {
1082 name: "Name".to_string(),
1083 }],
1084 columns: vec![],
1085 data: vec![PivotDataField {
1086 name: "Sales".to_string(),
1087 function: AggregateFunction::Sum,
1088 display_name: None,
1089 }],
1090 };
1091 let result = wb.add_pivot_table(&config);
1092 assert!(result.is_err());
1093 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1094 }
1095
1096 #[test]
1097 fn test_add_pivot_table_duplicate_name() {
1098 let mut wb = make_pivot_workbook();
1099 let config = basic_pivot_config();
1100 wb.add_pivot_table(&config).unwrap();
1101
1102 let result = wb.add_pivot_table(&config);
1103 assert!(result.is_err());
1104 assert!(matches!(
1105 result.unwrap_err(),
1106 Error::PivotTableAlreadyExists { .. }
1107 ));
1108 }
1109
1110 #[test]
1111 fn test_get_pivot_tables_empty() {
1112 let wb = Workbook::new();
1113 let pts = wb.get_pivot_tables();
1114 assert!(pts.is_empty());
1115 }
1116
1117 #[test]
1118 fn test_get_pivot_tables_after_add() {
1119 let mut wb = make_pivot_workbook();
1120 let config = basic_pivot_config();
1121 wb.add_pivot_table(&config).unwrap();
1122
1123 let pts = wb.get_pivot_tables();
1124 assert_eq!(pts.len(), 1);
1125 assert_eq!(pts[0].name, "PivotTable1");
1126 assert_eq!(pts[0].source_sheet, "Sheet1");
1127 assert_eq!(pts[0].source_range, "A1:C4");
1128 assert_eq!(pts[0].target_sheet, "Sheet1");
1129 assert_eq!(pts[0].location, "E1");
1130 }
1131
1132 #[test]
1133 fn test_delete_pivot_table() {
1134 let mut wb = make_pivot_workbook();
1135 let config = basic_pivot_config();
1136 wb.add_pivot_table(&config).unwrap();
1137 assert_eq!(wb.pivot_tables.len(), 1);
1138
1139 wb.delete_pivot_table("PivotTable1").unwrap();
1140 assert!(wb.pivot_tables.is_empty());
1141 assert!(wb.pivot_cache_defs.is_empty());
1142 assert!(wb.pivot_cache_records.is_empty());
1143 assert!(wb.workbook_xml.pivot_caches.is_none());
1144
1145 let pivot_overrides: Vec<_> = wb
1147 .content_types
1148 .overrides
1149 .iter()
1150 .filter(|o| {
1151 o.content_type == mime_types::PIVOT_TABLE
1152 || o.content_type == mime_types::PIVOT_CACHE_DEFINITION
1153 || o.content_type == mime_types::PIVOT_CACHE_RECORDS
1154 })
1155 .collect();
1156 assert!(pivot_overrides.is_empty());
1157 }
1158
1159 #[test]
1160 fn test_delete_pivot_table_not_found() {
1161 let wb_result = Workbook::new().delete_pivot_table("NonExistent");
1162 assert!(wb_result.is_err());
1163 assert!(matches!(
1164 wb_result.unwrap_err(),
1165 Error::PivotTableNotFound { .. }
1166 ));
1167 }
1168
1169 #[test]
1170 fn test_pivot_table_save_open_roundtrip() {
1171 let dir = TempDir::new().unwrap();
1172 let path = dir.path().join("pivot_roundtrip.xlsx");
1173
1174 let mut wb = make_pivot_workbook();
1175 let config = basic_pivot_config();
1176 wb.add_pivot_table(&config).unwrap();
1177
1178 wb.save(&path).unwrap();
1179
1180 let file = std::fs::File::open(&path).unwrap();
1182 let mut archive = zip::ZipArchive::new(file).unwrap();
1183 assert!(archive.by_name("xl/pivotTables/pivotTable1.xml").is_ok());
1184 assert!(archive
1185 .by_name("xl/pivotCache/pivotCacheDefinition1.xml")
1186 .is_ok());
1187 assert!(archive
1188 .by_name("xl/pivotCache/pivotCacheRecords1.xml")
1189 .is_ok());
1190
1191 let wb2 = Workbook::open(&path).unwrap();
1193 assert_eq!(wb2.pivot_tables.len(), 1);
1194 assert_eq!(wb2.pivot_tables[0].1.name, "PivotTable1");
1195 assert_eq!(wb2.pivot_cache_defs.len(), 1);
1196 assert_eq!(wb2.pivot_cache_records.len(), 1);
1197 }
1198
1199 #[test]
1200 fn test_add_multiple_pivot_tables() {
1201 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1202 let mut wb = make_pivot_workbook();
1203
1204 let config1 = basic_pivot_config();
1205 wb.add_pivot_table(&config1).unwrap();
1206
1207 let config2 = PivotTableConfig {
1208 name: "PivotTable2".to_string(),
1209 source_sheet: "Sheet1".to_string(),
1210 source_range: "A1:C4".to_string(),
1211 target_sheet: "Sheet1".to_string(),
1212 target_cell: "H1".to_string(),
1213 rows: vec![PivotField {
1214 name: "Region".to_string(),
1215 }],
1216 columns: vec![],
1217 data: vec![PivotDataField {
1218 name: "Sales".to_string(),
1219 function: AggregateFunction::Count,
1220 display_name: None,
1221 }],
1222 };
1223 wb.add_pivot_table(&config2).unwrap();
1224
1225 assert_eq!(wb.pivot_tables.len(), 2);
1226 assert_eq!(wb.pivot_cache_defs.len(), 2);
1227 assert_eq!(wb.pivot_tables[0].1.cache_id, 0);
1228 assert_eq!(wb.pivot_tables[1].1.cache_id, 1);
1229
1230 let pts = wb.get_pivot_tables();
1231 assert_eq!(pts.len(), 2);
1232 assert_eq!(pts[0].name, "PivotTable1");
1233 assert_eq!(pts[1].name, "PivotTable2");
1234 }
1235
1236 #[test]
1237 fn test_add_pivot_table_content_types_added() {
1238 let mut wb = make_pivot_workbook();
1239 let config = basic_pivot_config();
1240 wb.add_pivot_table(&config).unwrap();
1241
1242 let has_pt_ct = wb.content_types.overrides.iter().any(|o| {
1243 o.content_type == mime_types::PIVOT_TABLE
1244 && o.part_name == "/xl/pivotTables/pivotTable1.xml"
1245 });
1246 assert!(has_pt_ct);
1247
1248 let has_pcd_ct = wb.content_types.overrides.iter().any(|o| {
1249 o.content_type == mime_types::PIVOT_CACHE_DEFINITION
1250 && o.part_name == "/xl/pivotCache/pivotCacheDefinition1.xml"
1251 });
1252 assert!(has_pcd_ct);
1253
1254 let has_pcr_ct = wb.content_types.overrides.iter().any(|o| {
1255 o.content_type == mime_types::PIVOT_CACHE_RECORDS
1256 && o.part_name == "/xl/pivotCache/pivotCacheRecords1.xml"
1257 });
1258 assert!(has_pcr_ct);
1259 }
1260
1261 #[test]
1262 fn test_add_pivot_table_workbook_rels_and_pivot_caches() {
1263 let mut wb = make_pivot_workbook();
1264 let config = basic_pivot_config();
1265 wb.add_pivot_table(&config).unwrap();
1266
1267 let cache_rel = wb
1269 .workbook_rels
1270 .relationships
1271 .iter()
1272 .find(|r| r.rel_type == rel_types::PIVOT_CACHE_DEF);
1273 assert!(cache_rel.is_some());
1274 let cache_rel = cache_rel.unwrap();
1275 assert_eq!(cache_rel.target, "pivotCache/pivotCacheDefinition1.xml");
1276
1277 let pivot_caches = wb.workbook_xml.pivot_caches.as_ref().unwrap();
1279 assert_eq!(pivot_caches.caches.len(), 1);
1280 assert_eq!(pivot_caches.caches[0].cache_id, 0);
1281 assert_eq!(pivot_caches.caches[0].r_id, cache_rel.id);
1282 }
1283
1284 #[test]
1285 fn test_add_pivot_table_worksheet_rels_added() {
1286 let mut wb = make_pivot_workbook();
1287 let config = basic_pivot_config();
1288 wb.add_pivot_table(&config).unwrap();
1289
1290 let ws_rels = wb.worksheet_rels.get(&0).unwrap();
1292 let pt_rel = ws_rels
1293 .relationships
1294 .iter()
1295 .find(|r| r.rel_type == rel_types::PIVOT_TABLE);
1296 assert!(pt_rel.is_some());
1297 assert_eq!(pt_rel.unwrap().target, "../pivotTables/pivotTable1.xml");
1298 }
1299
1300 #[test]
1301 fn test_add_pivot_table_on_separate_target_sheet() {
1302 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1303 let mut wb = make_pivot_workbook();
1304 wb.new_sheet("Report").unwrap();
1305
1306 let config = PivotTableConfig {
1307 name: "CrossSheet".to_string(),
1308 source_sheet: "Sheet1".to_string(),
1309 source_range: "A1:C4".to_string(),
1310 target_sheet: "Report".to_string(),
1311 target_cell: "A1".to_string(),
1312 rows: vec![PivotField {
1313 name: "Name".to_string(),
1314 }],
1315 columns: vec![],
1316 data: vec![PivotDataField {
1317 name: "Sales".to_string(),
1318 function: AggregateFunction::Sum,
1319 display_name: None,
1320 }],
1321 };
1322 wb.add_pivot_table(&config).unwrap();
1323
1324 let pts = wb.get_pivot_tables();
1325 assert_eq!(pts.len(), 1);
1326 assert_eq!(pts[0].target_sheet, "Report");
1327 assert_eq!(pts[0].source_sheet, "Sheet1");
1328
1329 let ws_rels = wb.worksheet_rels.get(&1).unwrap();
1331 let pt_rel = ws_rels
1332 .relationships
1333 .iter()
1334 .find(|r| r.rel_type == rel_types::PIVOT_TABLE);
1335 assert!(pt_rel.is_some());
1336 }
1337
1338 #[test]
1339 fn test_pivot_table_invalid_source_range() {
1340 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1341 let mut wb = make_pivot_workbook();
1342 let config = PivotTableConfig {
1343 name: "BadRange".to_string(),
1344 source_sheet: "Sheet1".to_string(),
1345 source_range: "INVALID".to_string(),
1346 target_sheet: "Sheet1".to_string(),
1347 target_cell: "E1".to_string(),
1348 rows: vec![PivotField {
1349 name: "Name".to_string(),
1350 }],
1351 columns: vec![],
1352 data: vec![PivotDataField {
1353 name: "Sales".to_string(),
1354 function: AggregateFunction::Sum,
1355 display_name: None,
1356 }],
1357 };
1358 let result = wb.add_pivot_table(&config);
1359 assert!(result.is_err());
1360 }
1361
1362 #[test]
1363 fn test_delete_pivot_table_then_add_another() {
1364 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1365 let mut wb = make_pivot_workbook();
1366 let config1 = basic_pivot_config();
1367 wb.add_pivot_table(&config1).unwrap();
1368 wb.delete_pivot_table("PivotTable1").unwrap();
1369
1370 let config2 = PivotTableConfig {
1371 name: "PivotTable2".to_string(),
1372 source_sheet: "Sheet1".to_string(),
1373 source_range: "A1:C4".to_string(),
1374 target_sheet: "Sheet1".to_string(),
1375 target_cell: "E1".to_string(),
1376 rows: vec![PivotField {
1377 name: "Region".to_string(),
1378 }],
1379 columns: vec![],
1380 data: vec![PivotDataField {
1381 name: "Sales".to_string(),
1382 function: AggregateFunction::Max,
1383 display_name: None,
1384 }],
1385 };
1386 wb.add_pivot_table(&config2).unwrap();
1387
1388 assert_eq!(wb.pivot_tables.len(), 1);
1389 assert_eq!(wb.pivot_tables[0].1.name, "PivotTable2");
1390 }
1391
1392 #[test]
1393 fn test_pivot_table_cache_definition_stores_source_info() {
1394 let mut wb = make_pivot_workbook();
1395 let config = basic_pivot_config();
1396 wb.add_pivot_table(&config).unwrap();
1397
1398 let pcd = &wb.pivot_cache_defs[0].1;
1399 let ws_source = pcd.cache_source.worksheet_source.as_ref().unwrap();
1400 assert_eq!(ws_source.sheet, "Sheet1");
1401 assert_eq!(ws_source.reference, "A1:C4");
1402 assert_eq!(pcd.cache_fields.fields.len(), 3);
1403 assert_eq!(pcd.cache_fields.fields[0].name, "Name");
1404 assert_eq!(pcd.cache_fields.fields[1].name, "Region");
1405 assert_eq!(pcd.cache_fields.fields[2].name, "Sales");
1406 }
1407
1408 #[test]
1409 fn test_pivot_table_field_names_from_data() {
1410 let mut wb = make_pivot_workbook();
1411 let config = basic_pivot_config();
1412 wb.add_pivot_table(&config).unwrap();
1413
1414 let pt = &wb.pivot_tables[0].1;
1415 assert_eq!(pt.pivot_fields.fields.len(), 3);
1416 assert_eq!(pt.pivot_fields.fields[0].axis, Some("axisRow".to_string()));
1418 assert_eq!(pt.pivot_fields.fields[1].axis, None);
1420 assert_eq!(pt.pivot_fields.fields[2].data_field, Some(true));
1422 }
1423
1424 #[test]
1425 fn test_pivot_table_empty_header_row_error() {
1426 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1427 let mut wb = Workbook::new();
1428 let config = PivotTableConfig {
1430 name: "Empty".to_string(),
1431 source_sheet: "Sheet1".to_string(),
1432 source_range: "A1:B1".to_string(),
1433 target_sheet: "Sheet1".to_string(),
1434 target_cell: "D1".to_string(),
1435 rows: vec![PivotField {
1436 name: "X".to_string(),
1437 }],
1438 columns: vec![],
1439 data: vec![PivotDataField {
1440 name: "Y".to_string(),
1441 function: AggregateFunction::Sum,
1442 display_name: None,
1443 }],
1444 };
1445 let result = wb.add_pivot_table(&config);
1446 assert!(result.is_err());
1447 }
1448
1449 #[test]
1450 fn test_pivot_table_multiple_save_roundtrip() {
1451 use crate::pivot::{AggregateFunction, PivotDataField, PivotField};
1452 let dir = TempDir::new().unwrap();
1453 let path = dir.path().join("multi_pivot.xlsx");
1454
1455 let mut wb = make_pivot_workbook();
1456 let config1 = basic_pivot_config();
1457 wb.add_pivot_table(&config1).unwrap();
1458
1459 let config2 = PivotTableConfig {
1460 name: "PT2".to_string(),
1461 source_sheet: "Sheet1".to_string(),
1462 source_range: "A1:C4".to_string(),
1463 target_sheet: "Sheet1".to_string(),
1464 target_cell: "H1".to_string(),
1465 rows: vec![PivotField {
1466 name: "Region".to_string(),
1467 }],
1468 columns: vec![],
1469 data: vec![PivotDataField {
1470 name: "Sales".to_string(),
1471 function: AggregateFunction::Min,
1472 display_name: None,
1473 }],
1474 };
1475 wb.add_pivot_table(&config2).unwrap();
1476 wb.save(&path).unwrap();
1477
1478 let wb2 = Workbook::open(&path).unwrap();
1479 assert_eq!(wb2.pivot_tables.len(), 2);
1480 let names: Vec<&str> = wb2
1481 .pivot_tables
1482 .iter()
1483 .map(|(_, pt)| pt.name.as_str())
1484 .collect();
1485 assert!(names.contains(&"PivotTable1"));
1486 assert!(names.contains(&"PT2"));
1487 }
1488
1489 #[test]
1490 fn test_calculate_all_with_dependency_order() {
1491 let mut wb = Workbook::new();
1492 wb.set_cell_value("Sheet1", "A1", 10.0).unwrap();
1494 wb.set_cell_value(
1496 "Sheet1",
1497 "A2",
1498 CellValue::Formula {
1499 expr: "A1*2".to_string(),
1500 result: None,
1501 },
1502 )
1503 .unwrap();
1504 wb.set_cell_value(
1506 "Sheet1",
1507 "A3",
1508 CellValue::Formula {
1509 expr: "A2+A1".to_string(),
1510 result: None,
1511 },
1512 )
1513 .unwrap();
1514
1515 wb.calculate_all().unwrap();
1516
1517 let a2 = wb.get_cell_value("Sheet1", "A2").unwrap();
1519 match a2 {
1520 CellValue::Formula { result, .. } => {
1521 assert_eq!(*result.unwrap(), CellValue::Number(20.0));
1522 }
1523 _ => panic!("A2 should be a formula cell"),
1524 }
1525
1526 let a3 = wb.get_cell_value("Sheet1", "A3").unwrap();
1528 match a3 {
1529 CellValue::Formula { result, .. } => {
1530 assert_eq!(*result.unwrap(), CellValue::Number(30.0));
1531 }
1532 _ => panic!("A3 should be a formula cell"),
1533 }
1534 }
1535
1536 #[test]
1537 fn test_calculate_all_no_formulas() {
1538 let mut wb = Workbook::new();
1539 wb.set_cell_value("Sheet1", "A1", 10.0).unwrap();
1540 wb.set_cell_value("Sheet1", "B1", 20.0).unwrap();
1541 wb.calculate_all().unwrap();
1543 }
1544
1545 #[test]
1546 fn test_calculate_all_cycle_detection() {
1547 let mut wb = Workbook::new();
1548 wb.set_cell_value(
1550 "Sheet1",
1551 "A1",
1552 CellValue::Formula {
1553 expr: "B1".to_string(),
1554 result: None,
1555 },
1556 )
1557 .unwrap();
1558 wb.set_cell_value(
1559 "Sheet1",
1560 "B1",
1561 CellValue::Formula {
1562 expr: "A1".to_string(),
1563 result: None,
1564 },
1565 )
1566 .unwrap();
1567
1568 let result = wb.calculate_all();
1569 assert!(result.is_err());
1570 let err_str = result.unwrap_err().to_string();
1571 assert!(
1572 err_str.contains("circular reference"),
1573 "expected circular reference error, got: {err_str}"
1574 );
1575 }
1576
1577 #[test]
1578 fn test_set_get_doc_props() {
1579 let mut wb = Workbook::new();
1580 let props = crate::doc_props::DocProperties {
1581 title: Some("My Title".to_string()),
1582 subject: Some("My Subject".to_string()),
1583 creator: Some("Author".to_string()),
1584 keywords: Some("rust, excel".to_string()),
1585 description: Some("A test workbook".to_string()),
1586 last_modified_by: Some("Editor".to_string()),
1587 revision: Some("2".to_string()),
1588 created: Some("2024-01-01T00:00:00Z".to_string()),
1589 modified: Some("2024-06-01T12:00:00Z".to_string()),
1590 category: Some("Testing".to_string()),
1591 content_status: Some("Draft".to_string()),
1592 };
1593 wb.set_doc_props(props);
1594
1595 let got = wb.get_doc_props();
1596 assert_eq!(got.title.as_deref(), Some("My Title"));
1597 assert_eq!(got.subject.as_deref(), Some("My Subject"));
1598 assert_eq!(got.creator.as_deref(), Some("Author"));
1599 assert_eq!(got.keywords.as_deref(), Some("rust, excel"));
1600 assert_eq!(got.description.as_deref(), Some("A test workbook"));
1601 assert_eq!(got.last_modified_by.as_deref(), Some("Editor"));
1602 assert_eq!(got.revision.as_deref(), Some("2"));
1603 assert_eq!(got.created.as_deref(), Some("2024-01-01T00:00:00Z"));
1604 assert_eq!(got.modified.as_deref(), Some("2024-06-01T12:00:00Z"));
1605 assert_eq!(got.category.as_deref(), Some("Testing"));
1606 assert_eq!(got.content_status.as_deref(), Some("Draft"));
1607 }
1608
1609 #[test]
1610 fn test_set_get_app_props() {
1611 let mut wb = Workbook::new();
1612 let props = crate::doc_props::AppProperties {
1613 application: Some("SheetKit".to_string()),
1614 doc_security: Some(0),
1615 company: Some("Acme Corp".to_string()),
1616 app_version: Some("1.0.0".to_string()),
1617 manager: Some("Boss".to_string()),
1618 template: Some("default.xltx".to_string()),
1619 };
1620 wb.set_app_props(props);
1621
1622 let got = wb.get_app_props();
1623 assert_eq!(got.application.as_deref(), Some("SheetKit"));
1624 assert_eq!(got.doc_security, Some(0));
1625 assert_eq!(got.company.as_deref(), Some("Acme Corp"));
1626 assert_eq!(got.app_version.as_deref(), Some("1.0.0"));
1627 assert_eq!(got.manager.as_deref(), Some("Boss"));
1628 assert_eq!(got.template.as_deref(), Some("default.xltx"));
1629 }
1630
1631 #[test]
1632 fn test_custom_property_crud() {
1633 let mut wb = Workbook::new();
1634
1635 wb.set_custom_property(
1637 "Project",
1638 crate::doc_props::CustomPropertyValue::String("SheetKit".to_string()),
1639 );
1640
1641 let val = wb.get_custom_property("Project");
1643 assert_eq!(
1644 val,
1645 Some(crate::doc_props::CustomPropertyValue::String(
1646 "SheetKit".to_string()
1647 ))
1648 );
1649
1650 wb.set_custom_property(
1652 "Project",
1653 crate::doc_props::CustomPropertyValue::String("Updated".to_string()),
1654 );
1655 let val = wb.get_custom_property("Project");
1656 assert_eq!(
1657 val,
1658 Some(crate::doc_props::CustomPropertyValue::String(
1659 "Updated".to_string()
1660 ))
1661 );
1662
1663 assert!(wb.delete_custom_property("Project"));
1665 assert!(wb.get_custom_property("Project").is_none());
1666 assert!(!wb.delete_custom_property("Project")); }
1668
1669 #[test]
1670 fn test_doc_props_save_open_roundtrip() {
1671 let dir = TempDir::new().unwrap();
1672 let path = dir.path().join("doc_props.xlsx");
1673
1674 let mut wb = Workbook::new();
1675 wb.set_doc_props(crate::doc_props::DocProperties {
1676 title: Some("Test Title".to_string()),
1677 creator: Some("Test Author".to_string()),
1678 created: Some("2024-01-01T00:00:00Z".to_string()),
1679 ..Default::default()
1680 });
1681 wb.set_app_props(crate::doc_props::AppProperties {
1682 application: Some("SheetKit".to_string()),
1683 company: Some("TestCorp".to_string()),
1684 ..Default::default()
1685 });
1686 wb.set_custom_property("Version", crate::doc_props::CustomPropertyValue::Int(42));
1687 wb.save(&path).unwrap();
1688
1689 let wb2 = Workbook::open(&path).unwrap();
1690 let doc = wb2.get_doc_props();
1691 assert_eq!(doc.title.as_deref(), Some("Test Title"));
1692 assert_eq!(doc.creator.as_deref(), Some("Test Author"));
1693 assert_eq!(doc.created.as_deref(), Some("2024-01-01T00:00:00Z"));
1694
1695 let app = wb2.get_app_props();
1696 assert_eq!(app.application.as_deref(), Some("SheetKit"));
1697 assert_eq!(app.company.as_deref(), Some("TestCorp"));
1698
1699 let custom = wb2.get_custom_property("Version");
1700 assert_eq!(custom, Some(crate::doc_props::CustomPropertyValue::Int(42)));
1701 }
1702
1703 #[test]
1704 fn test_open_without_doc_props() {
1705 let dir = TempDir::new().unwrap();
1708 let path = dir.path().join("no_props.xlsx");
1709
1710 let wb = Workbook::new();
1711 wb.save(&path).unwrap();
1712
1713 let wb2 = Workbook::open(&path).unwrap();
1714 let doc = wb2.get_doc_props();
1715 assert!(doc.title.is_none());
1716 assert!(doc.creator.is_none());
1717
1718 let app = wb2.get_app_props();
1719 assert!(app.application.is_none());
1720
1721 assert!(wb2.get_custom_property("anything").is_none());
1722 }
1723
1724 #[test]
1725 fn test_custom_property_multiple_types() {
1726 let mut wb = Workbook::new();
1727
1728 wb.set_custom_property(
1729 "StringProp",
1730 crate::doc_props::CustomPropertyValue::String("hello".to_string()),
1731 );
1732 wb.set_custom_property("IntProp", crate::doc_props::CustomPropertyValue::Int(-7));
1733 wb.set_custom_property(
1734 "FloatProp",
1735 crate::doc_props::CustomPropertyValue::Float(3.15),
1736 );
1737 wb.set_custom_property(
1738 "BoolProp",
1739 crate::doc_props::CustomPropertyValue::Bool(true),
1740 );
1741 wb.set_custom_property(
1742 "DateProp",
1743 crate::doc_props::CustomPropertyValue::DateTime("2024-01-01T00:00:00Z".to_string()),
1744 );
1745
1746 assert_eq!(
1747 wb.get_custom_property("StringProp"),
1748 Some(crate::doc_props::CustomPropertyValue::String(
1749 "hello".to_string()
1750 ))
1751 );
1752 assert_eq!(
1753 wb.get_custom_property("IntProp"),
1754 Some(crate::doc_props::CustomPropertyValue::Int(-7))
1755 );
1756 assert_eq!(
1757 wb.get_custom_property("FloatProp"),
1758 Some(crate::doc_props::CustomPropertyValue::Float(3.15))
1759 );
1760 assert_eq!(
1761 wb.get_custom_property("BoolProp"),
1762 Some(crate::doc_props::CustomPropertyValue::Bool(true))
1763 );
1764 assert_eq!(
1765 wb.get_custom_property("DateProp"),
1766 Some(crate::doc_props::CustomPropertyValue::DateTime(
1767 "2024-01-01T00:00:00Z".to_string()
1768 ))
1769 );
1770 }
1771
1772 #[test]
1773 fn test_doc_props_default_values() {
1774 let wb = Workbook::new();
1775 let doc = wb.get_doc_props();
1776 assert!(doc.title.is_none());
1777 assert!(doc.subject.is_none());
1778 assert!(doc.creator.is_none());
1779 assert!(doc.keywords.is_none());
1780 assert!(doc.description.is_none());
1781 assert!(doc.last_modified_by.is_none());
1782 assert!(doc.revision.is_none());
1783 assert!(doc.created.is_none());
1784 assert!(doc.modified.is_none());
1785 assert!(doc.category.is_none());
1786 assert!(doc.content_status.is_none());
1787
1788 let app = wb.get_app_props();
1789 assert!(app.application.is_none());
1790 assert!(app.doc_security.is_none());
1791 assert!(app.company.is_none());
1792 assert!(app.app_version.is_none());
1793 assert!(app.manager.is_none());
1794 assert!(app.template.is_none());
1795 }
1796
1797 #[test]
1798 fn test_add_sparkline_and_get_sparklines() {
1799 let mut wb = Workbook::new();
1800 let config = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B1");
1801 wb.add_sparkline("Sheet1", &config).unwrap();
1802
1803 let sparklines = wb.get_sparklines("Sheet1").unwrap();
1804 assert_eq!(sparklines.len(), 1);
1805 assert_eq!(sparklines[0].data_range, "Sheet1!A1:A10");
1806 assert_eq!(sparklines[0].location, "B1");
1807 }
1808
1809 #[test]
1810 fn test_add_multiple_sparklines_to_same_sheet() {
1811 let mut wb = Workbook::new();
1812 let config1 = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B1");
1813 let config2 = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B2");
1814 let mut config3 = crate::sparkline::SparklineConfig::new("Sheet1!C1:C10", "D1");
1815 config3.sparkline_type = crate::sparkline::SparklineType::Column;
1816
1817 wb.add_sparkline("Sheet1", &config1).unwrap();
1818 wb.add_sparkline("Sheet1", &config2).unwrap();
1819 wb.add_sparkline("Sheet1", &config3).unwrap();
1820
1821 let sparklines = wb.get_sparklines("Sheet1").unwrap();
1822 assert_eq!(sparklines.len(), 3);
1823 assert_eq!(
1824 sparklines[2].sparkline_type,
1825 crate::sparkline::SparklineType::Column
1826 );
1827 }
1828
1829 #[test]
1830 fn test_remove_sparkline_by_location() {
1831 let mut wb = Workbook::new();
1832 let config1 = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B1");
1833 let config2 = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B2");
1834 wb.add_sparkline("Sheet1", &config1).unwrap();
1835 wb.add_sparkline("Sheet1", &config2).unwrap();
1836
1837 wb.remove_sparkline("Sheet1", "B1").unwrap();
1838
1839 let sparklines = wb.get_sparklines("Sheet1").unwrap();
1840 assert_eq!(sparklines.len(), 1);
1841 assert_eq!(sparklines[0].location, "B2");
1842 }
1843
1844 #[test]
1845 fn test_remove_nonexistent_sparkline_returns_error() {
1846 let mut wb = Workbook::new();
1847 let result = wb.remove_sparkline("Sheet1", "Z99");
1848 assert!(result.is_err());
1849 }
1850
1851 #[test]
1852 fn test_sparkline_on_nonexistent_sheet_returns_error() {
1853 let mut wb = Workbook::new();
1854 let config = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B1");
1855 let result = wb.add_sparkline("NoSuchSheet", &config);
1856 assert!(result.is_err());
1857 assert!(matches!(result.unwrap_err(), Error::SheetNotFound { .. }));
1858
1859 let result = wb.get_sparklines("NoSuchSheet");
1860 assert!(result.is_err());
1861 }
1862
1863 #[test]
1864 fn test_sparkline_save_open_roundtrip() {
1865 let dir = TempDir::new().unwrap();
1866 let path = dir.path().join("sparkline_roundtrip.xlsx");
1867
1868 let mut wb = Workbook::new();
1869 for i in 1..=10 {
1870 wb.set_cell_value(
1871 "Sheet1",
1872 &format!("A{i}"),
1873 CellValue::Number(i as f64 * 10.0),
1874 )
1875 .unwrap();
1876 }
1877
1878 let mut config = crate::sparkline::SparklineConfig::new("Sheet1!A1:A10", "B1");
1879 config.sparkline_type = crate::sparkline::SparklineType::Column;
1880 config.markers = true;
1881 config.high_point = true;
1882 config.line_weight = Some(1.5);
1883
1884 wb.add_sparkline("Sheet1", &config).unwrap();
1885
1886 let config2 = crate::sparkline::SparklineConfig::new("Sheet1!A1:A5", "C1");
1887 wb.add_sparkline("Sheet1", &config2).unwrap();
1888
1889 wb.save(&path).unwrap();
1890
1891 let wb2 = Workbook::open(&path).unwrap();
1892 let sparklines = wb2.get_sparklines("Sheet1").unwrap();
1893 assert_eq!(sparklines.len(), 2);
1894 assert_eq!(sparklines[0].data_range, "Sheet1!A1:A10");
1895 assert_eq!(sparklines[0].location, "B1");
1896 assert_eq!(
1897 sparklines[0].sparkline_type,
1898 crate::sparkline::SparklineType::Column
1899 );
1900 assert!(sparklines[0].markers);
1901 assert!(sparklines[0].high_point);
1902 assert_eq!(sparklines[0].line_weight, Some(1.5));
1903 assert_eq!(sparklines[1].data_range, "Sheet1!A1:A5");
1904 assert_eq!(sparklines[1].location, "C1");
1905 }
1906
1907 #[test]
1908 fn test_sparkline_empty_sheet_returns_empty_vec() {
1909 let wb = Workbook::new();
1910 let sparklines = wb.get_sparklines("Sheet1").unwrap();
1911 assert!(sparklines.is_empty());
1912 }
1913
1914 fn make_table_config(cols: &[&str]) -> crate::table::TableConfig {
1915 crate::table::TableConfig {
1916 name: "Table1".to_string(),
1917 display_name: "Table1".to_string(),
1918 range: "A1:D10".to_string(),
1919 columns: cols
1920 .iter()
1921 .map(|c| crate::table::TableColumn {
1922 name: c.to_string(),
1923 totals_row_function: None,
1924 totals_row_label: None,
1925 })
1926 .collect(),
1927 ..crate::table::TableConfig::default()
1928 }
1929 }
1930
1931 fn make_slicer_workbook() -> Workbook {
1932 let mut wb = Workbook::new();
1933 let table = make_table_config(&["Status", "Region", "Category", "Col1", "Col2"]);
1934 wb.add_table("Sheet1", &table).unwrap();
1935 wb
1936 }
1937
1938 fn make_slicer_config(name: &str, col: &str) -> crate::slicer::SlicerConfig {
1939 crate::slicer::SlicerConfig {
1940 name: name.to_string(),
1941 cell: "F1".to_string(),
1942 table_name: "Table1".to_string(),
1943 column_name: col.to_string(),
1944 caption: None,
1945 style: None,
1946 width: None,
1947 height: None,
1948 show_caption: None,
1949 column_count: None,
1950 }
1951 }
1952
1953 #[test]
1954 fn test_add_slicer_basic() {
1955 let mut wb = make_slicer_workbook();
1956 let config = make_slicer_config("StatusFilter", "Status");
1957 wb.add_slicer("Sheet1", &config).unwrap();
1958
1959 let slicers = wb.get_slicers("Sheet1").unwrap();
1960 assert_eq!(slicers.len(), 1);
1961 assert_eq!(slicers[0].name, "StatusFilter");
1962 assert_eq!(slicers[0].column_name, "Status");
1963 assert_eq!(slicers[0].table_name, "Table1");
1964 }
1965
1966 #[test]
1967 fn test_add_slicer_with_options() {
1968 let mut wb = make_slicer_workbook();
1969 let config = crate::slicer::SlicerConfig {
1970 name: "RegionSlicer".to_string(),
1971 cell: "G2".to_string(),
1972 table_name: "Table1".to_string(),
1973 column_name: "Region".to_string(),
1974 caption: Some("Filter by Region".to_string()),
1975 style: Some("SlicerStyleLight1".to_string()),
1976 width: Some(300),
1977 height: Some(250),
1978 show_caption: Some(true),
1979 column_count: Some(2),
1980 };
1981 wb.add_slicer("Sheet1", &config).unwrap();
1982
1983 let slicers = wb.get_slicers("Sheet1").unwrap();
1984 assert_eq!(slicers.len(), 1);
1985 assert_eq!(slicers[0].caption, "Filter by Region");
1986 assert_eq!(slicers[0].style, Some("SlicerStyleLight1".to_string()));
1987 }
1988
1989 #[test]
1990 fn test_add_slicer_duplicate_name() {
1991 let mut wb = make_slicer_workbook();
1992 let config = make_slicer_config("MySlicer", "Status");
1993 wb.add_slicer("Sheet1", &config).unwrap();
1994
1995 let result = wb.add_slicer("Sheet1", &config);
1996 assert!(result.is_err());
1997 assert!(result.unwrap_err().to_string().contains("already exists"));
1998 }
1999
2000 #[test]
2001 fn test_add_slicer_invalid_sheet() {
2002 let mut wb = make_slicer_workbook();
2003 let config = make_slicer_config("S1", "Status");
2004 let result = wb.add_slicer("NoSuchSheet", &config);
2005 assert!(result.is_err());
2006 }
2007
2008 #[test]
2009 fn test_add_slicer_table_not_found() {
2010 let mut wb = Workbook::new();
2011 let config = crate::slicer::SlicerConfig {
2012 name: "S1".to_string(),
2013 cell: "F1".to_string(),
2014 table_name: "NonExistent".to_string(),
2015 column_name: "Col".to_string(),
2016 caption: None,
2017 style: None,
2018 width: None,
2019 height: None,
2020 show_caption: None,
2021 column_count: None,
2022 };
2023 let result = wb.add_slicer("Sheet1", &config);
2024 assert!(result.is_err());
2025 assert!(matches!(result.unwrap_err(), Error::TableNotFound { .. }));
2026 }
2027
2028 #[test]
2029 fn test_add_slicer_column_not_found() {
2030 let mut wb = make_slicer_workbook();
2031 let config = crate::slicer::SlicerConfig {
2032 name: "S1".to_string(),
2033 cell: "F1".to_string(),
2034 table_name: "Table1".to_string(),
2035 column_name: "NonExistentColumn".to_string(),
2036 caption: None,
2037 style: None,
2038 width: None,
2039 height: None,
2040 show_caption: None,
2041 column_count: None,
2042 };
2043 let result = wb.add_slicer("Sheet1", &config);
2044 assert!(result.is_err());
2045 assert!(matches!(
2046 result.unwrap_err(),
2047 Error::TableColumnNotFound { .. }
2048 ));
2049 }
2050
2051 #[test]
2052 fn test_add_slicer_correct_table_id_and_column() {
2053 let mut wb = make_slicer_workbook();
2054 let config = make_slicer_config("RegFilter", "Region");
2055 wb.add_slicer("Sheet1", &config).unwrap();
2056
2057 let cache = &wb.slicer_caches[0].1;
2059 let tsc = cache.table_slicer_cache.as_ref().unwrap();
2060 assert_eq!(tsc.table_id, 1);
2061 assert_eq!(tsc.column, 2);
2062 }
2063
2064 #[test]
2065 fn test_get_slicers_resolves_table_name() {
2066 let mut wb = make_slicer_workbook();
2067 wb.add_slicer("Sheet1", &make_slicer_config("S1", "Category"))
2068 .unwrap();
2069
2070 let slicers = wb.get_slicers("Sheet1").unwrap();
2071 assert_eq!(slicers.len(), 1);
2072 assert_eq!(slicers[0].table_name, "Table1");
2073 assert_eq!(slicers[0].column_name, "Category");
2074 }
2075
2076 #[test]
2077 fn test_get_slicers_empty() {
2078 let wb = Workbook::new();
2079 let slicers = wb.get_slicers("Sheet1").unwrap();
2080 assert!(slicers.is_empty());
2081 }
2082
2083 #[test]
2084 fn test_delete_slicer() {
2085 let mut wb = make_slicer_workbook();
2086 let config = make_slicer_config("S1", "Status");
2087 wb.add_slicer("Sheet1", &config).unwrap();
2088
2089 assert_eq!(wb.get_slicers("Sheet1").unwrap().len(), 1);
2090
2091 wb.delete_slicer("Sheet1", "S1").unwrap();
2092 assert_eq!(wb.get_slicers("Sheet1").unwrap().len(), 0);
2093 }
2094
2095 #[test]
2096 fn test_delete_slicer_not_found() {
2097 let mut wb = Workbook::new();
2098 let result = wb.delete_slicer("Sheet1", "NonExistent");
2099 assert!(result.is_err());
2100 assert!(result.unwrap_err().to_string().contains("not found"));
2101 }
2102
2103 #[test]
2104 fn test_delete_slicer_cleans_content_types() {
2105 let mut wb = make_slicer_workbook();
2106 let config = make_slicer_config("S1", "Status");
2107 wb.add_slicer("Sheet1", &config).unwrap();
2108
2109 let ct_before = wb.content_types.overrides.len();
2110 wb.delete_slicer("Sheet1", "S1").unwrap();
2111 let ct_after = wb.content_types.overrides.len();
2112
2113 assert_eq!(ct_before - ct_after, 2);
2115 }
2116
2117 #[test]
2118 fn test_delete_slicer_cleans_workbook_rels() {
2119 let mut wb = make_slicer_workbook();
2120 let config = make_slicer_config("S1", "Status");
2121 wb.add_slicer("Sheet1", &config).unwrap();
2122
2123 let has_cache_rel = wb
2124 .workbook_rels
2125 .relationships
2126 .iter()
2127 .any(|r| r.rel_type == rel_types::SLICER_CACHE);
2128 assert!(has_cache_rel);
2129
2130 wb.delete_slicer("Sheet1", "S1").unwrap();
2131
2132 let has_cache_rel = wb
2133 .workbook_rels
2134 .relationships
2135 .iter()
2136 .any(|r| r.rel_type == rel_types::SLICER_CACHE);
2137 assert!(!has_cache_rel);
2138 }
2139
2140 #[test]
2141 fn test_multiple_slicers_on_same_sheet() {
2142 let mut wb = make_slicer_workbook();
2143 wb.add_slicer("Sheet1", &make_slicer_config("S1", "Col1"))
2144 .unwrap();
2145 wb.add_slicer("Sheet1", &make_slicer_config("S2", "Col2"))
2146 .unwrap();
2147
2148 let slicers = wb.get_slicers("Sheet1").unwrap();
2149 assert_eq!(slicers.len(), 2);
2150 }
2151
2152 #[test]
2153 fn test_slicer_roundtrip() {
2154 let tmp = TempDir::new().unwrap();
2155 let path = tmp.path().join("slicer_rt.xlsx");
2156
2157 let mut wb = make_slicer_workbook();
2158 wb.add_slicer("Sheet1", &make_slicer_config("MySlicer", "Category"))
2159 .unwrap();
2160 wb.save(&path).unwrap();
2161
2162 let wb2 = Workbook::open(&path).unwrap();
2163 let slicers = wb2.get_slicers("Sheet1").unwrap();
2164 assert_eq!(slicers.len(), 1);
2165 assert_eq!(slicers[0].name, "MySlicer");
2166 assert_eq!(slicers[0].column_name, "Category");
2167 assert_eq!(slicers[0].table_name, "Table1");
2168 }
2169
2170 #[test]
2171 fn test_slicer_content_types_added() {
2172 let mut wb = make_slicer_workbook();
2173 wb.add_slicer("Sheet1", &make_slicer_config("S1", "Status"))
2174 .unwrap();
2175
2176 let has_slicer_ct = wb
2177 .content_types
2178 .overrides
2179 .iter()
2180 .any(|o| o.content_type == mime_types::SLICER);
2181 let has_cache_ct = wb
2182 .content_types
2183 .overrides
2184 .iter()
2185 .any(|o| o.content_type == mime_types::SLICER_CACHE);
2186
2187 assert!(has_slicer_ct);
2188 assert!(has_cache_ct);
2189 }
2190
2191 #[test]
2192 fn test_slicer_worksheet_rels_added() {
2193 let mut wb = make_slicer_workbook();
2194 wb.add_slicer("Sheet1", &make_slicer_config("S1", "Status"))
2195 .unwrap();
2196
2197 let rels = wb.worksheet_rels.get(&0).unwrap();
2198 let has_slicer_rel = rels
2199 .relationships
2200 .iter()
2201 .any(|r| r.rel_type == rel_types::SLICER);
2202 assert!(has_slicer_rel);
2203 }
2204
2205 #[test]
2206 fn test_slicer_error_display() {
2207 let err = Error::SlicerNotFound {
2208 name: "Missing".to_string(),
2209 };
2210 assert_eq!(err.to_string(), "slicer 'Missing' not found");
2211
2212 let err = Error::SlicerAlreadyExists {
2213 name: "Dup".to_string(),
2214 };
2215 assert_eq!(err.to_string(), "slicer 'Dup' already exists");
2216 }
2217
2218 #[test]
2219 fn test_add_table_and_get_tables() {
2220 let mut wb = Workbook::new();
2221 let table = make_table_config(&["Name", "Age", "City"]);
2222 wb.add_table("Sheet1", &table).unwrap();
2223
2224 let tables = wb.get_tables("Sheet1").unwrap();
2225 assert_eq!(tables.len(), 1);
2226 assert_eq!(tables[0].name, "Table1");
2227 assert_eq!(tables[0].columns, vec!["Name", "Age", "City"]);
2228 }
2229
2230 #[test]
2231 fn test_add_table_duplicate_name() {
2232 let mut wb = Workbook::new();
2233 let table = make_table_config(&["Col"]);
2234 wb.add_table("Sheet1", &table).unwrap();
2235
2236 let result = wb.add_table("Sheet1", &table);
2237 assert!(result.is_err());
2238 assert!(result.unwrap_err().to_string().contains("already exists"));
2239 }
2240
2241 #[test]
2242 fn test_slicer_table_on_wrong_sheet() {
2243 let mut wb = Workbook::new();
2244 wb.new_sheet("Sheet2").unwrap();
2245 let table = make_table_config(&["Status"]);
2246 wb.add_table("Sheet2", &table).unwrap();
2247
2248 let config = make_slicer_config("S1", "Status");
2249 let result = wb.add_slicer("Sheet1", &config);
2250 assert!(result.is_err());
2251 assert!(matches!(result.unwrap_err(), Error::TableNotFound { .. }));
2252 }
2253}