summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxuri <xuri.me@gmail.com>2022-02-28 01:01:24 +0800
committerxuri <xuri.me@gmail.com>2022-02-28 01:01:24 +0800
commit42a9665aa91912f570bb83052b20cc50529d7b6a (patch)
tree74cddbdb12ba1fb9da638161c6d86ef088bf1cab
parent471c8f22d0ac6cc17915eea25d171e578c06ac7d (diff)
ref #65: formula function INDIRECT support and formula engine improvement
Support calculation with the none parameter formula function after infix operator notation
-rw-r--r--calc.go137
-rw-r--r--calc_test.go22
2 files changed, 127 insertions, 32 deletions
diff --git a/calc.go b/calc.go
index d4828be..8358d33 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 e49b29e..e620eb3 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",