From 1f329e8f968014e26351a729ba7e6e3c846e96db Mon Sep 17 00:00:00 2001 From: xuri Date: Tue, 2 Feb 2021 22:23:16 +0800 Subject: [PATCH] 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` --- adjust.go | 12 +++-- adjust_test.go | 4 +- calc.go | 116 ++++++++++++++++++++++++++++++------------------- lib.go | 13 ++++-- sheet.go | 20 ++++++++- sheet_test.go | 13 ++++++ styles.go | 4 +- table.go | 6 +-- 8 files changed, 127 insertions(+), 61 deletions(-) diff --git a/adjust.go b/adjust.go index f1ae5360..c391cb1c 100644 --- a/adjust.go +++ b/adjust.go @@ -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 diff --git a/adjust_test.go b/adjust_test.go index 3997bd92..bdbaebea 100644 --- a/adjust_test.go +++ b/adjust_test.go @@ -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")) diff --git a/calc.go b/calc.go index cb7d2f88..fd039187 100644 --- a/calc.go +++ b/calc.go @@ -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 diff --git a/lib.go b/lib.go index 3f135128..0ebe4683 100644 --- a/lib.go +++ b/lib.go @@ -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. diff --git a/sheet.go b/sheet.go index 9b80395e..bb94f6a1 100644 --- a/sheet.go +++ b/sheet.go @@ -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) diff --git a/sheet_test.go b/sheet_test.go index d68327e9..f218da73 100644 --- a/sheet_test.go +++ b/sheet_test.go @@ -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) { diff --git a/styles.go b/styles.go index 9fd0f185..851332b7 100644 --- a/styles.go +++ b/styles.go @@ -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 { diff --git a/table.go b/table.go index 59f1cfec..87759294 100644 --- a/table.go +++ b/table.go @@ -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)