Przeglądaj źródła

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`

xuri 4 lat temu
rodzic
commit
1f329e8f96
8 zmienionych plików z 127 dodań i 61 usunięć
  1. 8 4
      adjust.go
  2. 2 2
      adjust_test.go
  3. 72 44
      calc.go
  4. 10 3
      lib.go
  5. 18 2
      sheet.go
  6. 13 0
      sheet_test.go
  7. 1 3
      styles.go
  8. 3 3
      table.go

+ 8 - 4
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

+ 2 - 2
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"))

+ 72 - 44
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

+ 10 - 3
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.

+ 18 - 2
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)

+ 13 - 0
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) {

+ 1 - 3
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 {

+ 3 - 3
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)