1use std::collections::HashMap;
6use std::sync::LazyLock;
7
8use quick_xml::de::from_reader as xml_from_reader;
9use quick_xml::se::to_string as xml_to_string;
10use regex::Regex;
11
12use crate::constants::{
13 BUILT_IN_DEFINED_NAME_FILTER_DATABASE, CONTENT_TYPE_SPREADSHEET_ML_TABLE, MAX_FIELD_LENGTH,
14 NAMESPACE_SPREADSHEET, SOURCE_RELATIONSHIP, SOURCE_RELATIONSHIP_TABLE,
15 SUPPORTED_DEFINED_NAME_AFTER_START_CHAR_CODE_RANGE,
16 SUPPORTED_DEFINED_NAME_AT_START_CHAR_CODE_RANGE,
17};
18use crate::errors::Result;
19use crate::errors::{
20 ErrExistsTableName, ErrNameLength, ErrParameterInvalid, ErrSheetNotExist,
21 new_invalid_auto_filter_column_error, new_invalid_auto_filter_exp_error,
22 new_invalid_auto_filter_operator_error, new_invalid_name_error, new_no_exist_table_error,
23 new_unknown_filter_token_error,
24};
25use crate::file::File;
26use crate::lib_util::{
27 bool_ptr, column_name_to_number, coordinates_to_cell_name, coordinates_to_range_ref,
28 count_utf16_string, in_str_slice, range_ref_to_coordinates, sort_coordinates,
29};
30use crate::xml::table::{
31 AutoFilterOptions, Table, XlsxAutoFilter, XlsxCustomFilter, XlsxCustomFilters, XlsxFilter,
32 XlsxFilterColumn, XlsxFilters, XlsxTable, XlsxTableColumn, XlsxTableColumns,
33 XlsxTableStyleInfo,
34};
35use crate::xml::workbook::XlsxDefinedName;
36use crate::xml::worksheet::{XlsxTablePart, XlsxTableParts};
37
38static EXPRESSION_FORMAT: LazyLock<Regex> =
39 LazyLock::new(|| Regex::new(r#""(?:[^"]|"")*"|\S+"#).unwrap());
40static CONDITION_FORMAT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(or|\|\|)").unwrap());
41static BLANK_FORMAT: LazyLock<Regex> = LazyLock::new(|| Regex::new("blanks|nonblanks").unwrap());
42static MATCH_FORMAT: LazyLock<Regex> = LazyLock::new(|| Regex::new("[*?]").unwrap());
43
44impl File {
49 pub fn add_table(&self, sheet: &str, table: Option<&Table>) -> Result<()> {
51 let options = parse_table_options(table)?;
52 if options.range.is_empty() {
53 return Err(Box::new(ErrParameterInvalid));
54 }
55 let mut exist = false;
56 for entry in self.pkg.iter() {
57 let k = entry.key();
58 if k.contains("xl/tables/table") {
59 let data = crate::file::namespace_strict_to_transitional(entry.value());
60 if let Ok(t) = xml_from_reader::<_, XlsxTable>(data.as_slice()) {
61 if t.name == options.name {
62 exist = true;
63 break;
64 }
65 }
66 }
67 }
68 if exist {
69 return Err(Box::new(ErrExistsTableName));
70 }
71
72 let mut coordinates = range_ref_to_coordinates(&options.range)?;
73 sort_coordinates(&mut coordinates)?;
74 let table_id = self.count_tables() + 1;
75 let sheet_relationships_table_xml = format!("../tables/table{table_id}.xml");
76 let table_xml = sheet_relationships_table_xml.replace("..", "xl");
77 let sheet_xml_path = self.get_sheet_xml_path(sheet).unwrap_or_default();
78 let sheet_rels = format!(
79 "xl/worksheets/_rels/{}.rels",
80 sheet_xml_path.trim_start_matches("xl/worksheets/")
81 );
82 let r_id = self.add_rels(
83 &sheet_rels,
84 SOURCE_RELATIONSHIP_TABLE,
85 &sheet_relationships_table_xml,
86 "",
87 );
88 self.add_sheet_table(sheet, r_id)?;
89 self.add_sheet_name_space(sheet, SOURCE_RELATIONSHIP);
90 self.clear_calc_cache();
91 self.add_table_internal(
92 sheet,
93 &table_xml,
94 coordinates[0],
95 coordinates[1],
96 coordinates[2],
97 coordinates[3],
98 table_id,
99 &options,
100 )?;
101 self.add_content_type_part(table_id, "table")
102 }
103
104 pub fn get_tables(&self, sheet: &str) -> Result<Vec<Table>> {
106 let mut tables = Vec::new();
107 let ws = self.work_sheet_reader(sheet)?;
108 let table_parts = match ws.table_parts {
109 Some(tp) => tp,
110 None => return Ok(tables),
111 };
112 for tbl in table_parts.table_part {
113 let target =
114 self.get_sheet_relationships_target_by_id(sheet, tbl.rid.as_deref().unwrap_or(""));
115 let table_xml = target.replace("..", "xl");
116 let content = self.read_xml(&table_xml);
117 if content.is_empty() {
118 continue;
119 }
120 let data = crate::file::namespace_strict_to_transitional(&content);
121 let t: XlsxTable = xml_from_reader(data.as_slice())?;
122 let mut table = Table {
123 r_id: tbl.rid.unwrap_or_default(),
124 t_id: t.id,
125 table_xml,
126 range: t.r#ref,
127 name: t.name,
128 ..Default::default()
129 };
130 if let Some(info) = t.table_style_info {
131 table.style_name = info.name.unwrap_or_default();
132 table.show_column_stripes = info.show_column_stripes;
133 table.show_first_column = info.show_first_column;
134 table.show_last_column = info.show_last_column;
135 table.show_row_stripes = Some(info.show_row_stripes);
136 }
137 tables.push(table);
138 }
139 Ok(tables)
140 }
141
142 pub fn delete_table(&self, name: &str) -> Result<()> {
144 check_defined_name(name)?;
145 let tbls = self.get_tables_for_workbook()?;
146 self.clear_calc_cache();
147 for (sheet, tables) in tbls {
148 for table in tables {
149 if table.name != name {
150 continue;
151 }
152 let mut ws = self.work_sheet_reader(&sheet)?;
153 let mut found = false;
154 if let Some(ref mut tp) = ws.table_parts {
155 let mut i = 0;
156 while i < tp.table_part.len() {
157 if tp.table_part[i].rid.as_deref() == Some(table.r_id.as_str()) {
158 tp.table_part.remove(i);
159 found = true;
160 break;
161 }
162 i += 1;
163 }
164 if found {
165 self.pkg.remove(&table.table_xml);
166 self.remove_content_types_part(
167 CONTENT_TYPE_SPREADSHEET_ML_TABLE,
168 &format!("/{}", table.table_xml),
169 )?;
170 self.delete_sheet_relationships(&sheet, &table.r_id);
171 tp.count = Some(tp.table_part.len() as i64);
172 if tp.count == Some(0) {
173 ws.table_parts = None;
174 }
175 }
176 }
177 if found {
178 if let Some(path) = self.get_sheet_xml_path(&sheet) {
179 self.sheet.insert(path, ws);
180 }
181 return Ok(());
182 }
183 }
184 }
185 Err(new_no_exist_table_error(name).into())
186 }
187
188 pub fn auto_filter(
190 &self,
191 sheet: &str,
192 range_ref: &str,
193 opts: &[AutoFilterOptions],
194 ) -> Result<()> {
195 let mut coordinates = range_ref_to_coordinates(range_ref)?;
196 sort_coordinates(&mut coordinates)?;
197 let ref_str = coordinates_to_range_ref(&coordinates, true)?;
198
199 let mut wb = self.workbook_reader()?;
200 let sheet_id = self.get_sheet_index(sheet)?;
201 let filter_range = format!("'{sheet}'!{ref_str}");
202 let d = XlsxDefinedName {
203 name: Some(BUILT_IN_DEFINED_NAME_FILTER_DATABASE.to_string()),
204 hidden: Some(true),
205 local_sheet_id: Some((sheet_id - 1) as i64),
206 data: filter_range,
207 ..Default::default()
208 };
209
210 if wb.defined_names.is_none() {
211 wb.defined_names = Some(crate::xml::workbook::XlsxDefinedNames {
212 defined_name: vec![d],
213 });
214 } else {
215 let names = wb.defined_names.as_mut().unwrap();
216 let mut defined_name_exists = false;
217 for idx in 0..names.defined_name.len() {
218 let defined_name = &names.defined_name[idx];
219 let local_sheet_id = defined_name.local_sheet_id.unwrap_or(0) as i32;
220 if defined_name.name.as_deref().unwrap_or("")
221 == BUILT_IN_DEFINED_NAME_FILTER_DATABASE
222 && local_sheet_id == sheet_id - 1
223 && defined_name.hidden.unwrap_or(false)
224 {
225 names.defined_name[idx].data = d.data.clone();
226 defined_name_exists = true;
227 }
228 }
229 if !defined_name_exists {
230 names.defined_name.push(d);
231 }
232 }
233 *self.workbook.lock().unwrap() = Some(wb);
234
235 let columns = coordinates[2] - coordinates[0];
236 self.auto_filter_internal(sheet, &ref_str, columns, coordinates[0], opts)
237 }
238}
239
240impl File {
245 pub(crate) fn get_tables_for_workbook(&self) -> Result<HashMap<String, Vec<Table>>> {
247 let mut tables: HashMap<String, Vec<Table>> = HashMap::new();
248 for sheet_name in self.get_sheet_list() {
249 match self.get_tables(&sheet_name) {
250 Ok(tbls) => {
251 tables.insert(sheet_name, tbls);
252 }
253 Err(e) => {
254 if e.downcast_ref::<ErrSheetNotExist>().is_none() {
255 return Err(e);
256 }
257 }
258 }
259 }
260 Ok(tables)
261 }
262
263 fn add_sheet_table(&self, sheet: &str, r_id: i32) -> Result<()> {
265 let mut ws = self.work_sheet_reader(sheet)?;
266 let path = self
267 .get_sheet_xml_path(sheet)
268 .ok_or_else(|| ErrSheetNotExist {
269 sheet_name: sheet.to_string(),
270 })?;
271 let table = XlsxTablePart {
272 rid: Some(format!("rId{r_id}")),
273 };
274 if ws.table_parts.is_none() {
275 ws.table_parts = Some(XlsxTableParts::default());
276 }
277 let tp = ws.table_parts.as_mut().unwrap();
278 tp.count = tp.count.map(|c| c + 1).or(Some(1));
279 tp.table_part.push(table);
280 self.sheet.insert(path, ws);
281 Ok(())
282 }
283
284 pub(crate) fn set_table_columns(
286 &self,
287 sheet: &str,
288 show_header_row: bool,
289 x1: i32,
290 y1: i32,
291 x2: i32,
292 tbl: &mut XlsxTable,
293 ) -> Result<()> {
294 let mut idx = 0;
295 let mut header: Vec<String> = Vec::new();
296 let mut table_columns: Vec<XlsxTableColumn> = Vec::new();
297
298 let get_table_column = |name: &str| -> Option<XlsxTableColumn> {
299 if let Some(ref cols) = tbl.table_columns {
300 for column in &cols.table_column {
301 if column.name == name {
302 return Some(column.clone());
303 }
304 }
305 }
306 None
307 };
308
309 for i in x1..=x2 {
310 idx += 1;
311 let cell = coordinates_to_cell_name(i, y1, false)?;
312 let name = self.get_cell_value(sheet, &cell)?;
313 if name.parse::<i64>().is_ok() && show_header_row {
314 let _ = self.set_cell_str(sheet, &cell, &name);
315 }
316 let mut name = name;
317 if name.is_empty() || in_str_slice(&header, &name, true) != -1 {
318 name = format!("Column{idx}");
319 if show_header_row {
320 let _ = self.set_cell_str(sheet, &cell, &name);
321 }
322 }
323 header.push(name.clone());
324 if let Some(mut column) = get_table_column(&name) {
325 column.id = idx as i64;
326 column.data_dxf_id = 0;
327 column.query_table_field_id = 0;
328 table_columns.push(column);
329 continue;
330 }
331 table_columns.push(XlsxTableColumn {
332 id: idx as i64,
333 name,
334 ..Default::default()
335 });
336 }
337 tbl.table_columns = Some(XlsxTableColumns {
338 count: table_columns.len() as i64,
339 table_column: table_columns,
340 });
341 Ok(())
342 }
343
344 fn add_table_internal(
346 &self,
347 sheet: &str,
348 table_xml: &str,
349 x1: i32,
350 mut y1: i32,
351 x2: i32,
352 mut y2: i32,
353 i: i32,
354 opts: &Table,
355 ) -> Result<()> {
356 if y1 == y2 {
357 y2 += 1;
358 }
359 let hide_header_row = opts.show_header_row == Some(false);
360 if hide_header_row {
361 y1 += 1;
362 }
363 let ref_str = coordinates_to_range_ref(&[x1, y1, x2, y2], false)?;
364 let name = if opts.name.is_empty() {
365 format!("Table{i}")
366 } else {
367 opts.name.clone()
368 };
369 let mut t = XlsxTable {
370 xmlns: Some(NAMESPACE_SPREADSHEET.to_string()),
371 id: i as i64,
372 name: name.clone(),
373 display_name: Some(name),
374 r#ref: ref_str.clone(),
375 auto_filter: Some(XlsxAutoFilter {
376 r#ref: ref_str.clone(),
377 ..Default::default()
378 }),
379 table_style_info: Some(XlsxTableStyleInfo {
380 name: Some(opts.style_name.clone()),
381 show_first_column: opts.show_first_column,
382 show_last_column: opts.show_last_column,
383 show_row_stripes: opts.show_row_stripes.unwrap_or(true),
384 show_column_stripes: opts.show_column_stripes,
385 }),
386 ..Default::default()
387 };
388 let _ = self.set_table_columns(sheet, !hide_header_row, x1, y1, x2, &mut t);
389 if hide_header_row {
390 t.auto_filter = None;
391 t.header_row_count = Some(0);
392 }
393 let mut table = xml_to_string(&t)?.into_bytes();
394 crate::file::strip_empty_attributes(&mut table);
395 self.save_file_list(table_xml, &table);
396 Ok(())
397 }
398
399 fn auto_filter_internal(
401 &self,
402 sheet: &str,
403 ref_str: &str,
404 columns: i32,
405 col: i32,
406 opts: &[AutoFilterOptions],
407 ) -> Result<()> {
408 let path = self
409 .get_sheet_xml_path(sheet)
410 .ok_or_else(|| ErrSheetNotExist {
411 sheet_name: sheet.to_string(),
412 })?;
413 let mut ws = self.work_sheet_reader(sheet)?;
414 if ws.sheet_pr.is_some() {
415 ws.sheet_pr.as_mut().unwrap().filter_mode = Some(true);
416 } else {
417 ws.sheet_pr = Some(crate::xml::worksheet::XlsxSheetPr {
418 filter_mode: Some(true),
419 ..Default::default()
420 });
421 }
422 let mut filter = XlsxAutoFilter {
423 r#ref: ref_str.to_string(),
424 ..Default::default()
425 };
426 for opt in opts {
427 if opt.column.is_empty() || opt.expression.is_empty() {
428 continue;
429 }
430 let fs_col = column_name_to_number(&opt.column)?;
431 let offset = fs_col - col;
432 if offset < 0 || offset > columns {
433 return Err(new_invalid_auto_filter_column_error(&opt.column).into());
434 }
435 let mut fc = XlsxFilterColumn {
436 col_id: offset as i64,
437 ..Default::default()
438 };
439 let token = EXPRESSION_FORMAT
440 .find_iter(&opt.expression)
441 .map(|m| m.as_str().to_string())
442 .collect::<Vec<_>>();
443 if token.len() != 3 && token.len() != 7 {
444 return Err(new_invalid_auto_filter_exp_error(&opt.expression).into());
445 }
446 let (expressions, tokens) = self.parse_filter_expression(&opt.expression, &token)?;
447 self.write_auto_filter(&mut fc, &expressions, &tokens);
448 filter.filter_column.push(fc);
449 }
450 ws.auto_filter = Some(filter);
451 self.sheet.insert(path, ws);
452 Ok(())
453 }
454
455 fn write_auto_filter(&self, fc: &mut XlsxFilterColumn, exp: &[i32], tokens: &[String]) {
457 if exp.len() == 1 && exp[0] == 2 {
458 let filters = vec![XlsxFilter {
459 val: Some(tokens[0].clone()),
460 }];
461 fc.filters = Some(XlsxFilters {
462 filter: filters,
463 ..Default::default()
464 });
465 return;
466 }
467 if exp.len() == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
468 let filters = tokens
469 .iter()
470 .map(|v| XlsxFilter {
471 val: Some(v.clone()),
472 })
473 .collect();
474 fc.filters = Some(XlsxFilters {
475 filter: filters,
476 ..Default::default()
477 });
478 return;
479 }
480 let exp_rel = [0, 2];
481 let and_rel = [true, false];
482 for (k, v) in tokens.iter().enumerate() {
483 self.write_custom_filter(fc, exp[exp_rel[k]], v);
484 if k == 1 {
485 if let Some(ref mut cf) = fc.custom_filters {
486 cf.and = and_rel[exp[k] as usize];
487 }
488 }
489 }
490 }
491
492 fn write_custom_filter(&self, fc: &mut XlsxFilterColumn, operator: i32, val: &str) {
494 let operators: HashMap<i32, &str> = [
495 (1, "lessThan"),
496 (2, "equal"),
497 (3, "lessThanOrEqual"),
498 (4, "greaterThan"),
499 (5, "notEqual"),
500 (6, "greaterThanOrEqual"),
501 (22, "equal"),
502 ]
503 .into_iter()
504 .collect();
505 let custom_filter = XlsxCustomFilter {
506 operator: operators.get(&operator).map(|s| s.to_string()),
507 val: Some(val.to_string()),
508 };
509 if let Some(ref mut cf) = fc.custom_filters {
510 cf.custom_filter.push(custom_filter);
511 return;
512 }
513 fc.custom_filters = Some(XlsxCustomFilters {
514 custom_filter: vec![custom_filter],
515 ..Default::default()
516 });
517 }
518
519 fn parse_filter_expression(
521 &self,
522 expression: &str,
523 tokens: &[String],
524 ) -> Result<(Vec<i32>, Vec<String>)> {
525 if tokens.len() == 7 {
526 let conditional = if CONDITION_FORMAT.is_match(&tokens[3]) {
527 1
528 } else {
529 0
530 };
531 let (expression1, token1) = self.parse_filter_tokens(expression, &tokens[..3])?;
532 let (expression2, token2) = self.parse_filter_tokens(expression, &tokens[4..7])?;
533 return Ok((
534 vec![expression1[0], conditional, expression2[0]],
535 vec![token1, token2],
536 ));
537 }
538 let (exp, token) = self.parse_filter_tokens(expression, tokens)?;
539 Ok((exp, vec![token]))
540 }
541
542 fn parse_filter_tokens(
544 &self,
545 expression: &str,
546 tokens: &[String],
547 ) -> Result<(Vec<i32>, String)> {
548 let operators: HashMap<String, i32> = [
549 ("==", 2),
550 ("=", 2),
551 ("=~", 2),
552 ("eq", 2),
553 ("!=", 5),
554 ("!~", 5),
555 ("ne", 5),
556 ("<>", 5),
557 ("<", 1),
558 ("<=", 3),
559 (">", 4),
560 (">=", 6),
561 ]
562 .into_iter()
563 .map(|(k, v)| (k.to_string(), v))
564 .collect();
565 let operator = operators
566 .get(&tokens[1].to_lowercase())
567 .copied()
568 .ok_or_else(|| new_unknown_filter_token_error(&tokens[1]))?;
569 let mut token = tokens[2].clone();
570 let mut operator = operator;
571
572 if BLANK_FORMAT.is_match(&token.to_lowercase()) {
573 if operator != 2 && operator != 5 {
574 return Err(new_invalid_auto_filter_operator_error(&tokens[1], expression).into());
575 }
576 token = token.to_lowercase();
577 if token == "blanks" {
578 if operator == 5 {
579 token = " ".to_string();
580 }
581 } else {
582 if operator == 5 {
583 operator = 2;
584 token = "blanks".to_string();
585 } else {
586 operator = 5;
587 token = " ".to_string();
588 }
589 }
590 }
591
592 if MATCH_FORMAT.is_match(&token) && operator == 2 {
593 operator = 22;
594 }
595 Ok((vec![operator], token))
596 }
597}
598
599fn parse_table_options(opts: Option<&Table>) -> Result<Table> {
605 let mut options = match opts {
606 Some(o) => o.clone(),
607 None => {
608 return Ok(Table {
609 show_row_stripes: bool_ptr(true),
610 ..Default::default()
611 });
612 }
613 };
614 if options.show_row_stripes.is_none() {
615 options.show_row_stripes = bool_ptr(true);
616 }
617 check_defined_name(&options.name)?;
618 Ok(options)
619}
620
621fn check_defined_name(name: &str) -> Result<()> {
623 if count_utf16_string(name) > MAX_FIELD_LENGTH {
624 return Err(Box::new(ErrNameLength));
625 }
626 for (i, c) in name.chars().enumerate() {
627 let ranges = if i == 0 {
628 SUPPORTED_DEFINED_NAME_AT_START_CHAR_CODE_RANGE
629 } else {
630 SUPPORTED_DEFINED_NAME_AFTER_START_CHAR_CODE_RANGE
631 };
632 if !in_code_range(c as u32, ranges) {
633 return Err(new_invalid_name_error(name).into());
634 }
635 }
636 Ok(())
637}
638
639fn in_code_range(code: u32, tbl: &[(u32, u32)]) -> bool {
641 for (start, end) in tbl {
642 if *start <= code && code <= *end {
643 return true;
644 }
645 }
646 false
647}
648
649#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::options::Options;
657
658 fn new_file() -> File {
659 File::new_with_options(Options::default())
660 }
661
662 #[test]
663 fn add_table_basic() {
664 let f = new_file();
665 f.set_cell_str("Sheet1", "A1", "Header1").unwrap();
666 f.set_cell_str("Sheet1", "B1", "Header2").unwrap();
667 let table = Table {
668 range: "A1:B3".to_string(),
669 ..Default::default()
670 };
671 assert!(f.add_table("Sheet1", Some(&table)).is_ok());
672
673 let tables = f.get_tables("Sheet1").unwrap();
674 assert_eq!(tables.len(), 1);
675 assert_eq!(tables[0].range, "A1:B3");
676 assert_eq!(tables[0].name, "Table1");
677 }
678
679 #[test]
680 fn add_table_with_options() {
681 let f = new_file();
682 f.set_cell_str("Sheet1", "A1", "A").unwrap();
683 f.set_cell_str("Sheet1", "B1", "B").unwrap();
684 let table = Table {
685 range: "A1:B5".to_string(),
686 name: "table".to_string(),
687 style_name: "TableStyleMedium2".to_string(),
688 show_column_stripes: true,
689 show_first_column: true,
690 show_last_column: true,
691 show_row_stripes: Some(false),
692 ..Default::default()
693 };
694 assert!(f.add_table("Sheet1", Some(&table)).is_ok());
695
696 let tables = f.get_tables("Sheet1").unwrap();
697 assert_eq!(tables[0].name, "table");
698 assert_eq!(tables[0].style_name, "TableStyleMedium2");
699 assert!(tables[0].show_column_stripes);
700 }
701
702 #[test]
703 fn add_table_nil_returns_parameter_invalid() {
704 let f = new_file();
705 let err = f.add_table("Sheet1", None).unwrap_err();
706 assert!(err.downcast_ref::<ErrParameterInvalid>().is_some());
707 }
708
709 #[test]
710 fn add_table_duplicate_name_fails() {
711 let f = new_file();
712 let table = Table {
713 range: "A1:B2".to_string(),
714 name: "Table1".to_string(),
715 ..Default::default()
716 };
717 assert!(f.add_table("Sheet1", Some(&table)).is_ok());
718 let err = f.add_table("Sheet1", Some(&table)).unwrap_err();
719 assert!(err.downcast_ref::<ErrExistsTableName>().is_some());
720 }
721
722 #[test]
723 fn add_table_invalid_sheet_name() {
724 let f = new_file();
725 let table = Table {
726 range: "A1:B2".to_string(),
727 ..Default::default()
728 };
729 let err = f.add_table("Sheet:1", Some(&table)).unwrap_err();
730 assert!(
731 err.downcast_ref::<crate::errors::ErrSheetNameInvalid>()
732 .is_some()
733 );
734 }
735
736 #[test]
737 fn add_table_invalid_range() {
738 let f = new_file();
739 let table = Table {
740 range: "A:B1".to_string(),
741 ..Default::default()
742 };
743 assert!(f.add_table("Sheet1", Some(&table)).is_err());
744 }
745
746 #[test]
747 fn add_table_invalid_name() {
748 let f = new_file();
749 let cases = vec!["1Table", "-Table", "'Table", "Table 1", "A&B", "_1Table'"];
750 for name in cases {
751 let table = Table {
752 range: "A1:B2".to_string(),
753 name: name.to_string(),
754 ..Default::default()
755 };
756 assert!(f.add_table("Sheet1", Some(&table)).is_err());
757 }
758 }
759
760 #[test]
761 fn get_tables_empty() {
762 let f = new_file();
763 let tables = f.get_tables("Sheet1").unwrap();
764 assert!(tables.is_empty());
765 }
766
767 #[test]
768 fn get_tables_not_exist_sheet() {
769 let f = new_file();
770 let err = f.get_tables("SheetN").unwrap_err();
771 assert!(err.downcast_ref::<ErrSheetNotExist>().is_some());
772 }
773
774 #[test]
775 fn delete_table_round_trip() {
776 let f = new_file();
777 let table1 = Table {
778 range: "A1:B2".to_string(),
779 name: "Table1".to_string(),
780 ..Default::default()
781 };
782 let table2 = Table {
783 range: "B1:C2".to_string(),
784 name: "Table2".to_string(),
785 ..Default::default()
786 };
787 f.add_table("Sheet1", Some(&table1)).unwrap();
788 f.add_table("Sheet1", Some(&table2)).unwrap();
789 assert!(f.delete_table("Table2").is_ok());
790 assert!(f.delete_table("Table1").is_ok());
791 assert!(f.get_tables("Sheet1").unwrap().is_empty());
792 }
793
794 #[test]
795 fn delete_table_not_exist() {
796 let f = new_file();
797 let err = f.delete_table("Missing").unwrap_err();
798 let msg = format!("{err}");
799 assert!(msg.contains("table Missing does not exist"));
800 }
801
802 #[test]
803 fn auto_filter_basic() {
804 let f = new_file();
805 f.set_cell_str("Sheet1", "A1", "Name").unwrap();
806 f.set_cell_str("Sheet1", "B1", "Score").unwrap();
807 f.set_cell_int("Sheet1", "A2", 1).unwrap();
808 f.set_cell_int("Sheet1", "B2", 10).unwrap();
809 assert!(f.auto_filter("Sheet1", "A1:B2", &[]).is_ok());
810
811 let ws = f.work_sheet_reader("Sheet1").unwrap();
812 assert!(ws.auto_filter.is_some());
813 assert_eq!(ws.auto_filter.as_ref().unwrap().r#ref, "$A$1:$B$2");
814 }
815
816 #[test]
817 fn auto_filter_with_expression() {
818 let f = new_file();
819 let opts = vec![AutoFilterOptions {
820 column: "B".to_string(),
821 expression: "x != blanks".to_string(),
822 }];
823 assert!(f.auto_filter("Sheet1", "A1:B4", &opts).is_ok());
824 }
825
826 #[test]
827 fn auto_filter_invalid_column() {
828 let f = new_file();
829 let opts = vec![AutoFilterOptions {
830 column: "A".to_string(),
831 expression: "x == 1".to_string(),
832 }];
833 let err = f.auto_filter("Sheet1", "B1:C4", &opts).unwrap_err();
834 let msg = format!("{err}");
835 assert!(msg.contains("incorrect index of column"));
836 }
837
838 #[test]
839 fn auto_filter_invalid_expression_tokens() {
840 let f = new_file();
841 let opts = vec![AutoFilterOptions {
842 column: "B".to_string(),
843 expression: "x -- y".to_string(),
844 }];
845 assert!(f.auto_filter("Sheet1", "A1:B4", &opts).is_err());
846 }
847
848 #[test]
849 fn parse_filter_tokens_unknown_operator() {
850 let f = new_file();
851 let tokens = vec!["".to_string(), "!".to_string(), "".to_string()];
852 let err = f.parse_filter_tokens("", &tokens).unwrap_err();
853 let msg = format!("{err}");
854 assert!(msg.contains("unknown operator: !"));
855 }
856
857 #[test]
858 fn check_defined_name_cases() {
859 assert!(check_defined_name("Table1").is_ok());
860 assert!(check_defined_name("_Table").is_ok());
861 assert!(check_defined_name("1Table").is_err());
862 assert!(check_defined_name("Table 1").is_err());
863 assert!(check_defined_name("A&B").is_err());
864 }
865}