Breaking change: changed the third parameter for the `AddFilter`

- Support to add multiple filter columns
- Remove the exported type `AutoFilterListOptions`
- Support to specify if show header row of the table
- Update unit tests and documents of the function
This commit is contained in:
xuri 2023-03-04 00:07:04 +08:00
parent f707b2d2da
commit dc3bf331d5
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
6 changed files with 91 additions and 85 deletions

View File

@ -252,7 +252,7 @@ func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection,
if t.AutoFilter != nil { if t.AutoFilter != nil {
t.AutoFilter.Ref = t.Ref t.AutoFilter.Ref = t.Ref
} }
_, _ = f.setTableHeader(sheet, x1, y1, x2) _, _ = f.setTableHeader(sheet, true, x1, y1, x2)
table, _ := xml.Marshal(t) table, _ := xml.Marshal(t)
f.saveFileList(tableXML, table) f.saveFileList(tableXML, table)
} }

View File

@ -412,7 +412,7 @@ func TestInsertCols(t *testing.T) {
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))
assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", &AutoFilterOptions{Column: "B", Expression: "x != blanks"})) assert.NoError(t, f.AutoFilter(sheet1, "A2:B2", []AutoFilterOptions{{Column: "B", Expression: "x != blanks"}}))
assert.NoError(t, f.InsertCols(sheet1, "A", 1)) assert.NoError(t, f.InsertCols(sheet1, "A", 1))
// Test insert column with illegal cell reference // Test insert column with illegal cell reference

View File

@ -320,7 +320,7 @@ func TestRemoveRow(t *testing.T) {
t.FailNow() t.FailNow()
} }
err = f.AutoFilter(sheet1, "A2:A2", &AutoFilterOptions{Column: "A", Expression: "x != blanks"}) err = f.AutoFilter(sheet1, "A2:A2", []AutoFilterOptions{{Column: "A", Expression: "x != blanks"}})
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }

View File

