table.go 13.5 KB
Newer Older
xurime's avatar
xurime 已提交
1
// Copyright 2016 - 2019 The excelize Authors. All rights reserved. Use of
xurime's avatar
xurime 已提交
2 3 4 5 6 7 8
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to
// and read from XLSX files. Support reads and writes XLSX file generated by
// Microsoft Excel™ 2007 and later. Support save file without losing original
// charts of XLSX. This library needs Go version 1.8 or later.
xurime's avatar
xurime 已提交
9

xurime's avatar
xurime 已提交
10 11 12 13 14
package excelize

import (
	"encoding/json"
	"encoding/xml"
15 16
	"fmt"
	"regexp"
xurime's avatar
xurime 已提交
17 18 19 20
	"strconv"
	"strings"
)

xurime's avatar
xurime 已提交
21
// parseFormatTableSet provides a function to parse the format settings of the
xurime's avatar
xurime 已提交
22
// table with default value.
23
func parseFormatTableSet(formatSet string) (*formatTable, error) {
xurime's avatar
xurime 已提交
24 25 26 27
	format := formatTable{
		TableStyle:     "",
		ShowRowStripes: true,
	}
xurime's avatar
xurime 已提交
28
	err := json.Unmarshal(parseFormatSet(formatSet), &format)
29
	return &format, err
xurime's avatar
xurime 已提交
30 31
}

xurime's avatar
xurime 已提交
32 33
// AddTable provides the method to add table in a worksheet by given worksheet
// name, coordinate area and format set. For example, create a table of A1:D5
xurime's avatar
xurime 已提交
34 35 36 37 38 39
// on Sheet1:
//
//    xlsx.AddTable("Sheet1", "A1", "D5", ``)
//
// Create a table of F2:H6 on Sheet2 with format set:
//
40
//    xlsx.AddTable("Sheet2", "F2", "H6", `{"table_name":"table","table_style":"TableStyleMedium2", "show_first_column":true,"show_last_column":true,"show_row_stripes":false,"show_column_stripes":true}`)
xurime's avatar
xurime 已提交
41
//
42 43 44 45
// Note that the table at least two lines include string type header. Multiple
// tables coordinate areas can't have an intersection.
//
// table_name: The name of the table, in the same worksheet name of the table should be unique
xurime's avatar
xurime 已提交
46 47 48 49 50 51 52
//
// table_style: The built-in table style names
//
//    TableStyleLight1 - TableStyleLight21
//    TableStyleMedium1 - TableStyleMedium28
//    TableStyleDark1 - TableStyleDark11
//
53 54 55 56 57
func (f *File) AddTable(sheet, hcell, vcell, format string) error {
	formatSet, err := parseFormatTableSet(format)
	if err != nil {
		return err
	}
xurime's avatar
xurime 已提交
58
	// Coordinate conversion, convert C1:B3 to 2,0,1,2.
59 60
	hcol, hrow := MustCellNameToCoordinates(hcell)
	vcol, vrow := MustCellNameToCoordinates(vcell)
xurime's avatar
xurime 已提交
61

62 63
	if vcol < hcol {
		vcol, hcol = hcol, vcol
xurime's avatar
xurime 已提交
64
	}
65 66 67

	if vrow < hrow {
		vrow, hrow = hrow, vrow
xurime's avatar
xurime 已提交
68
	}
69

xurime's avatar
xurime 已提交
70 71 72 73 74 75
	tableID := f.countTables() + 1
	sheetRelationshipsTableXML := "../tables/table" + strconv.Itoa(tableID) + ".xml"
	tableXML := strings.Replace(sheetRelationshipsTableXML, "..", "xl", -1)
	// Add first table for given sheet.
	rID := f.addSheetRelationships(sheet, SourceRelationshipTable, sheetRelationshipsTableXML, "")
	f.addSheetTable(sheet, rID)
76
	f.addTable(sheet, tableXML, hcol, hrow, vcol, vrow, tableID, formatSet)
xurime's avatar
xurime 已提交
77
	f.addContentTypePart(tableID, "table")
78
	return err
xurime's avatar
xurime 已提交
79 80
}

xurime's avatar
xurime 已提交
81 82
// countTables provides a function to get table files count storage in the
// folder xl/tables.
xurime's avatar
xurime 已提交
83 84 85 86 87 88 89 90 91 92
func (f *File) countTables() int {
	count := 0
	for k := range f.XLSX {
		if strings.Contains(k, "xl/tables/table") {
			count++
		}
	}
	return count
}

