From 1c167b96a35a58990f777025914283b492d0785f Mon Sep 17 00:00:00 2001 From: xuri Date: Thu, 26 May 2022 00:15:28 +0800 Subject: [PATCH] Improves the calculation engine, docs update, and adds the dependabot - Initialize array formula support for the formula calculation engine - Update example and unit test of `AddPivotTable` - Update the supported hash algorithm of ProtectSheet --- .github/dependabot.yml | 10 ++++++ calc.go | 40 +++++++++++++++++++---- calc_test.go | 73 +++++++++++++++++++++++++++++++----------- pivotTable.go | 12 +++---- pivotTable_test.go | 12 +++---- sheet.go | 10 +++--- 6 files changed, 114 insertions(+), 43 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..dfefea14 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: monthly +- package-ecosystem: gomod + directory: / + schedule: + interval: monthly diff --git a/calc.go b/calc.go index 8a80fb5f..f636d7ff 100644 --- a/calc.go +++ b/calc.go @@ -829,6 +829,8 @@ func newEmptyFormulaArg() formulaArg { func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, error) { var err error opdStack, optStack, opfStack, opfdStack, opftStack, argsStack := NewStack(), NewStack(), NewStack(), NewStack(), NewStack(), NewStack() + var inArray, inArrayRow bool + var arrayRow []formulaArg for i := 0; i < len(tokens); i++ { token := tokens[i] @@ -841,6 +843,14 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, // function start if isFunctionStartToken(token) { + if token.TValue == "ARRAY" { + inArray = true + continue + } + if token.TValue == "ARRAYROW" { + inArrayRow = true + continue + } opfStack.Push(token) argsStack.Push(list.New().Init()) opftStack.Push(token) // to know which operators belong to a function use the function as a separator @@ -922,7 +932,19 @@ func (f *File) evalInfixExp(sheet, cell string, tokens []efp.Token) (formulaArg, if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) } - + if inArrayRow && isOperand(token) { + arrayRow = append(arrayRow, tokenToFormulaArg(token)) + continue + } + if inArrayRow && isFunctionStopToken(token) { + inArrayRow = false + continue + } + if inArray && isFunctionStopToken(token) { + argsStack.Peek().(*list.List).PushBack(opfdStack.Pop()) + arrayRow, inArray = []formulaArg{}, false + continue + } if err = f.evalInfixExpFunc(sheet, cell, token, nextToken, opfStack, opdStack, opftStack, opfdStack, argsStack); err != nil { return newEmptyFormulaArg(), err } @@ -1274,6 +1296,15 @@ func isOperand(token efp.Token) bool { return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) } +// tokenToFormulaArg create a formula argument by given token. +func tokenToFormulaArg(token efp.Token) formulaArg { + if token.TSubType == efp.TokenSubTypeNumber { + num, _ := strconv.ParseFloat(token.TValue, 64) + return newNumberFormulaArg(num) + } + return newStringFormulaArg(token.TValue) +} + // parseToken parse basic arithmetic operator priority and evaluate based on // operators and operands. func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Stack) error { @@ -1318,12 +1349,7 @@ func (f *File) parseToken(sheet string, token efp.Token, opdStack, optStack *Sta } // opd if isOperand(token) { - if token.TSubType == efp.TokenSubTypeNumber { - num, _ := strconv.ParseFloat(token.TValue, 64) - opdStack.Push(newNumberFormulaArg(num)) - } else { - opdStack.Push(newStringFormulaArg(token.TValue)) - } + opdStack.Push(tokenToFormulaArg(token)) } return nil } diff --git a/calc_test.go b/calc_test.go index 2cbcc97b..b71e93bb 100644 --- a/calc_test.go +++ b/calc_test.go @@ -60,55 +60,88 @@ func TestCalcCellValue(t *testing.T) { "=1&2": "12", "=15%": "0.15", "=1+20%": "1.2", + "={1}+2": "3", + "=1+{2}": "3", + "={1}+{2}": "3", `="A"="A"`: "TRUE", `="A"<>"A"`: "FALSE", // Engineering Functions // BESSELI - "=BESSELI(4.5,1)": "15.3892227537359", - "=BESSELI(32,1)": "5502845511211.25", + "=BESSELI(4.5,1)": "15.3892227537359", + "=BESSELI(32,1)": "5502845511211.25", + "=BESSELI({32},1)": "5502845511211.25", + "=BESSELI(32,{1})": "5502845511211.25", + "=BESSELI({32},{1})": "5502845511211.25", // BESSELJ - "=BESSELJ(1.9,2)": "0.329925727692387", + "=BESSELJ(1.9,2)": "0.329925727692387", + "=BESSELJ({1.9},2)": "0.329925727692387", + "=BESSELJ(1.9,{2})": "0.329925727692387", + "=BESSELJ({1.9},{2})": "0.329925727692387", // BESSELK - "=BESSELK(0.05,0)": "3.11423403428966", - "=BESSELK(0.05,1)": "19.9096743272486", - "=BESSELK(0.05,2)": "799.501207124235", - "=BESSELK(3,2)": "0.0615104585619118", + "=BESSELK(0.05,0)": "3.11423403428966", + "=BESSELK(0.05,1)": "19.9096743272486", + "=BESSELK(0.05,2)": "799.501207124235", + "=BESSELK(3,2)": "0.0615104585619118", + "=BESSELK({3},2)": "0.0615104585619118", + "=BESSELK(3,{2})": "0.0615104585619118", + "=BESSELK({3},{2})": "0.0615104585619118", // BESSELY - "=BESSELY(0.05,0)": "-1.97931100684153", - "=BESSELY(0.05,1)": "-12.789855163794", - "=BESSELY(0.05,2)": "-509.61489554492", - "=BESSELY(9,2)": "-0.229082087487741", + "=BESSELY(0.05,0)": "-1.97931100684153", + "=BESSELY(0.05,1)": "-12.789855163794", + "=BESSELY(0.05,2)": "-509.61489554492", + "=BESSELY(9,2)": "-0.229082087487741", + "=BESSELY({9},2)": "-0.229082087487741", + "=BESSELY(9,{2})": "-0.229082087487741", + "=BESSELY({9},{2})": "-0.229082087487741", // BIN2DEC "=BIN2DEC(\"10\")": "2", "=BIN2DEC(\"11\")": "3", "=BIN2DEC(\"0000000010\")": "2", "=BIN2DEC(\"1111111110\")": "-2", "=BIN2DEC(\"110\")": "6", + "=BIN2DEC({\"110\"})": "6", // BIN2HEX "=BIN2HEX(\"10\")": "2", "=BIN2HEX(\"0000000001\")": "1", "=BIN2HEX(\"10\",10)": "0000000002", "=BIN2HEX(\"1111111110\")": "FFFFFFFFFE", "=BIN2HEX(\"11101\")": "1D", + "=BIN2HEX({\"11101\"})": "1D", // BIN2OCT "=BIN2OCT(\"101\")": "5", "=BIN2OCT(\"0000000001\")": "1", "=BIN2OCT(\"10\",10)": "0000000002", "=BIN2OCT(\"1111111110\")": "7777777776", "=BIN2OCT(\"1110\")": "16", + "=BIN2OCT({\"1110\"})": "16", // BITAND - "=BITAND(13,14)": "12", + "=BITAND(13,14)": "12", + "=BITAND({13},14)": "12", + "=BITAND(13,{14})": "12", + "=BITAND({13},{14})": "12", // BITLSHIFT - "=BITLSHIFT(5,2)": "20", - "=BITLSHIFT(3,5)": "96", + "=BITLSHIFT(5,2)": "20", + "=BITLSHIFT({3},5)": "96", + "=BITLSHIFT(3,5)": "96", + "=BITLSHIFT(3,{5})": "96", + "=BITLSHIFT({3},{5})": "96", // BITOR - "=BITOR(9,12)": "13", + "=BITOR(9,12)": "13", + "=BITOR({9},12)": "13", + "=BITOR(9,{12})": "13", + "=BITOR({9},{12})": "13", // BITRSHIFT - "=BITRSHIFT(20,2)": "5", - "=BITRSHIFT(52,4)": "3", + "=BITRSHIFT(20,2)": "5", + "=BITRSHIFT(52,4)": "3", + "=BITRSHIFT({52},4)": "3", + "=BITRSHIFT(52,{4})": "3", + "=BITRSHIFT({52},{4})": "3", // BITXOR - "=BITXOR(5,6)": "3", - "=BITXOR(9,12)": "5", + "=BITXOR(5,6)": "3", + "=BITXOR(9,12)": "5", + "=BITXOR({9},12)": "5", + "=BITXOR(9,{12})": "5", + "=BITXOR({9},{12})": "5", // COMPLEX "=COMPLEX(5,2)": "5+2i", "=COMPLEX(5,-9)": "5-9i", @@ -221,6 +254,7 @@ func TestCalcCellValue(t *testing.T) { "=HEX2OCT(\"8\",10)": "0000000010", "=HEX2OCT(\"FFFFFFFFF8\")": "7777777770", "=HEX2OCT(\"1F3\")": "763", + "=HEX2OCT({\"1F3\"})": "763", // IMABS "=IMABS(\"2j\")": "2", "=IMABS(\"-1+2i\")": "2.23606797749979", @@ -773,6 +807,7 @@ func TestCalcCellValue(t *testing.T) { "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.6666666666667", "=SUM(1+ROW())": "2", "=SUM((SUM(2))+1)": "3", + "=SUM({1,2,3,4,\"\"})": "10", // SUMIF `=SUMIF(F1:F5, "")`: "0", `=SUMIF(A1:A5, "3")`: "3", diff --git a/pivotTable.go b/pivotTable.go index 28c86329..de671f75 100644 --- a/pivotTable.go +++ b/pivotTable.go @@ -102,12 +102,12 @@ type PivotTableField struct { // types := []string{"Meat", "Dairy", "Beverages", "Produce"} // region := []string{"East", "West", "North", "South"} // f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}) -// for i := 0; i < 30; i++ { -// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)]) -// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000)) -// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)]) +// for row := 2; row < 32; row++ { +// f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]) +// f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)) +// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) // } // if err := f.AddPivotTable(&excelize.PivotTableOption{ // DataRange: "Sheet1!$A$1:$E$31", diff --git a/pivotTable_test.go b/pivotTable_test.go index 2f95ed49..adba2eb1 100644 --- a/pivotTable_test.go +++ b/pivotTable_test.go @@ -18,12 +18,12 @@ func TestAddPivotTable(t *testing.T) { types := []string{"Meat", "Dairy", "Beverages", "Produce"} region := []string{"East", "West", "North", "South"} assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"})) - for i := 0; i < 30; i++ { - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), month[rand.Intn(12)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), year[rand.Intn(3)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), types[rand.Intn(4)])) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), rand.Intn(5000))) - assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), region[rand.Intn(4)])) + for row := 2; row < 32; row++ { + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), year[rand.Intn(3)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)])) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000))) + assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) } assert.NoError(t, f.AddPivotTable(&PivotTableOption{ DataRange: "Sheet1!$A$1:$E$31", diff --git a/sheet.go b/sheet.go index 1c17f782..47a20638 100644 --- a/sheet.go +++ b/sheet.go @@ -1069,12 +1069,12 @@ func (f *File) SetHeaderFooter(sheet string, settings *FormatHeaderFooter) error return err } -// ProtectSheet provides a function to prevent other users from accidentally -// or deliberately changing, moving, or deleting data in a worksheet. The +// ProtectSheet provides a function to prevent other users from accidentally or +// deliberately changing, moving, or deleting data in a worksheet. The // optional field AlgorithmName specified hash algorithm, support XOR, MD4, -// MD5, SHA1, SHA256, SHA384, and SHA512 currently, if no hash algorithm -// specified, will be using the XOR algorithm as default. For example, -// protect Sheet1 with protection settings: +// MD5, SHA-1, SHA2-56, SHA-384, and SHA-512 currently, if no hash algorithm +// specified, will be using the XOR algorithm as default. For example, protect +// Sheet1 with protection settings: // // err := f.ProtectSheet("Sheet1", &excelize.FormatSheetProtection{ // AlgorithmName: "SHA-512",