This closes #774, closes #775 and closes #776

- correct adjust calculation chain in duplicate rows
- correct adjust defined name in the workbook when delete worksheet
- use absolute reference in the auto filters defined name to make it compatible with OpenOffice
- API `CoordinatesToCellName` have a new optional param to specify if using an absolute reference format
- Fix cyclomatic complexity issue of internal function `newFills` and `parseToken`
This commit is contained in:
xuri 2021-02-02 22:23:16 +08:00
parent db7b4ee362
commit 1f329e8f96
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
8 changed files with 127 additions and 61 deletions

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -39,6 +39,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
if err != nil {
return err
}
sheetID := f.getSheetID(sheet)
if dir == rows {
f.adjustRowDimensions(ws, num, offset)
} else {
@ -51,7 +52,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
if err = f.adjustAutoFilter(ws, dir, num, offset); err != nil {
return err
}
if err = f.adjustCalcChain(dir, num, offset); err != nil {
if err = f.adjustCalcChain(dir, num, offset, sheetID); err != nil {
return err
}
checkSheet(ws)
@ -197,7 +198,7 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu
// areaRefToCoordinates provides a function to convert area reference to a
// pair of coordinates.
func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
rng := strings.Split(ref, ":")
rng := strings.Split(strings.Replace(ref, "$", "", -1), ":")
return areaRangeToCoordinates(rng[0], rng[1])
}
@ -310,11 +311,14 @@ func (f *File) deleteMergeCell(ws *xlsxWorksheet, idx int) {
// adjustCalcChain provides a function to update the calculation chain when
// inserting or deleting rows or columns.
func (f *File) adjustCalcChain(dir adjustDirection, num, offset int) error {
func (f *File) adjustCalcChain(dir adjustDirection, num, offset, sheetID int) error {
if f.CalcChain == nil {
return nil
}
for index, c := range f.CalcChain.C {
if c.I != sheetID {
continue
}
colNum, rowNum, err := CellNameToCoordinates(c.R)
if err != nil {
return err

View File

@ -98,13 +98,13 @@ func TestAdjustCalcChain(t *testing.T) {
f := NewFile()
f.CalcChain = &xlsxCalcChain{
C: []xlsxCalcChainC{
{R: "B2"},
{R: "B2", I: 2}, {R: "B2", I: 1},
},
}
assert.NoError(t, f.InsertCol("Sheet1", "A"))
assert.NoError(t, f.InsertRow("Sheet1", 1))
f.CalcChain.C[0].R = "invalid coordinates"
f.CalcChain.C[1].R = "invalid coordinates"
assert.EqualError(t, f.InsertCol("Sheet1", "A"), `cannot convert cell "invalid coordinates" to coordinates: invalid cell name "invalid coordinates"`)
f.CalcChain = nil
assert.NoError(t, f.InsertCol("Sheet1", "A"))

116
calc.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -288,7 +288,7 @@ func getPriority(token efp.Token) (pri int) {
if token.TValue == "-" && token.TType == efp.TokenTypeOperatorPrefix {
pri = 6
}
if token.TSubType == efp.TokenSubTypeStart && token.TType == efp.TokenTypeSubexpression { // (
if isBeginParenthesesToken(token) { // (
pri = 0
}
return
@ -356,7 +356,7 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error)
}
// function start
if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart {
if isFunctionStartToken(token) {
opfStack.Push(token)
argsStack.Push(list.New().Init())
continue
@ -436,44 +436,8 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error)
Type: ArgString,
})
}
// current token is function stop
if token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop {
for !opftStack.Empty() {
// calculate trigger
topOpt := opftStack.Peek().(efp.Token)
if err := calculate(opfdStack, topOpt); err != nil {
return efp.Token{}, err
}
opftStack.Pop()
}
// push opfd to args
if opfdStack.Len() > 0 {
argsStack.Peek().(*list.List).PushBack(formulaArg{
String: opfdStack.Pop().(efp.Token).TValue,
Type: ArgString,
})
}
// call formula function to evaluate
arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer(
"_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue),
[]reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))})
if arg.Type == ArgError {
return efp.Token{}, errors.New(arg.Value())
}
argsStack.Pop()
opfStack.Pop()
if opfStack.Len() > 0 { // still in function stack
if nextToken.TType == efp.TokenTypeOperatorInfix {
// mathematics calculate in formula function
opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
} else {
argsStack.Peek().(*list.List).PushBack(arg)
}
} else {
opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
if err = evalInfixExpFunc(token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil {
return efp.Token{}, err
}
}
}
@ -490,6 +454,50 @@ func (f *File) evalInfixExp(sheet string, tokens []efp.Token) (efp.Token, error)
return opdStack.Peek().(efp.Token), err
}
// evalInfixExpFunc evaluate formula function in the infix expression.
func evalInfixExpFunc(token, nextToken efp.Token, opfStack, opdStack, opftStack, opfdStack, argsStack *Stack) error {
if !isFunctionStopToken(token) {
return nil
}
// current token is function stop
for !opftStack.Empty() {
// calculate trigger
topOpt := opftStack.Peek().(efp.Token)
if err := calculate(opfdStack, topOpt); err != nil {
return err
}
opftStack.Pop()
}
// push opfd to args
if opfdStack.Len() > 0 {
argsStack.Peek().(*list.List).PushBack(formulaArg{
String: opfdStack.Pop().(efp.Token).TValue,
Type: ArgString,
})
}
// call formula function to evaluate
arg := callFuncByName(&formulaFuncs{}, strings.NewReplacer(
"_xlfn", "", ".", "").Replace(opfStack.Peek().(efp.Token).TValue),
[]reflect.Value{reflect.ValueOf(argsStack.Peek().(*list.List))})
if arg.Type == ArgError {
return errors.New(arg.Value())
}
argsStack.Pop()
opfStack.Pop()
if opfStack.Len() > 0 { // still in function stack
if nextToken.TType == efp.TokenTypeOperatorInfix {
// mathematics calculate in formula function
opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
} else {
argsStack.Peek().(*list.List).PushBack(arg)
}
} else {
opdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber})
}
return nil
}
// calcPow evaluate exponentiation arithmetic operations.
func calcPow(rOpd, lOpd string, opdStack *Stack) error {
lOpdVal, err := strconv.ParseFloat(lOpd, 64)
@ -722,6 +730,26 @@ func (f *File) parseOperatorPrefixToken(optStack, opdStack *Stack, token efp.Tok
return
}
// isFunctionStartToken determine if the token is function stop.
func isFunctionStartToken(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStart
}
// isFunctionStopToken determine if the token is function stop.
func isFunctionStopToken(token efp.Token) bool {
return token.TType == efp.TokenTypeFunction && token.TSubType == efp.TokenSubTypeStop
}
// isBeginParenthesesToken determine if the token is begin parentheses: (.
func isBeginParenthesesToken(token efp.Token) bool {
return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart
}
// isEndParenthesesToken determine if the token is end parentheses: ).
func isEndParenthesesToken(token efp.Token) bool {
return token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop
}
// isOperatorPrefixToken determine if the token is parse operator prefix
// token.
func isOperatorPrefixToken(token efp.Token) bool {
@ -771,11 +799,11 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta
return err
}
}
if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStart { // (
if isBeginParenthesesToken(token) { // (
optStack.Push(token)
}
if token.TType == efp.TokenTypeSubexpression && token.TSubType == efp.TokenSubTypeStop { // )
for optStack.Peek().(efp.Token).TSubType != efp.TokenSubTypeStart && optStack.Peek().(efp.Token).TType != efp.TokenTypeSubexpression { // != (
if isEndParenthesesToken(token) { // )
for !isBeginParenthesesToken(optStack.Peek().(efp.Token)) { // != (
topOpt := optStack.Peek().(efp.Token)
if err := calculate(opdStack, topOpt); err != nil {
return err

13
lib.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -201,13 +201,20 @@ func CellNameToCoordinates(cell string) (int, int, error) {
// Example:
//
// excelize.CoordinatesToCellName(1, 1) // returns "A1", nil
// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil
//
func CoordinatesToCellName(col, row int) (string, error) {
func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
if col < 1 || row < 1 {
return "", fmt.Errorf("invalid cell coordinates [%d, %d]", col, row)
}
sign := ""
for _, a := range abs {
if a {
sign = "$"
}
}
colname, err := ColumnNumberToName(col)
return colname + strconv.Itoa(row), err
return sign + colname + sign + strconv.Itoa(row), err
}
// boolPtr returns a pointer to a bool with the given value.

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -500,6 +500,22 @@ func (f *File) DeleteSheet(name string) {
wb := f.workbookReader()
wbRels := f.relsReader(f.getWorkbookRelsPath())
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
deleteSheetID := f.getSheetID(name)
// Delete and adjust defined names
if wb.DefinedNames != nil {
for idx := 0; idx < len(wb.DefinedNames.DefinedName); idx++ {
dn := wb.DefinedNames.DefinedName[idx]
if dn.LocalSheetID != nil {
sheetID := *dn.LocalSheetID + 1
if sheetID == deleteSheetID {
wb.DefinedNames.DefinedName = append(wb.DefinedNames.DefinedName[:idx], wb.DefinedNames.DefinedName[idx+1:]...)
idx--
} else if sheetID > deleteSheetID {
wb.DefinedNames.DefinedName[idx].LocalSheetID = intPtr(*dn.LocalSheetID - 1)
}
}
}
}
for idx, sheet := range wb.Sheets.Sheet {
if sheet.Name == sheetName {
wb.Sheets.Sheet = append(wb.Sheets.Sheet[:idx], wb.Sheets.Sheet[idx+1:]...)
@ -517,7 +533,7 @@ func (f *File) DeleteSheet(name string) {
}
target := f.deleteSheetFromWorkbookRels(sheet.ID)
f.deleteSheetFromContentTypes(target)
f.deleteCalcChain(sheet.SheetID, "") // Delete CalcChain
f.deleteCalcChain(sheet.SheetID, "")
delete(f.sheetMap, sheetName)
delete(f.XLSX, sheetXML)
delete(f.XLSX, rels)

View File

@ -383,6 +383,19 @@ func TestDeleteSheet(t *testing.T) {
f.DeleteSheet("Sheet1")
assert.Equal(t, "Sheet2", f.GetSheetName(f.GetActiveSheetIndex()))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet.xlsx")))
// Test with auto filter defined names
f = NewFile()
f.NewSheet("Sheet2")
f.NewSheet("Sheet3")
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A"))
assert.NoError(t, f.SetCellValue("Sheet2", "A1", "A"))
assert.NoError(t, f.SetCellValue("Sheet3", "A1", "A"))
assert.NoError(t, f.AutoFilter("Sheet1", "A1", "A1", ""))
assert.NoError(t, f.AutoFilter("Sheet2", "A1", "A1", ""))
assert.NoError(t, f.AutoFilter("Sheet3", "A1", "A1", ""))
f.DeleteSheet("Sheet2")
f.DeleteSheet("Sheet1")
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx")))
}
func BenchmarkNewSheet(b *testing.B) {

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -2410,8 +2410,6 @@ func newFills(style *Style, fg bool) *xlsxFill {
gradient.Left = 0.5
gradient.Right = 0.5
gradient.Top = 0.5
default:
break
}
var stops []*xlsxGradientFillStop
for index, color := range style.Fill.Color {

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2020 The excelize Authors. All rights reserved. Use of
// Copyright 2016 - 2021 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
@ -287,8 +287,8 @@ func (f *File) AutoFilter(sheet, hcell, vcell, format string) error {
}
formatSet, _ := parseAutoFilterSet(format)
cellStart, _ := CoordinatesToCellName(hcol, hrow)
cellEnd, _ := CoordinatesToCellName(vcol, vrow)
cellStart, _ := CoordinatesToCellName(hcol, hrow, true)
cellEnd, _ := CoordinatesToCellName(vcol, vrow, true)
ref, filterDB := cellStart+":"+cellEnd, "_xlnm._FilterDatabase"
wb := f.workbookReader()
sheetID := f.GetSheetIndex(sheet)