ref #65: formula function INDIRECT support and formula engine improvement

Support calculation with the none parameter formula function after infix operator notation
This commit is contained in:
xuri 2022-02-28 01:01:24 +08:00
parent 471c8f22d0
commit 42a9665aa9
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
2 changed files with 127 additions and 32 deletions

137
calc.go
View File

@ -459,6 +459,7 @@ type formulaFuncs struct {
// IMSUM // IMSUM
// IMTAN // IMTAN
// INDEX // INDEX
// INDIRECT
// INT // INT
// INTRATE // INTRATE
// IPMT // IPMT
@ -851,22 +852,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token,
if !isFunctionStopToken(token) { if !isFunctionStopToken(token) {
return nil return nil
} }
// current token is function stop prepareEvalInfixExp(opfStack, opftStack, opfdStack, argsStack)
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))
}
// call formula function to evaluate // call formula function to evaluate
arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer( arg := callFuncByName(&formulaFuncs{f: f, sheet: sheet, cell: cell}, strings.NewReplacer(
"_xlfn.", "", ".", "dot").Replace(opfStack.Peek().(efp.Token).TValue), "_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 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. // calcPow evaluate exponentiation arithmetic operations.
func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error { func calcPow(rOpd, lOpd efp.Token, opdStack *Stack) error {
lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64) lOpdVal, err := strconv.ParseFloat(lOpd.TValue, 64)
@ -1080,6 +1094,16 @@ func calculate(opdStack *Stack, opt efp.Token) error {
result := 0 - opdVal result := 0 - opdVal
opdStack.Push(efp.Token{TValue: fmt.Sprintf("%g", result), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) 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{ tokenCalcFunc := map[string]func(rOpd, lOpd efp.Token, opdStack *Stack) error{
"^": calcPow, "^": calcPow,
"*": calcMultiply, "*": calcMultiply,
@ -1093,29 +1117,16 @@ func calculate(opdStack *Stack, opt efp.Token) error {
">=": calcGe, ">=": calcGe,
"&": calcSplice, "&": 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] fn, ok := tokenCalcFunc[opt.TValue]
if ok { if ok {
if opdStack.Len() < 2 { 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 return ErrInvalidFormula
} }
rOpd := opdStack.Pop().(efp.Token) rOpd := opdStack.Pop().(efp.Token)
lOpd := opdStack.Pop().(efp.Token) lOpd := opdStack.Pop().(efp.Token)
if rOpd.TSubType == efp.TokenSubTypeError {
return errors.New(rOpd.TValue)
}
if lOpd.TSubType == efp.TokenSubTypeError { if lOpd.TSubType == efp.TokenSubTypeError {
return errors.New(lOpd.TValue) return errors.New(lOpd.TValue)
} }
@ -9959,6 +9970,68 @@ func (fn *formulaFuncs) INDEX(argsList *list.List) formulaArg {
return cells.List[colIdx] 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 // LOOKUP function performs an approximate match lookup in a one-column or
// one-row range, and returns the corresponding value from another one-column // one-row range, and returns the corresponding value from another one-column
// or one-row range. The syntax of the function is: // or one-row range. The syntax of the function is:

View File

@ -724,6 +724,7 @@ func TestCalcCellValue(t *testing.T) {
"=((3+5*2)+3)/5+(-6)/4*2+3": "3.2", "=((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/2+5+(4+2)*3": "2",
"=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664", "=1+SUM(SUM(1,2*3),4)*4/3+5+(4+2)*3": "38.666666666666664",
"=SUM(1+ROW())": "2",
// SUMIF // SUMIF
`=SUMIF(F1:F5, "")`: "0", `=SUMIF(F1:F5, "")`: "0",
`=SUMIF(A1:A5, "3")`: "3", `=SUMIF(A1:A5, "3")`: "3",
@ -1447,6 +1448,16 @@ func TestCalcCellValue(t *testing.T) {
"=SUM(INDEX(A1:B2,2,0))": "7", "=SUM(INDEX(A1:B2,2,0))": "7",
"=SUM(INDEX(A1:B4,0,2))": "9", "=SUM(INDEX(A1:B4,0,2))": "9",
"=SUM(INDEX(E1:F5,5,2))": "34440", "=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
"=LOOKUP(F8,F8:F9,F8:F9)": "32080", "=LOOKUP(F8,F8:F9,F8:F9)": "32080",
"=LOOKUP(F8,F8:F9,D8:D9)": "Feb", "=LOOKUP(F8,F8:F9,D8:D9)": "Feb",
@ -2872,6 +2883,17 @@ func TestCalcCellValue(t *testing.T) {
"=INDEX(A1:A2,0,0)": "#VALUE!", "=INDEX(A1:A2,0,0)": "#VALUE!",
"=INDEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", "=INDEX(0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=INDEX(0,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()": "LOOKUP requires at least 2 arguments", "=LOOKUP()": "LOOKUP requires at least 2 arguments",
"=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array", "=LOOKUP(D2,D1,D2)": "LOOKUP requires second argument of table array",