Init auto filter support, relate issue #59.

This commit is contained in:
Ri Xu 2017-06-08 11:11:11 +08:00
parent c5dc632951
commit c89d842352
6 changed files with 486 additions and 3 deletions

View File

@ -477,6 +477,19 @@ func TestSheetVisibility(t *testing.T) {
}
}
func TestRowVisibility(t *testing.T) {
xlsx, err := OpenFile("./test/Workbook_2.xlsx")
if err != nil {
t.Log(err)
}
xlsx.HideRow("Sheet3", 2)
xlsx.UnhideRow("Sheet3", 2)
err = xlsx.Save()
if err != nil {
t.Log(err)
}
}
func TestCopySheet(t *testing.T) {
xlsx, err := OpenFile("./test/Workbook_2.xlsx")
if err != nil {
@ -541,6 +554,45 @@ func TestAddComments(t *testing.T) {
}
}
func TestAutoFilter(t *testing.T) {
xlsx, err := OpenFile("./test/Workbook_2.xlsx")
if err != nil {
t.Log(err)
}
err = xlsx.AutoFilter("Sheet3", "D4", "B1", ``)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x != blanks"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x == blanks"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x != nonblanks"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x == nonblanks"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x <= 1 and x >= 2"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x == 1 or x == 2"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x == 1 or x == 2*"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x <= 1 and x >= blanks"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x -- y or x == *2*"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x != y or x ? *2"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x -- y o r x == *2"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"B","expression":"x -- y"}`)
t.Log(err)
err = xlsx.AutoFilter("Sheet3", "D4", "B1", `{"column":"A","expression":"x -- y"}`)
t.Log(err)
err = xlsx.Save()
if err != nil {
t.Log(err)
}
}
func TestAddChart(t *testing.T) {
xlsx, err := OpenFile("./test/Workbook1.xlsx")
if err != nil {

24
rows.go
View File

@ -148,3 +148,27 @@ func (xlsx *xlsxC) getValueFrom(f *File, d *xlsxSST) (string, error) {
return f.formattedValue(xlsx.S, xlsx.V), nil
}
}
// HideRow provides a function to set hidden of a single row by given worksheet index and row index. For example, hide row 3 in Sheet1:
//
// xlsx.HideRow("Sheet1", 2)
//
func (f *File) HideRow(sheet string, rowIndex int) {
xlsx := f.workSheetReader(sheet)
rows := rowIndex + 1
cells := 0
completeRow(xlsx, rows, cells)
xlsx.SheetData.Row[rowIndex].Hidden = true
}
// UnhideRow provides a function to set unhidden of a single row by given worksheet index and row index. For example, unhide row 3 in Sheet1:
//
// xlsx.UnhideRow("Sheet1", 2)
//
func (f *File) UnhideRow(sheet string, rowIndex int) {
xlsx := f.workSheetReader(sheet)
rows := rowIndex + 1
cells := 0
completeRow(xlsx, rows, cells)
xlsx.SheetData.Row[rowIndex].Hidden = false
}

View File

@ -450,7 +450,7 @@ func (f *File) HideSheet(name string) {
content := f.workbookReader()
count := 0
for _, v := range content.Sheets.Sheet {
if v.State != `hidden` {
if v.State != "hidden" {
count++
}
}
@ -462,7 +462,7 @@ func (f *File) HideSheet(name string) {
tabSelected = xlsx.SheetViews.SheetView[0].TabSelected
}
if v.Name == name && count > 1 && !tabSelected {
content.Sheets.Sheet[k].State = `hidden`
content.Sheets.Sheet[k].State = "hidden"
}
}
}

285
table.go
View File

@ -3,6 +3,8 @@ package excelize
import (
"encoding/json"
"encoding/xml"
"fmt"
"regexp"
"strconv"
"strings"
)
@ -150,3 +152,286 @@ func (f *File) addTable(sheet, tableXML string, hxAxis, hyAxis, vxAxis, vyAxis,
table, _ := xml.Marshal(t)
f.saveFileList(tableXML, string(table))
}
// parseAutoFilterSet provides function to parse the settings of the auto
// filter.
func parseAutoFilterSet(formatSet string) *formatAutoFilter {
format := formatAutoFilter{}
json.Unmarshal([]byte(formatSet), &format)
return &format
}
// AutoFilter provides the method to add auto filter in a worksheet by given
// sheet index, coordinate area and settings. An autofilter in Excel is a way of
// filtering a 2D range of data based on some simple criteria. For example
// applying an autofilter to a cell range A1:D4 in the worksheet 1:
//
// err = xlsx.AutoFilter("Sheet1", "A1", "D4", "")
//
// Filter data in an autofilter:
//
// err = xlsx.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`)
//
// column defines the filter columns in a autofilter range based on simple criteria
//
// It isn't sufficient to just specify the filter condition. You must also hide any rows that don't match the filter condition. Rows are hidden using the HideRow() method. Excelize can't filter rows automatically since this isn't part of the file format.
//
// Setting a filter criteria for a column:
//
// expression defines the conditions, the following operators are available for setting the filter criteria:
//
// ==
// !=
// >
// <
// >=
// <=
// and
// or
//
// An expression can comprise a single statement or two statements separated by the and and or operators. For example:
//
// x < 2000
// x > 2000
// x == 2000
// x > 2000 and x < 5000
// x == 2000 or x == 5000
//
// Filtering of blank or non-blank data can be achieved by using a value of Blanks or NonBlanks in the expression:
//
// x == Blanks
// x == NonBlanks
//
// Excel also allows some simple string matching operations:
//
// x == b* // begins with b
// x != b* // doesnt begin with b
// x == *b // ends with b
// x != *b // doesnt end with b
// x == *b* // contains b
// x != *b* // doesn't contains b
//
// You can also use '*' to match any character or number and '?' to match any single character or number. No other regular expression quantifier is supported by Excel's filters. Excel's regular expression characters can be escaped using '~'.
//
// The placeholder variable x in the above examples can be replaced by any simple string. The actual placeholder name is ignored internally so the following are all equivalent:
//
// x < 2000
// col < 2000
// Price < 2000
//
func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
formatSet := parseAutoFilterSet(format)
hcell = strings.ToUpper(hcell)
vcell = strings.ToUpper(vcell)
// Coordinate conversion, convert C1:B3 to 2,0,1,2.
hcol := string(strings.Map(letterOnlyMapF, hcell))
hrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, hcell))
hyAxis := hrow - 1
hxAxis := titleToNumber(hcol)
vcol := string(strings.Map(letterOnlyMapF, vcell))
vrow, _ := strconv.Atoi(strings.Map(intOnlyMapF, vcell))
vyAxis := vrow - 1
vxAxis := titleToNumber(vcol)
if vxAxis < hxAxis {
hcell, vcell = vcell, hcell
vxAxis, hxAxis = hxAxis, vxAxis
}
if vyAxis < hyAxis {
hcell, vcell = vcell, hcell
vyAxis, hyAxis = hyAxis, vyAxis
}
ref := toAlphaString(hxAxis+1) + strconv.Itoa(hyAxis+1) + ":" + toAlphaString(vxAxis+1) + strconv.Itoa(vyAxis+1)
refRange := vxAxis - hxAxis
err := f.autoFilter(sheet, ref, refRange, hxAxis, formatSet)
return err
}
// autoFilter provides function to extract the tokens from the filter
// expression. The tokens are mainly non-whitespace groups.
func (f *File) autoFilter(sheet, ref string, refRange, hxAxis int, formatSet *formatAutoFilter) error {
xlsx := f.workSheetReader(sheet)
if xlsx.SheetPr != nil {
xlsx.SheetPr.FilterMode = true
}
xlsx.SheetPr = &xlsxSheetPr{FilterMode: true}
filter := &xlsxAutoFilter{
Ref: ref,
}
xlsx.AutoFilter = filter
if formatSet.Column == "" || formatSet.Expression == "" {
return nil
}
col := titleToNumber(formatSet.Column)
offset := col - hxAxis
if offset < 0 || offset > refRange {
return fmt.Errorf("Incorrect index of column '%s'", formatSet.Column)
}
filter.FilterColumn = &xlsxFilterColumn{
ColID: offset,
}
re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
token := re.FindAllString(formatSet.Expression, -1)
if len(token) != 3 && len(token) != 7 {
return fmt.Errorf("Incorrect number of tokens in criteria '%s'", formatSet.Expression)
}
expressions, tokens, err := f.parseFilterExpression(formatSet.Expression, token)
if err != nil {
return err
}
f.writeAutoFilter(filter, expressions, tokens)
xlsx.AutoFilter = filter
return nil
}
// writeAutoFilter provides funtion 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) {
if len(exp) == 1 && exp[0] == 2 {
// Single equality.
filters := []*xlsxFilter{}
filters = append(filters, &xlsxFilter{Val: tokens[0]})
filter.FilterColumn.Filters = &xlsxFilters{Filter: filters}
} else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
// Double equality with "or" operator.
filters := []*xlsxFilter{}
for _, v := range tokens {
filters = append(filters, &xlsxFilter{Val: v})
}
filter.FilterColumn.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)
if k == 1 {
filter.FilterColumn.CustomFilters.And = andRel[exp[k]]
}
}
}
}
// writeCustomFilter provides function to write the <customFilter> element.
func (f *File) writeCustomFilter(filter *xlsxAutoFilter, operator int, val string) {
operators := map[int]string{
1: "lessThan",
2: "equal",
3: "lessThanOrEqual",
4: "greaterThan",
5: "notEqual",
6: "greaterThanOrEqual",
22: "equal",
}
customFilter := xlsxCustomFilter{
Operator: operators[operator],
Val: val,
}
if filter.FilterColumn.CustomFilters != nil {
filter.FilterColumn.CustomFilters.CustomFilter = append(filter.FilterColumn.CustomFilters.CustomFilter, &customFilter)
} else {
customFilters := []*xlsxCustomFilter{}
customFilters = append(customFilters, &customFilter)
filter.FilterColumn.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
}
}
// parseFilterExpression provides function to converts the tokens of a possibly
// conditional expression into 1 or 2 sub expressions for further parsing.
//
// Examples:
//
// ('x', '==', 2000) -> exp1
// ('x', '>', 2000, 'and', 'x', '<', 5000) -> exp1 and exp2
//
func (f *File) parseFilterExpression(expression string, tokens []string) ([]int, []string, error) {
expressions := []int{}
t := []string{}
if len(tokens) == 7 {
// The number of tokens will be either 3 (for 1 expression) or 7 (for 2
// expressions).
conditional := 0
c := tokens[3]
re, _ := regexp.Match(`(or|\|\|)`, []byte(c))
if re {
conditional = 1
}
expression1, token1, err := f.parseFilterTokens(expression, tokens[0:3])
if err != nil {
return expressions, t, err
}
expression2, token2, err := f.parseFilterTokens(expression, tokens[4:7])
if err != nil {
return expressions, t, err
}
expressions = []int{expression1[0], conditional, expression2[0]}
t = []string{token1, token2}
} else {
exp, token, err := f.parseFilterTokens(expression, tokens)
if err != nil {
return expressions, t, err
}
expressions = exp
t = []string{token}
}
return expressions, t, nil
}
// parseFilterTokens provides function to parse the 3 tokens of a filter
// expression and return the operator and token.
func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, string, error) {
operators := map[string]int{
"==": 2,
"=": 2,
"=~": 2,
"eq": 2,
"!=": 5,
"!~": 5,
"ne": 5,
"<>": 5,
"<": 1,
"<=": 3,
">": 4,
">=": 6,
}
operator, ok := operators[strings.ToLower(tokens[1])]
if !ok {
// Convert the operator from a number to a descriptive string.
return []int{}, "", fmt.Errorf("Unknown operator: %s", tokens[1])
}
token := tokens[2]
// Special handling for Blanks/NonBlanks.
re, _ := regexp.Match("blanks|nonblanks", []byte(strings.ToLower(token)))
if re {
// Only allow Equals or NotEqual in this context.
if operator != 2 && operator != 5 {
return []int{operator}, token, fmt.Errorf("The operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression)
}
token = strings.ToLower(token)
// The operator should always be 2 (=) to flag a "simple" equality in
// the binary record. Therefore we convert <> to =.
if token == "blanks" {
if operator == 5 {
token = " "
}
} else {
if operator == 5 {
operator = 2
token = "blanks"
} else {
operator = 5
token = " "
}
}
}
// if the string token contains an Excel match character then change the
// operator type to indicate a non "simple" equality.
re, _ = regexp.Match("[*?]", []byte(token))
if operator == 2 && re {
operator = 22
}
return []int{operator}, token, nil
}

View File

@ -35,7 +35,118 @@ type xlsxTable struct {
// applied column by column to a table of data in the worksheet. This collection
// expresses AutoFilter settings.
type xlsxAutoFilter struct {
Ref string `xml:"ref,attr"`
Ref string `xml:"ref,attr"`
FilterColumn *xlsxFilterColumn `xml:"filterColumn"`
}
// xlsxFilterColumn directly maps the filterColumn element. The filterColumn
// collection identifies a particular column in the AutoFilter range and
// specifies filter information that has been applied to this column. If a
// column in the AutoFilter range has no criteria specified, then there is no
// corresponding filterColumn collection expressed for that column.
type xlsxFilterColumn struct {
ColID int `xml:"colId,attr"`
HiddenButton bool `xml:"hiddenButton,attr,omitempty"`
ShowButton bool `xml:"showButton,attr,omitempty"`
CustomFilters *xlsxCustomFilters `xml:"customFilters"`
Filters *xlsxFilters `xml:"filters"`
ColorFilter *xlsxColorFilter `xml:"colorFilter"`
DynamicFilter *xlsxDynamicFilter `xml:"dynamicFilter"`
IconFilter *xlsxIconFilter `xml:"iconFilter"`
Top10 *xlsxTop10 `xml:"top10"`
}
// xlsxCustomFilters directly maps the customFilters element. When there is more
// than one custom filter criteria to apply (an 'and' or 'or' joining two
// criteria), then this element groups the customFilter elements together.
type xlsxCustomFilters struct {
And bool `xml:"and,attr,omitempty"`
CustomFilter []*xlsxCustomFilter `xml:"customFilter"`
}
// xlsxCustomFilter directly maps the customFilter element. A custom AutoFilter
// specifies an operator and a value. There can be at most two customFilters
// specified, and in that case the parent element specifies whether the two
// conditions are joined by 'and' or 'or'. For any cells whose values do not
// meet the specified criteria, the corresponding rows shall be hidden from view
// when the filter is applied.
type xlsxCustomFilter struct {
Operator string `xml:"operator,attr,omitempty"`
Val string `xml:"val,attr,omitempty"`
}
// xlsxFilters directly maps the filters (Filter Criteria) element. When
// multiple values are chosen to filter by, or when a group of date values are
// chosen to filter by, this element groups those criteria together.
type xlsxFilters struct {
Blank bool `xml:"blank,attr,omitempty"`
CalendarType string `xml:"calendarType,attr,omitempty"`
Filter []*xlsxFilter `xml:"filter"`
DateGroupItem []*xlsxDateGroupItem `xml:"dateGroupItem"`
}
// xlsxFilter directly maps the filter element. This element expresses a filter
// criteria value.
type xlsxFilter struct {
Val string `xml:"val,attr,omitempty"`
}
// xlsxColorFilter directly maps the colorFilter element. This element specifies
// the color to filter by and whether to use the cell's fill or font color in
// the filter criteria. If the cell's font or fill color does not match the
// color specified in the criteria, the rows corresponding to those cells are
// hidden from view.
type xlsxColorFilter struct {
CellColor bool `xml:"cellColor,attr"`
DxfID int `xml:"dxfId,attr"`
}
// xlsxDynamicFilter directly maps the dynamicFilter element. This collection
// specifies dynamic filter criteria. These criteria are considered dynamic
// because they can change, either with the data itself (e.g., "above average")
// or with the current system date (e.g., show values for "today"). For any
// cells whose values do not meet the specified criteria, the corresponding rows
// shall be hidden from view when the filter is applied.
type xlsxDynamicFilter struct {
MaxValISO string `xml:"maxValIso,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Val float64 `xml:"val,attr,omitempty"`
ValISO string `xml:"valIso,attr,omitempty"`
}
// xlsxIconFilter directly maps the iconFilter element. This element specifies
// the icon set and particular icon within that set to filter by. For any cells
// whose icon does not match the specified criteria, the corresponding rows
// shall be hidden from view when the filter is applied.
type xlsxIconFilter struct {
IconID int `xml:"iconId,attr"`
IconSet string `xml:"iconSet,attr,omitempty"`
}
// xlsxTop10 directly maps the top10 element. This element specifies the top N
// (percent or number of items) to filter by.
type xlsxTop10 struct {
FilterVal float64 `xml:"filterVal,attr,omitempty"`
Percent bool `xml:"percent,attr,omitempty"`
Top bool `xml:"top,attr"`
Val float64 `xml:"val,attr,omitempty"`
}
// xlsxDateGroupItem directly maps the dateGroupItem element. This collection is
// used to express a group of dates or times which are used in an AutoFilter
// criteria. [Note: See parent element for an example. end note] Values are
// always written in the calendar type of the first date encountered in the
// filter range, so that all subsequent dates, even when formatted or
// represented by other calendar types, can be correctly compared for the
// purposes of filtering.
type xlsxDateGroupItem struct {
DateTimeGrouping string `xml:"dateTimeGrouping,attr,omitempty"`
Day int `xml:"day,attr,omitempty"`
Hour int `xml:"hour,attr,omitempty"`
Minute int `xml:"minute,attr,omitempty"`
Month int `xml:"month,attr,omitempty"`
Second int `xml:"second,attr,omitempty"`
Year int `xml:"year,attr,omitempty"`
}
// xlsxTableColumns directly maps the element representing the collection of all
@ -81,3 +192,13 @@ type formatTable struct {
ShowRowStripes bool `json:"show_row_stripes"`
ShowColumnStripes bool `json:"show_column_stripes"`
}
// formatAutoFilter directly maps the auto filter settings.
type formatAutoFilter struct {
Column string `json:"column"`
Expression string `json:"expression"`
FilterList []struct {
Column string `json:"column"`
Value []int `json:"value"`
} `json:"filter_list"`
}

View File

@ -14,6 +14,7 @@ type xlsxWorksheet struct {
Cols *xlsxCols `xml:"cols,omitempty"`
SheetData xlsxSheetData `xml:"sheetData"`
SheetProtection *xlsxSheetProtection `xml:"sheetProtection"`
AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
MergeCells *xlsxMergeCells `xml:"mergeCells,omitempty"`
ConditionalFormatting []*xlsxConditionalFormatting `xml:"conditionalFormatting"`
DataValidations *xlsxDataValidations `xml:"dataValidations"`