Skip to main content

excelize_rs/
table.rs

1//! Table and AutoFilter support.
2//!
3//! Ported from Go `table.go`.
4
5use 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
44// ------------------------------------------------------------------
45// Public API
46// ------------------------------------------------------------------
47
48impl File {
49    /// Add a table to a worksheet.
50    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    /// Get the tables in a worksheet.
105    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    /// Delete a table by name.
143    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    /// Apply an AutoFilter to a worksheet range.
189    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
240// ------------------------------------------------------------------
241// Internal helpers
242// ------------------------------------------------------------------
243
244impl File {
245    /// Get all tables in the workbook keyed by worksheet name.
246    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    /// Add a `tablePart` reference to a worksheet.
264    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    /// Set the table column names from the header row cells.
285    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    /// Add a table part with the given coordinates and format.
345    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    /// Apply an AutoFilter to a worksheet.
400    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    /// Write the filter column as default or custom filters.
456    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    /// Write a single `<customFilter>` element.
493    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    /// Parse the filter expression tokens into operator codes.
520    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    /// Parse a single 3-token filter expression.
543    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
599// ------------------------------------------------------------------
600// Free functions
601// ------------------------------------------------------------------
602
603/// Parse table options and apply defaults.
604fn 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
621/// Check whether a defined name or table name contains illegal characters.
622fn 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
639/// Returns true if `code` lies within any of the inclusive ranges in `tbl`.
640fn 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// ------------------------------------------------------------------
650// Tests
651// ------------------------------------------------------------------
652
653#[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}