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 {
t.AutoFilter.Ref = t.Ref
}
_, _ = f.setTableHeader(sheet, x1, y1, x2)
_, _ = f.setTableHeader(sheet, true, x1, y1, x2)
table, _ := xml.Marshal(t)
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.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))
// Test insert column with illegal cell reference

View File

@ -320,7 +320,7 @@ func TestRemoveRow(t *testing.T) {
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) {
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
// 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 (
tableColumns []*xlsxTableColumn
idx int
@ -144,11 +144,15 @@ func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn,
}
name, _ := f.GetCellValue(sheet, cell)
if _, err := strconv.Atoi(name); err == nil {
_ = f.SetCellStr(sheet, cell, name)
if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name)
}
}
if name == "" {
name = "Column" + strconv.Itoa(idx)
_ = f.SetCellStr(sheet, cell, name)
if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name)
}
}
tableColumns = append(tableColumns, &xlsxTableColumn{
ID: idx,
@ -188,13 +192,16 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
if y1 == y2 {
y2++
}
hideHeaderRow := opts != nil && opts.ShowHeaderRow != nil && !*opts.ShowHeaderRow
if hideHeaderRow {
y1++
}
// Correct table range reference, such correct C1:B3 to B1:C3.
ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2})
if err != nil {
return err
}
tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2)
tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2)
name := opts.Name
if name == "" {
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,
},
}
if hideHeaderRow {
t.AutoFilter = nil
t.HeaderRowCount = intPtr(0)
}
table, _ := xml.Marshal(t)
f.saveFileList(tableXML, table)
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
// 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:
//
// err := f.AutoFilter("Sheet1", "A1:D4", &excelize.AutoFilterOptions{
// Column: "B", Expression: "x != blanks",
// err := f.AutoFilter("Sheet1", "A1:D4", []excelize.AutoFilterOptions{
// {Column: "B", Expression: "x != blanks"},
// })
//
// 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
// col < 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)
if err != nil {
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
// 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)
if err != nil {
return err
@ -356,66 +367,65 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts *AutoFilterO
Ref: ref,
}
ws.AutoFilter = filter
if opts == nil || opts.Column == "" || opts.Expression == "" {
return nil
for _, opt := range opts {
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
return nil
}
// writeAutoFilter provides a function to check for single or double custom
// 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 {
// Single equality.
var filters []*xlsxFilter
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 {
// Double equality with "or" operator.
var filters []*xlsxFilter
for _, v := range tokens {
filters = append(filters, &xlsxFilter{Val: v})
}
filter.FilterColumn[0].Filters = &xlsxFilters{Filter: filters}
fc.Filters = &xlsxFilters{Filter: filters}
} else {
// Non default custom filter.
expRel := map[int]int{0: 0, 1: 2}
andRel := map[int]bool{0: true, 1: false}
for k, v := range tokens {
f.writeCustomFilter(filter, exp[expRel[k]], v)
f.writeCustomFilter(fc, exp[expRel[k]], v)
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.
func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) {
func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string) {
operators := map[int]string{
1: "lessThan",
2: "equal",
@ -429,12 +439,12 @@ func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val strin
Operator: operators[operator],
Val: val,
}
if filter.FilterColumn[0].CustomFilters != nil {
filter.FilterColumn[0].CustomFilters.CustomFilter = append(filter.FilterColumn[0].CustomFilters.CustomFilter, &customFilter)
if fc.CustomFilters != nil {
fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter)
} else {
var customFilters []*xlsxCustomFilter
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{
Name: "table",
StyleName: "TableStyleMedium2",
ShowColumnStripes: true,
ShowFirstColumn: true,
ShowLastColumn: 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"}))
// Test add table in not exist worksheet
@ -60,7 +62,7 @@ func TestAddTable(t *testing.T) {
func TestSetTableHeader(t *testing.T) {
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]")
}
@ -68,16 +70,16 @@ func TestAutoFilter(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilter%d.xlsx")
f, err := prepareTestBook1()
assert.NoError(t, err)
for i, opts := range []*AutoFilterOptions{
nil,
{Column: "B", Expression: ""},
{Column: "B", Expression: "x != blanks"},
{Column: "B", Expression: "x == blanks"},
{Column: "B", Expression: "x != nonblanks"},
{Column: "B", Expression: "x == nonblanks"},
{Column: "B", Expression: "x <= 1 and x >= 2"},
{Column: "B", Expression: "x == 1 or x == 2"},
{Column: "B", Expression: "x == 1 or x == 2*"},
for i, opts := range [][]AutoFilterOptions{
{},
{{Column: "B", Expression: ""}},
{{Column: "B", Expression: "x != blanks"}},
{{Column: "B", Expression: "x == blanks"}},
{{Column: "B", Expression: "x != nonblanks"}},
{{Column: "B", Expression: "x == nonblanks"}},
{{Column: "B", Expression: "x <= 1 and 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) {
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")
f, err := prepareTestBook1()
assert.NoError(t, err)
for i, opts := range []*AutoFilterOptions{
{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 o r x == *2"},
{Column: "B", Expression: "x -- y"},
{Column: "A", Expression: "x -- y"},
for i, opts := range [][]AutoFilterOptions{
{{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 o r x == *2"}},
{{Column: "B", Expression: "x -- y"}},
{{Column: "A", Expression: "x -- y"}},
} {
t.Run(fmt.Sprintf("Expression%d", i+1), func(t *testing.T) {
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",
Expression: "",
}), "sheet SheetN does not exist")
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{
}}), "sheet SheetN does not exist")
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "-",
Expression: "-",
}), newInvalidColumnNameError("-").Error())
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, &AutoFilterOptions{
}}), newInvalidColumnNameError("-").Error())
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
Column: "A",
Expression: "-",
}), `incorrect index of column 'A'`)
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, &AutoFilterOptions{
}}), `incorrect index of column 'A'`)
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "A",
Expression: "-",
}), `incorrect number of tokens in criteria '-'`)
}}), `incorrect number of tokens in criteria '-'`)
}
func TestParseFilterTokens(t *testing.T) {

View File

@ -26,7 +26,7 @@ type xlsxTable struct {
DisplayName string `xml:"displayName,attr,omitempty"`
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,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"`
ID int `xml:"id,attr"`
InsertRow bool `xml:"insertRow,attr,omitempty"`
@ -200,21 +200,15 @@ type xlsxTableStyleInfo struct {
type TableOptions struct {
Name string
StyleName string
ShowColumnStripes bool
ShowFirstColumn bool
ShowHeaderRow *bool
ShowLastColumn 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.
type AutoFilterOptions struct {
Column string
Expression string
FilterList []AutoFilterListOptions
}