xurime's avatar
xurime 已提交
93
// addSheetTable provides a function to add tablePart element to
94
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
xurime's avatar
xurime 已提交
95 96 97 98 99
func (f *File) addSheetTable(sheet string, rID int) {
	xlsx := f.workSheetReader(sheet)
	table := &xlsxTablePart{
		RID: "rId" + strconv.Itoa(rID),
	}
100 101
	if xlsx.TableParts == nil {
		xlsx.TableParts = &xlsxTableParts{}
xurime's avatar
xurime 已提交
102
	}
103 104
	xlsx.TableParts.Count++
	xlsx.TableParts.TableParts = append(xlsx.TableParts.TableParts, table)
xurime's avatar
xurime 已提交
105 106
}

xurime's avatar
xurime 已提交
107 108
// addTable provides a function to add table by given worksheet name,
// coordinate area and format set.
109
func (f *File) addTable(sheet, tableXML string, hcol, hrow, vcol, vrow, i int, formatSet *formatTable) {
xurime's avatar
xurime 已提交
110
	// Correct the minimum number of rows, the table at least two lines.
111 112
	if hrow == vrow {
		vrow++
xurime's avatar
xurime 已提交
113
	}
114

xurime's avatar
xurime 已提交
115
	// Correct table reference coordinate area, such correct C1:B3 to B1:C3.
116 117 118 119 120 121
	ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow)

	var (
		tableColumn []*xlsxTableColumn
	)

xurime's avatar
xurime 已提交
122
	idx := 0
123
	for i := hcol; i <= vcol; i++ {
xurime's avatar
xurime 已提交
124
		idx++
125
		cell := MustCoordinatesToCellName(i, hrow)
xurime's avatar
xurime 已提交
126 127 128 129 130 131 132 133 134 135 136 137 138
		name := f.GetCellValue(sheet, cell)
		if _, err := strconv.Atoi(name); err == nil {
			f.SetCellStr(sheet, cell, name)
		}
		if name == "" {
			name = "Column" + strconv.Itoa(idx)
			f.SetCellStr(sheet, cell, name)
		}
		tableColumn = append(tableColumn, &xlsxTableColumn{
			ID:   idx,
			Name: name,
		})
	}
139 140 141 142
	name := formatSet.TableName
	if name == "" {
		name = "Table" + strconv.Itoa(i)
	}
xurime's avatar
xurime 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
	t := xlsxTable{
		XMLNS:       NameSpaceSpreadSheet,
		ID:          i,
		Name:        name,
		DisplayName: name,
		Ref:         ref,
		AutoFilter: &xlsxAutoFilter{
			Ref: ref,
		},
		TableColumns: &xlsxTableColumns{
			Count:       idx,
			TableColumn: tableColumn,
		},
		TableStyleInfo: &xlsxTableStyleInfo{
			Name:              formatSet.TableStyle,
			ShowFirstColumn:   formatSet.ShowFirstColumn,
			ShowLastColumn:    formatSet.ShowLastColumn,
			ShowRowStripes:    formatSet.ShowRowStripes,
			ShowColumnStripes: formatSet.ShowColumnStripes,
		},
	}
	table, _ := xml.Marshal(t)
165
	f.saveFileList(tableXML, table)
xurime's avatar
xurime 已提交
166
}
167

xurime's avatar
xurime 已提交
168
// parseAutoFilterSet provides a function to parse the settings of the auto
169
// filter.
170
func parseAutoFilterSet(formatSet string) (*formatAutoFilter, error) {
171
	format := formatAutoFilter{}
172 173
	err := json.Unmarshal([]byte(formatSet), &format)
	return &format, err
174 175 176
}