@ -131,7 +131,7 @@ func (f *File) addSheetTable(sheet string, rID int) error {
// setTableHeader provides a function to set cells value in header row for the // setTableHeader provides a function to set cells value in header row for the
// table. // table.
func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, error) { func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) ([]*xlsxTableColumn, error) {
var ( var (
tableColumns []*xlsxTableColumn tableColumns []*xlsxTableColumn
idx int idx int
@ -144,11 +144,15 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn,
} }
name, _ := f.GetCellValue(sheet, cell) name, _ := f.GetCellValue(sheet, cell)
if _, err := strconv.Atoi(name); err == nil { if _, err := strconv.Atoi(name); err == nil {
_ = f.SetCellStr(sheet, cell, name) if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name)
}
} }
if name == "" { if name == "" {
name = "Column" + strconv.Itoa(idx) name = "Column" + strconv.Itoa(idx)
_ = f.SetCellStr(sheet, cell, name) if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name)
}
} }
tableColumns = append(tableColumns, &xlsxTableColumn{ tableColumns = append(tableColumns, &xlsxTableColumn{
ID: idx, ID: idx,
@ -188,13 +192,16 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
if y1 == y2 { if y1 == y2 {
y2++ y2++
} }
hideHeaderRow := opts != nil && opts.ShowHeaderRow != nil && !*opts.ShowHeaderRow
if hideHeaderRow {
y1++
}
// Correct table range reference, such correct C1:B3 to B1:C3. // Correct table range reference, such correct C1:B3 to B1:C3.
ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2})
if err != nil { if err != nil {
return err return err
} }
tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2) tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2)
name := opts.Name name := opts.Name
if name == "" { if name == "" {
name = "Table" + strconv.Itoa(i) name = "Table" + strconv.Itoa(i)
@ -220,6 +227,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
ShowColumnStripes: opts.ShowColumnStripes, ShowColumnStripes: opts.ShowColumnStripes,
}, },
} }
if hideHeaderRow {
t.AutoFilter = nil
t.HeaderRowCount = intPtr(0)
}
table, _ := xml.Marshal(t) table, _ := xml.Marshal(t)
f.saveFileList(tableXML, table) f.saveFileList(tableXML, table)
return nil return nil
@ -230,12 +241,12 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
// way of filtering a 2D range of data based on some simple criteria. For // way of filtering a 2D range of data based on some simple criteria. For
// example applying an auto filter to a cell range A1:D4 in the Sheet1: // example applying an auto filter to a cell range A1:D4 in the Sheet1:
// //
// err := f.AutoFilter("Sheet1", "A1:D4", nil) // err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{})
// //
// Filter data in an auto filter: // Filter data in an auto filter:
// //
// err := f.AutoFilter("Sheet1", "A1:D4", &excelize.AutoFilterOptions{ // err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{
// Column: "B", Expression: "x != blanks", // {Column: "B", Expression: "x != blanks"},
// }) // })
// //
// Column defines the filter columns in an auto filter range based on simple // Column defines the filter columns in an auto filter range based on simple
@ -296,7 +307,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
// x < 2000 // x < 2000
// col < 2000 // col < 2000
// Price < 2000 // Price < 2000
func (f *File) AutoFilter(sheet, rangeRef string, opts *AutoFilterOptions) error { func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) error {
coordinates, err := rangeRefToCoordinates(rangeRef) coordinates, err := rangeRefToCoordinates(rangeRef)
if err != nil { if err != nil {
return err return err
@ -343,7 +354,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts *AutoFilterOptions) error
// autoFilter provides a function to extract the tokens from the filter // autoFilter provides a function to extract the tokens from the filter
// expression. The tokens are mainly non-whitespace groups. // expression. The tokens are mainly non-whitespace groups.
func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterOptions) error { func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilterOptions) error {
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
@ -356,66 +367,65 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterO
Ref: ref, Ref: ref,
} }
ws.AutoFilter = filter ws.AutoFilter = filter
if opts == nil || opts.Column == "" || opts.Expression == "" { for _, opt := range opts {
return nil if opt.Column == "" || opt.Expression == "" {
continue
}
fsCol, err := ColumnNameToNumber(opt.Column)
if err != nil {
return err
}
offset := fsCol - col
if offset < 0 || offset > columns {
return fmt.Errorf("incorrect index of column '%s'", opt.Column)
}
fc := &xlsxFilterColumn{ColID: offset}
re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
token := re.FindAllString(opt.Expression, -1)
if len(token) != 3 && len(token) != 7 {
return fmt.Errorf("incorrect number of tokens in criteria '%s'", opt.Expression)
}
expressions, tokens, err := f.parseFilterExpression(opt.Expression, token)
if err != nil {
return err
}
f.writeAutoFilter(fc, expressions, tokens)
filter.FilterColumn = append(filter.FilterColumn, fc)
} }
fsCol, err := ColumnNameToNumber(opts.Column)
if err != nil {
return err
}
offset := fsCol - col
if offset < 0 || offset > columns {
return fmt.Errorf("incorrect index of column '%s'", opts.Column)
}
filter.FilterColumn = append(filter.FilterColumn, &xlsxFilterColumn{
ColID: offset,
})
re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
token := re.FindAllString(opts.Expression, -1)
if len(token) != 3 && len(token) != 7 {
return fmt.Errorf("incorrect number of tokens in criteria '%s'", opts.Expression)
}
expressions, tokens, err := f.parseFilterExpression(opts.Expression, token)
if err != nil {
return err
}
f.writeAutoFilter(filter, expressions, tokens)
ws.AutoFilter = filter ws.AutoFilter = filter
return nil return nil
} }
// writeAutoFilter provides a function to check for single or double custom // writeAutoFilter provides a function to check for single or double custom
// filters as default filters and handle them accordingly. // filters as default filters and handle them accordingly.
func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) { func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string) {
if len(exp) == 1 && exp[0] == 2 { if len(exp) == 1 && exp[0] == 2 {
// Single equality. // Single equality.
var filters []*xlsxFilter var filters []*xlsxFilter
filters = append(filters, &xlsxFilter{Val: tokens[0]}) filters = append(filters, &xlsxFilter{Val: tokens[0]})
filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} fc.Filters = &xlsxFilters{Filter: filters}
} else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { } else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
// Double equality with "or" operator. // Double equality with "or" operator.
var filters []*xlsxFilter var filters []*xlsxFilter
for _, v := range tokens { for _, v := range tokens {
filters = append(filters, &xlsxFilter{Val: v}) filters = append(filters, &xlsxFilter{Val: v})
} }
filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters} fc.Filters = &xlsxFilters{Filter: filters}
} else { } else {
// Non default custom filter. // Non default custom filter.
expRel := map[int]int{0: 0, 1: 2} expRel := map[int]int{0: 0, 1: 2}
andRel := map[int]bool{0: true, 1: false} andRel := map[int]bool{0: true, 1: false}
for k, v := range tokens { for k, v := range tokens {
f.writeCustomFilter(filter, exp[expRel[k]], v) f.writeCustomFilter(fc, exp[expRel[k]], v)
if k == 1 { if k == 1 {
filter.FilterColumn[0].CustomFilters.And = andRel[exp[k]] fc.CustomFilters.And = andRel[exp[k]]
} }
} }
} }
} }
// writeCustomFilter provides a function to write the <customFilter> element. // writeCustomFilter provides a function to write the <customFilter> element.
func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) { func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) {
operators := map[int]string{ operators := map[int]string{
1: "lessThan", 1: "lessThan",
2: "equal", 2: "equal",
@ -429,12 +439,12 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin
Operator: operators[operator], Operator: operators[operator],
Val: val, Val: val,
} }
if filter.FilterColumn[0].CustomFilters != nil { if fc.CustomFilters != nil {
filter.FilterColumn[0].CustomFilters.CustomFilter = append(filter.FilterColumn[0].CustomFilters.CustomFilter, &customFilter) fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter)
} else { } else {
var customFilters []*xlsxCustomFilter var customFilters []*xlsxCustomFilter
customFilters = append(customFilters, &customFilter) customFilters = append(customFilters, &customFilter)
filter.FilterColumn[0].CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters} fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
} }
} }

