From 42a9665aa91912f570bb83052b20cc50529d7b6a Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 28 Feb 2022 01:01:24 +0800 Subject: [PATCH] ref #65: formula function INDIRECT support and formula engine improvement Support calculation with the none parameter formula function after infix operator notation --- calc.go | 137 +++++++++++++++++++++++++++++++++++++++------------ calc_test.go | 22 +++++++++ 2 files changed, 127 insertions(+), 32 deletions(-) diff --git a/calc.go b/calc.go index d4828be8..8358d333 100644 --- a/calc.go +++ b/calc.go @@ -459,6 +459,7 @@ type formulaFuncs struct { // IMSUM // IMTAN // INDEX +// INDIRECT // INT // INTRATE // IPMT @@ -851,22 +852,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, if !isFunctionStopToken(token) { return nil } - // current token is function stop - for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { - // calculate trigger - topOpt := opftStack.Peek().(efp.Token) - if err := calculate(opfdStack, topOpt); err != nil { - argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error())) - opftStack.Pop() - continue - } - opftStack.Pop() - } - - // push opfd to args - if opfdStack.Len() > 0 { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) - } + prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack) // call formula function to evaluate arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( "_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), @@ -894,6 +880,34 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, return nil } +// prepareEvalInfixExp check the token and stack state for formula function +// evaluate. +func prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack *Stack) { + // current token is function stop + for opftStack.Peek().(efp.Token) != opfStack.Peek().(efp.Token) { + // calculate trigger + topOpt := opftStack.Peek().(efp.Token) + if err := calculate(opfdStack, topOpt); err != nil { + argsStack.Peek().(*list.List).PushBack(newErrorFormulaArg(err.Error(), err.Error())) + opftStack.Pop() + continue + } + opftStack.Pop() + } + argument := true + if opftStack.Len() > 2 && opfdStack.Len() == 1 { + topOpt := opftStack.Pop() + if opftStack.Peek().(efp.Token).TType == efp.TokenTypeOperatorInfix { + argument = false + } + opftStack.Push(topOpt) + } + // push opfd to args + if argument && opfdStack.Len() > 0 { + argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(opfdStack.Pop().(efp.Token).TValue)) + } +} + // calcPow evaluate exponentiation arithmetic operations. func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) @@ -1080,6 +1094,16 @@ func calculate(opdStack *Stack, opt efp.Token) error { result := 0 - opdVal opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } + if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { + if opdStack.Len() < 2 { + return ErrInvalidFormula + } + rOpd := opdStack.Pop().(efp.Token) + lOpd := opdStack.Pop().(efp.Token) + if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { + return err + } + } tokenCalcFunc := map[string]func(rOpd, lOpd efp.Token, opdStack *Stack) error{ "^": calcPow, "*": calcMultiply, @@ -1093,29 +1117,16 @@ func calculate(opdStack *Stack, opt efp.Token) error { ">=": calcGe, "&": calcSplice, } - if opt.TValue == "-" && opt.TType == efp.TokenTypeOperatorInfix { - if opdStack.Len() < 2 { - return ErrInvalidFormula - } - rOpd := opdStack.Pop().(efp.Token) - lOpd := opdStack.Pop().(efp.Token) - if err := calcSubtract(rOpd, lOpd, opdStack); err != nil { - return err - } - } fn, ok := tokenCalcFunc[opt.TValue] if ok { if opdStack.Len() < 2 { - if opdStack.Len() == 1 { - rOpd := opdStack.Pop().(efp.Token) - if rOpd.TSubType == efp.TokenSubTypeError { - return errors.New(rOpd.TValue) - } - } return ErrInvalidFormula } rOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token) + if rOpd.TSubType == efp.TokenSubTypeError { + return errors.New(rOpd.TValue) + } if lOpd.TSubType == efp.TokenSubTypeError { return errors.New(lOpd.TValue) } @@ -9959,6 +9970,68 @@ func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg { return cells.List[colIdx] } +// INDIRECT function converts a text string into a cell reference. The syntax +// of the Indirect function is: +// +// INDIRECT(ref_text,[a1]) +// +func (fn *formulaFuncs) INDIRECT(argsList *list.List) formulaArg { + if argsList.Len() != 1 && argsList.Len() != 2 { + return newErrorFormulaArg(formulaErrorVALUE, "INDIRECT requires 1 or 2 arguments") + } + refText := argsList.Front().Value.(formulaArg).Value() + a1 := newBoolFormulaArg(true) + if argsList.Len() == 2 { + if a1 = argsList.Back().Value.(formulaArg).ToBool(); a1.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + R1C1ToA1 := func(ref string) (cell string, err error) { + parts := strings.Split(strings.TrimLeft(ref, "R"), "C") + if len(parts) != 2 { + return + } + row, err := strconv.Atoi(parts[0]) + if err != nil { + return + } + col, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + cell, err = CoordinatesToCellName(col, row) + return + } + refs := strings.Split(refText, ":") + fromRef, toRef := refs[0], "" + if len(refs) == 2 { + toRef = refs[1] + } + if a1.Number == 0 { + from, err := R1C1ToA1(refs[0]) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + fromRef = from + if len(refs) == 2 { + to, err := R1C1ToA1(refs[1]) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + toRef = to + } + } + if len(refs) == 1 { + value, err := fn.f.GetCellValue(fn.sheet, fromRef) + if err != nil { + return newErrorFormulaArg(formulaErrorREF, formulaErrorREF) + } + return newStringFormulaArg(value) + } + arg, _ := fn.f.parseReference(fn.sheet, fromRef+":"+toRef) + return arg +} + // LOOKUP function performs an approximate match lookup in a one-column or // one-row range, and returns the corresponding value from another one-column // or one-row range. The syntax of the function is: diff --git a/calc_test.go b/calc_test.go index e49b29ec..e620eb36 100644 --- a/calc_test.go +++ b/calc_test.go @@ -724,6 +724,7 @@ func TestCalcCellValue(t *testing.T) { "=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=1+SUM(SUM(1,2*3),4)*-4/2+5+(4+2)*3": "2", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", + "=SUM(1+ROW())": "2", // SUMIF `=SUMIF(F1:F5, "")`: "0", `=SUMIF(A1:A5, "3")`: "3", @@ -1447,6 +1448,16 @@ func TestCalcCellValue(t *testing.T) { "=SUM(INDEX(A1:B2,2,0))": "7", "=SUM(INDEX(A1:B4,0,2))": "9", "=SUM(INDEX(E1:F5,5,2))": "34440", + // INDIRECT + "=INDIRECT(\"E1\")": "Team", + "=INDIRECT(\"E\"&1)": "Team", + "=INDIRECT(\"E\"&ROW())": "Team", + "=INDIRECT(\"E\"&ROW(),TRUE)": "Team", + "=INDIRECT(\"R1C5\",FALSE)": "Team", + "=INDIRECT(\"R\"&1&\"C\"&5,FALSE)": "Team", + "=SUM(INDIRECT(\"A1:B2\"))": "12", + "=SUM(INDIRECT(\"A1:B2\",TRUE))": "12", + "=SUM(INDIRECT(\"R1C1:R2C2\",FALSE))": "12", // LOOKUP "=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb", @@ -2872,6 +2883,17 @@ func TestCalcCellValue(t *testing.T) { "=INDEX(A1:A2,0,0)": "#VALUE!", "=INDEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=INDEX(0,0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + // INDIRECT + "=INDIRECT()": "INDIRECT requires 1 or 2 arguments", + "=INDIRECT(\"E\"&1,TRUE,1)": "INDIRECT requires 1 or 2 arguments", + "=INDIRECT(\"R1048577C1\",\"\")": "#VALUE!", + "=INDIRECT(\"E1048577\")": "#REF!", + "=INDIRECT(\"R1048577C1\",FALSE)": "#REF!", + "=INDIRECT(\"R1C16385\",FALSE)": "#REF!", + "=INDIRECT(\"\",FALSE)": "#REF!", + "=INDIRECT(\"R C1\",FALSE)": "#REF!", + "=INDIRECT(\"R1C \",FALSE)": "#REF!", + "=INDIRECT(\"R1C1:R2C \",FALSE)": "#REF!", // LOOKUP "=LOOKUP()": "LOOKUP requires at least 2 arguments", "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array",