// AutoFilter provides the method to add auto filter in a worksheet by given
xurime's avatar
xurime 已提交
177 178 179
// worksheet name, 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 Sheet1:
180 181 182 183 184 185 186
//
//    err = xlsx.AutoFilter("Sheet1", "A1", "D4", "")
//
// Filter data in an autofilter:
//
//    err = xlsx.AutoFilter("Sheet1", "A1", "D4", `{"column":"B","expression":"x != blanks"}`)
//
187 188
// column defines the filter columns in a autofilter range based on simple
// criteria
189
//
xurime's avatar
xurime 已提交
190 191 192 193
// 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 SetRowVisible() method. Excelize can't filter rows automatically since
// this isn't part of the file format.
194 195 196
//
// Setting a filter criteria for a column:
//
xurime's avatar
xurime 已提交
197 198
// expression defines the conditions, the following operators are available
// for setting the filter criteria:
199 200 201 202 203 204 205 206 207 208
//
//    ==
//    !=
//    >
//    <
//    >=
//    <=
//    and
//    or
//
xurime's avatar
xurime 已提交
209 210
// An expression can comprise a single statement or two statements separated
// by the 'and' and 'or' operators. For example:
211 212 213 214 215 216 217
//
//    x <  2000
//    x >  2000
//    x == 2000
//    x >  2000 and x <  5000
//    x == 2000 or  x == 5000
//
218 219
// Filtering of blank or non-blank data can be achieved by using a value of
// Blanks or NonBlanks in the expression:
220 221 222 223 224 225 226 227 228 229 230 231 232
//
//    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
//
233 234 235 236
// 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 '~'.
237
//
238 239 240
// 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:
241 242 243 244 245 246
//
//    x     < 2000
//    col   < 2000
//    Price < 2000
//
func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
247 248
	hcol, hrow := MustCellNameToCoordinates(hcell)
	vcol, vrow := MustCellNameToCoordinates(vcell)
249

250 251
	if vcol < hcol {
		vcol, hcol = hcol, vcol
252 253
	}

254 255
	if vrow < hrow {
		vrow, hrow = hrow, vrow
256
	}
257 258 259 260 261

	formatSet, _ := parseAutoFilterSet(format)
	ref := MustCoordinatesToCellName(hcol, hrow) + ":" + MustCoordinatesToCellName(vcol, vrow)
	refRange := vcol - hcol
	return f.autoFilter(sheet, ref, refRange, hcol, formatSet)
262 263
}

xurime's avatar
xurime 已提交
264
// autoFilter provides a function to extract the tokens from the filter
265
// expression. The tokens are mainly non-whitespace groups.
266
func (f *File) autoFilter(sheet, ref string, refRange, col int, formatSet *formatAutoFilter) error {
267 268 269 270 271 272 273 274 275 276 277 278
	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
	}
279 280 281

	fsCol := MustColumnNameToNumber(formatSet.Column)
	offset := fsCol - col
282
	if offset < 0 || offset > refRange {
xurime's avatar
xurime 已提交
283
		return fmt.Errorf("incorrect index of column '%s'", formatSet.Column)
284
	}
285

286 287 288 289 290 291
	filter.FilterColumn = &xlsxFilterColumn{
		ColID: offset,
	}
	re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
	token := re.FindAllString(formatSet.Expression, -1)
	if len(token) != 3 && len(token) != 7 {
xurime's avatar
xurime 已提交
292
		return fmt.Errorf("incorrect number of tokens in criteria '%s'", formatSet.Expression)
293 294 295 296 297 298 299 300 301 302
	}
	expressions, tokens, err := f.parseFilterExpression(formatSet.Expression, token)
	if err != nil {
		return err
	}
	f.writeAutoFilter(filter, expressions, tokens)
	xlsx.AutoFilter = filter
	return nil
}

xurime's avatar
xurime 已提交
303 304
// writeAutoFilter provides a function to check for single or double custom
// filters as default filters and handle them accordingly.
305 306 307
func (f *File) writeAutoFilter(filter *xlsxAutoFilter, exp []int, tokens []string) {
	if len(exp) == 1 && exp[0] == 2 {
		// Single equality.
308
		var filters []*xlsxFilter
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
		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]]
			}
		}
	}
}

xurime's avatar
xurime 已提交
331
// writeCustomFilter provides a function to write the <customFilter> element.
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
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}
	}
}

xurime's avatar
xurime 已提交
355 356 357
// parseFilterExpression provides a function to converts the tokens of a
// possibly conditional expression into 1 or 2 sub expressions for further
// parsing.
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
//
// 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
}

xurime's avatar
xurime 已提交
397
// parseFilterTokens provides a function to parse the 3 tokens of a filter
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
// 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.
xurime's avatar
xurime 已提交
417
		return []int{}, "", fmt.Errorf("unknown operator: %s", tokens[1])
418 419 420 421 422 423 424
	}
	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 {
xurime's avatar
xurime 已提交
425
			return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression)
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
		}
		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
}