View File

@ -16,12 +16,14 @@ func TestAddTable(t *testing.T) {
assert.NoError(t, f.AddTable("Sheet2", "A2:B5", &TableOptions{ assert.NoError(t, f.AddTable("Sheet2", "A2:B5", &TableOptions{
Name: "table", Name: "table",
StyleName: "TableStyleMedium2", StyleName: "TableStyleMedium2",
ShowColumnStripes: true,
ShowFirstColumn: true, ShowFirstColumn: true,
ShowLastColumn: true, ShowLastColumn: true,
ShowRowStripes: boolPtr(true), ShowRowStripes: boolPtr(true),
ShowColumnStripes: true, }))
}, assert.NoError(t, f.AddTable("Sheet2", "D1:D11", &TableOptions{
)) ShowHeaderRow: boolPtr(false),
}))
assert.NoError(t, f.AddTable("Sheet2", "F1:F1", &TableOptions{StyleName: "TableStyleMedium8"})) assert.NoError(t, f.AddTable("Sheet2", "F1:F1", &TableOptions{StyleName: "TableStyleMedium8"}))
// Test add table in not exist worksheet // Test add table in not exist worksheet
@ -60,7 +62,7 @@ func TestAddTable(t *testing.T) {
func TestSetTableHeader(t *testing.T) { func TestSetTableHeader(t *testing.T) {
f := NewFile() f := NewFile()
_, err := f.setTableHeader("Sheet1", 1, 0, 1) _, err := f.setTableHeader("Sheet1", true, 1, 0, 1)
assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.EqualError(t, err, "invalid cell reference [1, 0]")
} }
@ -68,16 +70,16 @@ func TestAutoFilter(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilter%d.xlsx") outFile := filepath.Join("test", "TestAutoFilter%d.xlsx")
f, err := prepareTestBook1() f, err := prepareTestBook1()
assert.NoError(t, err) assert.NoError(t, err)
for i, opts := range []*AutoFilterOptions{ for i, opts := range [][]AutoFilterOptions{
nil, {},
{Column: "B", Expression: ""}, {{Column: "B", Expression: ""}},
{Column: "B", Expression: "x != blanks"}, {{Column: "B", Expression: "x != blanks"}},
{Column: "B", Expression: "x == blanks"}, {{Column: "B", Expression: "x == blanks"}},
{Column: "B", Expression: "x != nonblanks"}, {{Column: "B", Expression: "x != nonblanks"}},
{Column: "B", Expression: "x == nonblanks"}, {{Column: "B", Expression: "x == nonblanks"}},
{Column: "B", Expression: "x <= 1 and x >= 2"}, {{Column: "B", Expression: "x <= 1 and x >= 2"}},
{Column: "B", Expression: "x == 1 or x == 2"}, {{Column: "B", Expression: "x == 1 or x == 2"}},
{Column: "B", Expression: "x == 1 or x == 2*"}, {{Column: "B", Expression: "x == 1 or x == 2*"}},
} { } {
t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) {
assert.NoError(t, f.AutoFilter("Sheet1", "D4:B1", opts)) assert.NoError(t, f.AutoFilter("Sheet1", "D4:B1", opts))
@ -100,13 +102,13 @@ func TestAutoFilterError(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx") outFile := filepath.Join("test", "TestAutoFilterError%d.xlsx")
f, err := prepareTestBook1() f, err := prepareTestBook1()
assert.NoError(t, err) assert.NoError(t, err)
for i, opts := range []*AutoFilterOptions{ for i, opts := range [][]AutoFilterOptions{
{Column: "B", Expression: "x <= 1 and x >= blanks"}, {{Column: "B", Expression: "x <= 1 and x >= blanks"}},
{Column: "B", Expression: "x -- y or x == *2*"}, {{Column: "B", Expression: "x -- y or x == *2*"}},
{Column: "B", Expression: "x != y or x ? *2"}, {{Column: "B", Expression: "x != y or x ? *2"}},
{Column: "B", Expression: "x -- y o r x == *2"}, {{Column: "B", Expression: "x -- y o r x == *2"}},
{Column: "B", Expression: "x -- y"}, {{Column: "B", Expression: "x -- y"}},
{Column: "A", Expression: "x -- y"}, {{Column: "A", Expression: "x -- y"}},
} { } {
t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) { t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) {
if assert.Error(t, f.AutoFilter("Sheet2", "D4:B1", opts)) { if assert.Error(t, f.AutoFilter("Sheet2", "D4:B1", opts)) {
@ -115,22 +117,22 @@ func TestAutoFilterError(t *testing.T) {
}) })
} }
assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, &AutoFilterOptions{ assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "", Expression: "",
}), "sheet SheetN does not exist") }}), "sheet SheetN does not exist")
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "-", Column: "-",
Expression: "-", Expression: "-",
}), newInvalidColumnNameError("-").Error()) }}), newInvalidColumnNameError("-").Error())
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &AutoFilterOptions{ assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "-", Expression: "-",
}), `incorrect index of column 'A'`) }}), `incorrect index of column 'A'`)
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{ assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "-", Expression: "-",
}), `incorrect number of tokens in criteria '-'`) }}), `incorrect number of tokens in criteria '-'`)
} }
func TestParseFilterTokens(t *testing.T) { func TestParseFilterTokens(t *testing.T) {

View File

@ -26,7 +26,7 @@ type xlsxTable struct {
DisplayName string `xml:"displayName,attr,omitempty"` DisplayName string `xml:"displayName,attr,omitempty"`
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"` HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
HeaderRowCount int `xml:"headerRowCount,attr,omitempty"` HeaderRowCount *int `xml:"headerRowCount,attr"`
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"` HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
ID int `xml:"id,attr"` ID int `xml:"id,attr"`
InsertRow bool `xml:"insertRow,attr,omitempty"` InsertRow bool `xml:"insertRow,attr,omitempty"`
@ -200,21 +200,15 @@ type xlsxTableStyleInfo struct {
type TableOptions struct { type TableOptions struct {
Name string Name string
StyleName string StyleName string
ShowColumnStripes bool
ShowFirstColumn bool ShowFirstColumn bool
ShowHeaderRow *bool
ShowLastColumn bool ShowLastColumn bool
ShowRowStripes *bool ShowRowStripes *bool
ShowColumnStripes bool
}
// AutoFilterListOptions directly maps the auto filter list settings.
type AutoFilterListOptions struct {
Column string
Value []int
} }
// AutoFilterOptions directly maps the auto filter settings. // AutoFilterOptions directly maps the auto filter settings.
type AutoFilterOptions struct { type AutoFilterOptions struct {
Column string Column string
Expression string Expression string
FilterList []AutoFilterListOptions
} }