From 5429f131f87a6c35564a44e491e1047af79510fb Mon Sep 17 00:00:00 2001 From: xuri Date: Sun, 8 Jan 2023 00:23:53 +0800 Subject: [PATCH] This closes #1438, fix cell data type issue for formula calculation engine - Update dependencies module - Update unit tests --- calc.go | 437 ++++++++++++++++++++++------------------------- calc_test.go | 172 ++++++++++++------- cell.go | 8 +- excelize_test.go | 3 +- go.mod | 6 +- go.sum | 17 +- 6 files changed, 335 insertions(+), 308 deletions(-) diff --git a/calc.go b/calc.go index 895f78bc..b864a23e 100644 --- a/calc.go +++ b/calc.go @@ -768,28 +768,11 @@ type formulaFuncs struct { // Z.TEST // ZTEST func (f *File) CalcCellValue(sheet, cell string) (result string, err error) { - return f.calcCellValue(&calcContext{ + var token formulaArg + token, err = f.calcCellValue(&calcContext{ entry: fmt.Sprintf("%s!%s", sheet, cell), iterations: make(map[string]uint), }, sheet, cell) -} - -func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result string, err error) { - var ( - formula string - token formulaArg - ) - if formula, err = f.GetCellFormula(sheet, cell); err != nil { - return - } - ps := efp.ExcelParser() - tokens := ps.Parse(formula) - if tokens == nil { - return - } - if token, err = f.evalInfixExp(ctx, sheet, cell, tokens); err != nil { - return - } result = token.Value() if isNum, precision, decimal := isNumeric(result); isNum { if precision > 15 { @@ -803,6 +786,22 @@ func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result strin return } +// calcCellValue calculate cell value by given context, worksheet name and cell +// reference. +func (f *File) calcCellValue(ctx *calcContext, sheet, cell string) (result formulaArg, err error) { + var formula string + if formula, err = f.GetCellFormula(sheet, cell); err != nil { + return + } + ps := efp.ExcelParser() + tokens := ps.Parse(formula) + if tokens == nil { + return + } + result, err = f.evalInfixExp(ctx, sheet, cell, tokens) + return +} + // getPriority calculate arithmetic operator priority. func getPriority(token efp.Token) (pri int) { pri = tokenPriority[token.TValue] @@ -919,8 +918,8 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T if err != nil { return result, err } - if result.Type != ArgString { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), errors.New(formulaErrorVALUE) + if result.Type == ArgError { + return result, errors.New(result.Error) } opfdStack.Push(result) continue @@ -933,7 +932,7 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T } result, err := f.parseReference(ctx, sheet, token.TValue) if err != nil { - return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE), err + return newEmptyFormulaArg(), err } if result.Type == ArgUnknown { return newEmptyFormulaArg(), errors.New(formulaErrorVALUE) @@ -977,10 +976,6 @@ func (f *File) evalInfixExp(ctx *calcContext, sheet, cell string, tokens []efp.T continue } - // current token is logical - if token.TType == efp.TokenTypeOperand && token.TSubType == efp.TokenSubTypeLogical { - argsStack.Peek().(*list.List).PushBack(newStringFormulaArg(token.TValue)) - } if inArrayRow && isOperand(token) { continue } @@ -1341,16 +1336,33 @@ func isOperatorPrefixToken(token efp.Token) bool { // isOperand determine if the token is parse operand. func isOperand(token efp.Token) bool { - return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText) + return token.TType == efp.TokenTypeOperand && (token.TSubType == efp.TokenSubTypeNumber || token.TSubType == efp.TokenSubTypeText || token.TSubType == efp.TokenSubTypeLogical) } // tokenToFormulaArg create a formula argument by given token. func tokenToFormulaArg(token efp.Token) formulaArg { - if token.TSubType == efp.TokenSubTypeNumber { + switch token.TSubType { + case efp.TokenSubTypeLogical: + return newBoolFormulaArg(strings.EqualFold(token.TValue, "TRUE")) + case efp.TokenSubTypeNumber: num, _ := strconv.ParseFloat(token.TValue, 64) return newNumberFormulaArg(num) + default: + return newStringFormulaArg(token.TValue) + } +} + +// formulaArgToToken create a token by given formula argument. +func formulaArgToToken(arg formulaArg) efp.Token { + switch arg.Type { + case ArgNumber: + if arg.Boolean { + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeLogical} + } + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber} + default: + return efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeText} } - return newStringFormulaArg(token.TValue) } // parseToken parse basic arithmetic operator priority and evaluate based on @@ -1366,12 +1378,7 @@ func (f *File) parseToken(ctx *calcContext, sheet string, token efp.Token, opdSt if err != nil { return errors.New(formulaErrorNAME) } - if result.Type != ArgString { - return errors.New(formulaErrorVALUE) - } - token.TValue = result.String - token.TType = efp.TokenTypeOperand - token.TSubType = efp.TokenSubTypeText + token = formulaArgToToken(result) } if isOperatorPrefixToken(token) { if err := f.parseOperatorPrefixToken(optStack, opdStack, token); err != nil { @@ -1505,20 +1512,39 @@ func prepareValueRef(cr cellRef, valueRange []int) { } // cellResolver calc cell value by given worksheet name, cell reference and context. -func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (string, error) { - var value string +func (f *File) cellResolver(ctx *calcContext, sheet, cell string) (formulaArg, error) { + var ( + arg formulaArg + value string + err error + ) ref := fmt.Sprintf("%s!%s", sheet, cell) if formula, _ := f.GetCellFormula(sheet, cell); len(formula) != 0 { ctx.Lock() if ctx.entry != ref && ctx.iterations[ref] <= f.options.MaxCalcIterations { ctx.iterations[ref]++ ctx.Unlock() - value, _ = f.calcCellValue(ctx, sheet, cell) - return value, nil + arg, _ = f.calcCellValue(ctx, sheet, cell) + return arg, nil } ctx.Unlock() } - return f.GetCellValue(sheet, cell, Options{RawCellValue: true}) + if value, err = f.GetCellValue(sheet, cell, Options{RawCellValue: true}); err != nil { + return arg, err + } + arg = newStringFormulaArg(value) + cellType, _ := f.GetCellType(sheet, cell) + switch cellType { + case CellTypeBool: + return arg.ToBool(), err + case CellTypeNumber, CellTypeUnset: + if arg.Value() == "" { + return newEmptyFormulaArg(), err + } + return arg.ToNumber(), err + default: + return arg, err + } } // rangeResolver extract value as string from given reference and range list. @@ -1556,17 +1582,15 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) for row := valueRange[0]; row <= valueRange[1]; row++ { var matrixRow []formulaArg for col := valueRange[2]; col <= valueRange[3]; col++ { - var cell, value string + var cell string + var value formulaArg if cell, err = CoordinatesToCellName(col, row); err != nil { return } if value, err = f.cellResolver(ctx, sheet, cell); err != nil { return } - matrixRow = append(matrixRow, formulaArg{ - String: value, - Type: ArgString, - }) + matrixRow = append(matrixRow, value) } arg.Matrix = append(arg.Matrix, matrixRow) } @@ -1579,10 +1603,10 @@ func (f *File) rangeResolver(ctx *calcContext, cellRefs, cellRanges *list.List) if cell, err = CoordinatesToCellName(cr.Col, cr.Row); err != nil { return } - if arg.String, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil { + if arg, err = f.cellResolver(ctx, cr.Sheet, cell); err != nil { return } - arg.Type = ArgString + arg.cellRefs, arg.cellRanges = cellRefs, cellRanges } return } @@ -4618,10 +4642,11 @@ func newNumberMatrix(arg formulaArg, phalanx bool) (numMtx [][]float64, ele form } numMtx = append(numMtx, make([]float64, len(row))) for c, cell := range row { - if ele = cell.ToNumber(); ele.Type != ArgNumber { + if cell.Type != ArgNumber { + ele = newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) return } - numMtx[r][c] = ele.Number + numMtx[r][c] = cell.Number } } return @@ -4946,31 +4971,24 @@ func (fn *formulaFuncs) POWER(argsList *list.List) formulaArg { // // PRODUCT(number1,[number2],...) func (fn *formulaFuncs) PRODUCT(argsList *list.List) formulaArg { - val, product := 0.0, 1.0 - var err error + product := 1.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { token := arg.Value.(formulaArg) switch token.Type { case ArgString: - if token.String == "" { - continue + num := token.ToNumber() + if num.Type != ArgNumber { + return num } - if val, err = strconv.ParseFloat(token.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - product = product * val + product = product * num.Number case ArgNumber: product = product * token.Number case ArgMatrix: for _, row := range token.Matrix { - for _, value := range row { - if value.Value() == "" { - continue + for _, cell := range row { + if cell.Type == ArgNumber { + product *= cell.Number } - if val, err = strconv.ParseFloat(value.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - product *= val } } } @@ -5685,26 +5703,23 @@ func (fn *formulaFuncs) SUMIF(argsList *list.List) formulaArg { if argsList.Len() == 3 { sumRange = argsList.Back().Value.(formulaArg).Matrix } - var sum, val float64 - var err error + var sum float64 + var arg formulaArg for rowIdx, row := range rangeMtx { - for colIdx, col := range row { - var ok bool - fromVal := col.String - if col.String == "" { + for colIdx, cell := range row { + arg = cell + if arg.Type == ArgEmpty { continue } - ok, _ = formulaCriteriaEval(fromVal, criteria) - if ok { + if ok, _ := formulaCriteriaEval(arg.Value(), criteria); ok { if argsList.Len() == 3 { if len(sumRange) > rowIdx && len(sumRange[rowIdx]) > colIdx { - fromVal = sumRange[rowIdx][colIdx].String + arg = sumRange[rowIdx][colIdx] } } - if val, err = strconv.ParseFloat(fromVal, 64); err != nil { - continue + if arg.Type == ArgNumber { + sum += arg.Number } - sum += val } } } @@ -7662,14 +7677,16 @@ func (fn *formulaFuncs) COUNT(argsList *list.List) formulaArg { for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { - case ArgString, ArgNumber: - if arg.ToNumber().Type != ArgError { + case ArgString: + if num := arg.ToNumber(); num.Type == ArgNumber { count++ } + case ArgNumber: + count++ case ArgMatrix: for _, row := range arg.Matrix { - for _, value := range row { - if value.ToNumber().Type != ArgError { + for _, cell := range row { + if cell.Type == ArgNumber { count++ } } @@ -7818,17 +7835,16 @@ func (fn *formulaFuncs) DEVSQ(argsList *list.List) formulaArg { } avg, count, result := fn.AVERAGE(argsList), -1, 0.0 for arg := argsList.Front(); arg != nil; arg = arg.Next() { - for _, number := range arg.Value.(formulaArg).ToList() { - num := number.ToNumber() - if num.Type != ArgNumber { + for _, cell := range arg.Value.(formulaArg).ToList() { + if cell.Type != ArgNumber { continue } count++ if count == 0 { - result = math.Pow(num.Number-avg.Number, 2) + result = math.Pow(cell.Number-avg.Number, 2) continue } - result += math.Pow(num.Number-avg.Number, 2) + result += math.Pow(cell.Number-avg.Number, 2) } } if count == -1 { @@ -9338,12 +9354,12 @@ func (fn *formulaFuncs) MODE(argsList *list.List) formulaArg { var values []float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { cells := arg.Value.(formulaArg) - if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + if cells.Type != ArgMatrix && cells.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } for _, cell := range cells.ToList() { - if num := cell.ToNumber(); num.Type == ArgNumber { - values = append(values, num.Number) + if cell.Type == ArgNumber { + values = append(values, cell.Number) } } } @@ -9381,12 +9397,12 @@ func (fn *formulaFuncs) MODEdotMULT(argsList *list.List) formulaArg { var values []float64 for arg := argsList.Front(); arg != nil; arg = arg.Next() { cells := arg.Value.(formulaArg) - if cells.Type != ArgMatrix && cells.ToNumber().Type != ArgNumber { + if cells.Type != ArgMatrix && cells.Type != ArgNumber { return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } for _, cell := range cells.ToList() { - if num := cell.ToNumber(); num.Type == ArgNumber { - values = append(values, num.Number) + if cell.Type == ArgNumber { + values = append(values, cell.Number) } } } @@ -9700,8 +9716,8 @@ func (fn *formulaFuncs) kth(name string, argsList *list.List) formulaArg { } var data []float64 for _, arg := range array { - if numArg := arg.ToNumber(); numArg.Type == ArgNumber { - data = append(data, numArg.Number) + if arg.Type == ArgNumber { + data = append(data, arg.Number) } } if len(data) < k { @@ -9776,25 +9792,10 @@ func (fn *formulaFuncs) MAXIFS(argsList *list.List) formulaArg { // calcListMatrixMax is part of the implementation max. func calcListMatrixMax(maxa bool, max float64, arg formulaArg) float64 { - for _, row := range arg.ToList() { - switch row.Type { - case ArgString: - if !maxa && (row.Value() == "TRUE" || row.Value() == "FALSE") { - continue - } else { - num := row.ToBool() - if num.Type == ArgNumber && num.Number > max { - max = num.Number - continue - } - } - num := row.ToNumber() - if num.Type != ArgError && num.Number > max { - max = num.Number - } - case ArgNumber: - if row.Number > max { - max = row.Number + for _, cell := range arg.ToList() { + if cell.Type == ArgNumber && cell.Number > max { + if maxa && cell.Boolean || !cell.Boolean { + max = cell.Number } } } @@ -9846,33 +9847,31 @@ func (fn *formulaFuncs) MEDIAN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "MEDIAN requires at least 1 argument") } var values []float64 - var median, digits float64 - var err error + var median float64 for token := argsList.Front(); token != nil; token = token.Next() { arg := token.Value.(formulaArg) switch arg.Type { case ArgString: - num := arg.ToNumber() - if num.Type == ArgError { - return newErrorFormulaArg(formulaErrorVALUE, num.Error) + value := arg.ToNumber() + if value.Type != ArgNumber { + return value } - values = append(values, num.Number) + values = append(values, value.Number) case ArgNumber: values = append(values, arg.Number) case ArgMatrix: for _, row := range arg.Matrix { - for _, value := range row { - if value.String == "" { - continue + for _, cell := range row { + if cell.Type == ArgNumber { + values = append(values, cell.Number) } - if digits, err = strconv.ParseFloat(value.String, 64); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - values = append(values, digits) } } } } + if len(values) == 0 { + return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) + } sort.Float64s(values) if len(values)%2 == 0 { median = (values[len(values)/2-1] + values[len(values)/2]) / 2 @@ -9936,25 +9935,10 @@ func (fn *formulaFuncs) MINIFS(argsList *list.List) formulaArg { // calcListMatrixMin is part of the implementation min. func calcListMatrixMin(mina bool, min float64, arg formulaArg) float64 { - for _, row := range arg.ToList() { - switch row.Type { - case ArgString: - if !mina && (row.Value() == "TRUE" || row.Value() == "FALSE") { - continue - } else { - num := row.ToBool() - if num.Type == ArgNumber && num.Number < min { - min = num.Number - continue - } - } - num := row.ToNumber() - if num.Type != ArgError && num.Number < min { - min = num.Number - } - case ArgNumber: - if row.Number < min { - min = row.Number + for _, cell := range arg.ToList() { + if cell.Type == ArgNumber && cell.Number < min { + if mina && cell.Boolean || !cell.Boolean { + min = cell.Number } } } @@ -10016,7 +10000,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula } var sum, deltaX, deltaY, x, y, length float64 for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10027,7 +10011,7 @@ func (fn *formulaFuncs) pearsonProduct(name string, argsList *list.List) formula x /= length y /= length for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10077,9 +10061,8 @@ func (fn *formulaFuncs) PERCENTILEdotEXC(argsList *list.List) formulaArg { if arg.Type == ArgError { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10125,9 +10108,8 @@ func (fn *formulaFuncs) PERCENTILE(argsList *list.List) formulaArg { if arg.Type == ArgError { return arg } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10156,11 +10138,10 @@ func (fn *formulaFuncs) percentrank(name string, argsList *list.List) formulaArg var numbers []float64 for _, arg := range array { if arg.Type == ArgError { - return arg + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) } - num := arg.ToNumber() - if num.Type == ArgNumber { - numbers = append(numbers, num.Number) + if arg.Type == ArgNumber { + numbers = append(numbers, arg.Number) } } cnt := len(numbers) @@ -10350,9 +10331,8 @@ func (fn *formulaFuncs) rank(name string, argsList *list.List) formulaArg { } var arr []float64 for _, arg := range argsList.Front().Next().Value.(formulaArg).ToList() { - n := arg.ToNumber() - if n.Type == ArgNumber { - arr = append(arr, n.Number) + if arg.Type == ArgNumber { + arr = append(arr, arg.Number) } } sort.Float64s(arr) @@ -10422,12 +10402,11 @@ func (fn *formulaFuncs) skew(name string, argsList *list.List) formulaArg { summer += math.Pow((num.Number-mean.Number)/stdDev.Number, 3) count++ case ArgList, ArgMatrix: - for _, row := range token.ToList() { - numArg := row.ToNumber() - if numArg.Type != ArgNumber { + for _, cell := range token.ToList() { + if cell.Type != ArgNumber { continue } - summer += math.Pow((numArg.Number-mean.Number)/stdDev.Number, 3) + summer += math.Pow((cell.Number-mean.Number)/stdDev.Number, 3) count++ } } @@ -10558,7 +10537,7 @@ func (fn *formulaFuncs) STEYX(argsList *list.List) formulaArg { } var count, sumX, sumY, squareX, squareY, sigmaXY float64 for i := 0; i < len(array1); i++ { - num1, num2 := array1[i].ToNumber(), array2[i].ToNumber() + num1, num2 := array1[i], array2[i] if !(num1.Type == ArgNumber && num2.Type == ArgNumber) { continue } @@ -10804,8 +10783,7 @@ func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float6 var fVal formulaArg for i := 0; i < c1; i++ { for j := 0; j < r1; j++ { - fVal = mtx1[i][j].ToNumber() - if fVal.Type == ArgNumber { + if fVal = mtx1[i][j]; fVal.Type == ArgNumber { sum1 += fVal.Number sumSqr1 += fVal.Number * fVal.Number cnt1++ @@ -10814,8 +10792,7 @@ func tTest(bTemplin bool, mtx1, mtx2 [][]formulaArg, c1, c2, r1, r2 int) (float6 } for i := 0; i < c2; i++ { for j := 0; j < r2; j++ { - fVal = mtx2[i][j].ToNumber() - if fVal.Type == ArgNumber { + if fVal = mtx2[i][j]; fVal.Type == ArgNumber { sum2 += fVal.Number sumSqr2 += fVal.Number * fVal.Number cnt2++ @@ -10851,7 +10828,7 @@ func (fn *formulaFuncs) tTest(mtx1, mtx2 [][]formulaArg, fTails, fTyp float64) f var fVal1, fVal2 formulaArg for i := 0; i < c1; i++ { for j := 0; j < r1; j++ { - fVal1, fVal2 = mtx1[i][j].ToNumber(), mtx2[i][j].ToNumber() + fVal1, fVal2 = mtx1[i][j], mtx2[i][j] if fVal1.Type != ArgNumber || fVal2.Type != ArgNumber { continue } @@ -10895,11 +10872,11 @@ func (fn *formulaFuncs) TTEST(argsList *list.List) formulaArg { var array1, array2, tails, typeArg formulaArg array1 = argsList.Front().Value.(formulaArg) array2 = argsList.Front().Next().Value.(formulaArg) - if tails = argsList.Front().Next().Next().Value.(formulaArg).ToNumber(); tails.Type != ArgNumber { - return tails + if tails = argsList.Front().Next().Next().Value.(formulaArg); tails.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if typeArg = argsList.Back().Value.(formulaArg).ToNumber(); typeArg.Type != ArgNumber { - return typeArg + if typeArg = argsList.Back().Value.(formulaArg); typeArg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } if len(array1.Matrix) == 0 || len(array2.Matrix) == 0 { return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM) @@ -10944,11 +10921,10 @@ func (fn *formulaFuncs) TRIMMEAN(argsList *list.List) formulaArg { var arr []float64 arrArg := argsList.Front().Value.(formulaArg).ToList() for _, cell := range arrArg { - num := cell.ToNumber() - if num.Type != ArgNumber { + if cell.Type != ArgNumber { continue } - arr = append(arr, num.Number) + arr = append(arr, cell.Number) } discard := math.Floor(float64(len(arr)) * percent.Number / 2) sort.Float64s(arr) @@ -11184,16 +11160,12 @@ func (fn *formulaFuncs) ISBLANK(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "ISBLANK requires 1 argument") } token := argsList.Front().Value.(formulaArg) - result := "FALSE" switch token.Type { - case ArgUnknown: - result = "TRUE" - case ArgString: - if token.String == "" { - result = "TRUE" - } + case ArgUnknown, ArgEmpty: + return newBoolFormulaArg(true) + default: + return newBoolFormulaArg(false) } - return newStringFormulaArg(result) } // ISERR function tests if an initial supplied expression (or value) returns @@ -11256,21 +11228,22 @@ func (fn *formulaFuncs) ISEVEN(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISEVEN requires 1 argument") } - var ( - token = argsList.Front().Value.(formulaArg) - result = "FALSE" - numeric int - err error - ) - if token.Type == ArgString { - if numeric, err = strconv.Atoi(token.String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) + token := argsList.Front().Value.(formulaArg) + switch token.Type { + case ArgEmpty: + return newBoolFormulaArg(true) + case ArgNumber, ArgString: + num := token.ToNumber() + if num.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - if numeric == numeric/2*2 { - return newStringFormulaArg("TRUE") + if num.Number == 1 { + return newBoolFormulaArg(false) } + return newBoolFormulaArg(num.Number == num.Number/2*2) + default: + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - return newStringFormulaArg(result) } // ISFORMULA function tests if a specified cell contains a formula, and if so, @@ -11335,12 +11308,10 @@ func (fn *formulaFuncs) ISNONTEXT(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNONTEXT requires 1 argument") } - token := argsList.Front().Value.(formulaArg) - result := "TRUE" - if token.Type == ArgString && token.String != "" { - result = "FALSE" + if argsList.Front().Value.(formulaArg).Type == ArgString { + return newBoolFormulaArg(false) } - return newStringFormulaArg(result) + return newBoolFormulaArg(true) } // ISNUMBER function tests if a supplied value is a number. If so, @@ -11352,13 +11323,10 @@ func (fn *formulaFuncs) ISNUMBER(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISNUMBER requires 1 argument") } - token, result := argsList.Front().Value.(formulaArg), false - if token.Type == ArgString && token.String != "" { - if _, err := strconv.Atoi(token.String); err == nil { - result = true - } + if argsList.Front().Value.(formulaArg).Type == ArgNumber { + return newBoolFormulaArg(true) } - return newBoolFormulaArg(result) + return newBoolFormulaArg(false) } // ISODD function tests if a supplied number (or numeric expression) evaluates @@ -11370,21 +11338,14 @@ func (fn *formulaFuncs) ISODD(argsList *list.List) formulaArg { if argsList.Len() != 1 { return newErrorFormulaArg(formulaErrorVALUE, "ISODD requires 1 argument") } - var ( - token = argsList.Front().Value.(formulaArg) - result = "FALSE" - numeric int - err error - ) - if token.Type == ArgString { - if numeric, err = strconv.Atoi(token.String); err != nil { - return newErrorFormulaArg(formulaErrorVALUE, err.Error()) - } - if numeric != numeric/2*2 { - return newStringFormulaArg("TRUE") - } + arg := argsList.Front().Value.(formulaArg).ToNumber() + if arg.Type != ArgNumber { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } - return newStringFormulaArg(result) + if int(arg.Number) != int(arg.Number)/2*2 { + return newBoolFormulaArg(true) + } + return newBoolFormulaArg(false) } // ISREF function tests if a supplied value is a reference. If so, the @@ -11524,13 +11485,12 @@ func (fn *formulaFuncs) TYPE(argsList *list.List) formulaArg { return newNumberFormulaArg(16) case ArgMatrix: return newNumberFormulaArg(64) - default: - if arg := token.ToNumber(); arg.Type != ArgError || len(token.Value()) == 0 { - return newNumberFormulaArg(1) - } - if arg := token.ToBool(); arg.Type != ArgError { + case ArgNumber, ArgEmpty: + if token.Boolean { return newNumberFormulaArg(4) } + return newNumberFormulaArg(1) + default: return newNumberFormulaArg(2) } } @@ -13734,9 +13694,9 @@ func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg { return newErrorFormulaArg(formulaErrorVALUE, "TEXTJOIN accepts at most 252 arguments") } delimiter := argsList.Front().Value.(formulaArg) - ignoreEmpty := argsList.Front().Next().Value.(formulaArg).ToBool() - if ignoreEmpty.Type != ArgNumber { - return ignoreEmpty + ignoreEmpty := argsList.Front().Next().Value.(formulaArg) + if ignoreEmpty.Type != ArgNumber || !ignoreEmpty.Boolean { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) } args, ok := textJoin(argsList.Front().Next().Next(), []string{}, ignoreEmpty.Number != 0) if ok.Type != ArgNumber { @@ -13755,7 +13715,7 @@ func textJoin(arg *list.Element, arr []string, ignoreEmpty bool) ([]string, form switch arg.Value.(formulaArg).Type { case ArgError: return arr, arg.Value.(formulaArg) - case ArgString: + case ArgString, ArgEmpty: val := arg.Value.(formulaArg).Value() if val != "" || !ignoreEmpty { arr = append(arr, val) @@ -14040,7 +14000,7 @@ func matchPattern(pattern, name string) (matched bool) { // match, and make compare result as formula criteria condition type. func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if lhs.Type != rhs.Type { - return criteriaErr + return criteriaNe } switch lhs.Type { case ArgNumber: @@ -14068,8 +14028,9 @@ func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte return compareFormulaArgList(lhs, rhs, matchMode, caseSensitive) case ArgMatrix: return compareFormulaArgMatrix(lhs, rhs, matchMode, caseSensitive) + default: + return criteriaErr } - return criteriaErr } // compareFormulaArgList compares the left-hand sides and the right-hand sides @@ -14247,8 +14208,8 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires second argument of table array", name)) return } - arg := argsList.Front().Next().Next().Value.(formulaArg).ToNumber() - if arg.Type != ArgNumber { + arg := argsList.Front().Next().Next().Value.(formulaArg) + if arg.Type != ArgNumber || arg.Boolean { errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit)) return } @@ -14256,7 +14217,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, if argsList.Len() == 4 { rangeLookup := argsList.Back().Value.(formulaArg).ToBool() if rangeLookup.Type == ArgError { - errArg = newErrorFormulaArg(formulaErrorVALUE, rangeLookup.Error) + errArg = rangeLookup return } if rangeLookup.Number == 0 { @@ -14442,6 +14403,8 @@ start: } } else if lookupValue.Type == ArgMatrix { lhs = lookupArray + } else if lookupArray.Type == ArgString { + lhs = newStringFormulaArg(cell.Value()) } if compareFormulaArg(lhs, lookupValue, matchMode, false) == criteriaEq { matchIdx = i @@ -14512,6 +14475,8 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear } } else if lookupValue.Type == ArgMatrix && vertical { lhs = lookupArray + } else if lookupValue.Type == ArgString { + lhs = newStringFormulaArg(cell.Value()) } result := compareFormulaArg(lhs, lookupValue, matchMode, false) if result == criteriaEq { @@ -14524,7 +14489,7 @@ func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, sear high = mid - 1 } else if result == criteriaL { matchIdx = mid - if lhs.Value() != "" { + if cell.Type != ArgEmpty { lastMatchIdx = matchIdx } low = mid + 1 diff --git a/calc_test.go b/calc_test.go index 9ebfef81..5e87763e 100644 --- a/calc_test.go +++ b/calc_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/xuri/efp" ) func prepareCalcData(cellData [][]interface{}) *File { @@ -572,6 +573,7 @@ func TestCalcCellValue(t *testing.T) { "=FLOOR(-26.75,-0.1)": "-26.7", "=FLOOR(-26.75,-1)": "-26", "=FLOOR(-26.75,-5)": "-25", + "=FLOOR(-2.05,2)": "-4", "=FLOOR(FLOOR(26.75,1),1)": "26", // _xlfn.FLOOR.MATH "=_xlfn.FLOOR.MATH(58.55)": "58", @@ -706,8 +708,8 @@ func TestCalcCellValue(t *testing.T) { "=POWER(4,POWER(1,1))": "4", // PRODUCT "=PRODUCT(3,6)": "18", - `=PRODUCT("",3,6)`: "18", - `=PRODUCT(PRODUCT(1),3,6)`: "18", + "=PRODUCT(\"3\",\"6\")": "18", + "=PRODUCT(PRODUCT(1),3,6)": "18", "=PRODUCT(C1:C2)": "1", // QUOTIENT "=QUOTIENT(5,2)": "2", @@ -836,7 +838,8 @@ func TestCalcCellValue(t *testing.T) { "=SUBTOTAL(111,A1:A6,A1:A6)": "1.25", // SUM "=SUM(1,2)": "3", - `=SUM("",1,2)`: "3", + "=SUM(\"1\",\"2\")": "3", + "=SUM(\"\",1,2)": "3", "=SUM(1,2+3)": "6", "=SUM(SUM(1,2),2)": "5", "=(-2-SUM(-4+7))*5": "-25", @@ -874,11 +877,12 @@ func TestCalcCellValue(t *testing.T) { "=SUMPRODUCT(A1:B3)": "15", "=SUMPRODUCT(A1:A3,B1:B3,B2:B4)": "20", // SUMSQ - "=SUMSQ(A1:A4)": "14", - "=SUMSQ(A1,B1,A2,B2,6)": "82", - `=SUMSQ("",A1,B1,A2,B2,6)`: "82", - `=SUMSQ(1,SUMSQ(1))`: "2", - "=SUMSQ(MUNIT(3))": "3", + "=SUMSQ(A1:A4)": "14", + "=SUMSQ(A1,B1,A2,B2,6)": "82", + "=SUMSQ(\"\",A1,B1,A2,B2,6)": "82", + "=SUMSQ(1,SUMSQ(1))": "2", + "=SUMSQ(\"1\",SUMSQ(1))": "2", + "=SUMSQ(MUNIT(3))": "3", // SUMX2MY2 "=SUMX2MY2(A1:A4,B1:B4)": "-36", // SUMX2PY2 @@ -914,6 +918,7 @@ func TestCalcCellValue(t *testing.T) { // AVERAGEA "=AVERAGEA(INT(1))": "1", "=AVERAGEA(A1)": "1", + "=AVERAGEA(\"1\")": "1", "=AVERAGEA(A1:A2)": "1.5", "=AVERAGEA(D2:F9)": "12671.375", // BETA.DIST @@ -1013,6 +1018,7 @@ func TestCalcCellValue(t *testing.T) { "=COUNTA()": "0", "=COUNTA(A1:A5,B2:B5,\"text\",1,INT(2))": "8", "=COUNTA(COUNTA(1),MUNIT(1))": "2", + "=COUNTA(D1:D2)": "2", // COUNTBLANK "=COUNTBLANK(MUNIT(1))": "0", "=COUNTBLANK(1)": "0", @@ -1074,10 +1080,11 @@ func TestCalcCellValue(t *testing.T) { "=GAMMALN.PRECISE(0.4)": "0.796677817701784", "=GAMMALN.PRECISE(4.5)": "2.45373657084244", // GAUSS - "=GAUSS(-5)": "-0.499999713348428", - "=GAUSS(0)": "0", - "=GAUSS(0.1)": "0.039827837277029", - "=GAUSS(2.5)": "0.493790334674224", + "=GAUSS(-5)": "-0.499999713348428", + "=GAUSS(0)": "0", + "=GAUSS(\"0\")": "0", + "=GAUSS(0.1)": "0.039827837277029", + "=GAUSS(2.5)": "0.493790334674224", // GEOMEAN "=GEOMEAN(2.5,3,0.5,1,3)": "1.6226711115996", // HARMEAN @@ -1373,6 +1380,7 @@ func TestCalcCellValue(t *testing.T) { // ISEVEN "=ISEVEN(A1)": "FALSE", "=ISEVEN(A2)": "TRUE", + "=ISEVEN(G1)": "TRUE", // ISFORMULA "=ISFORMULA(A1)": "FALSE", "=ISFORMULA(\"A\")": "FALSE", @@ -1388,7 +1396,7 @@ func TestCalcCellValue(t *testing.T) { "=ISNA(A1)": "FALSE", "=ISNA(NA())": "TRUE", // ISNONTEXT - "=ISNONTEXT(A1)": "FALSE", + "=ISNONTEXT(A1)": "TRUE", "=ISNONTEXT(A5)": "TRUE", `=ISNONTEXT("Excelize")`: "FALSE", "=ISNONTEXT(NA())": "TRUE", @@ -1421,7 +1429,7 @@ func TestCalcCellValue(t *testing.T) { // TYPE "=TYPE(2)": "1", "=TYPE(10/2)": "1", - "=TYPE(C1)": "1", + "=TYPE(C2)": "1", "=TYPE(\"text\")": "2", "=TYPE(TRUE)": "4", "=TYPE(NA())": "16", @@ -1446,6 +1454,7 @@ func TestCalcCellValue(t *testing.T) { "=IFERROR(1/2,0)": "0.5", "=IFERROR(ISERROR(),0)": "0", "=IFERROR(1/0,0)": "0", + "=IFERROR(G1,2)": "0", "=IFERROR(B2/MROUND(A2,1),0)": "2.5", // IFNA "=IFNA(1,\"not found\")": "1", @@ -1787,16 +1796,17 @@ func TestCalcCellValue(t *testing.T) { "=VALUE(\"01/02/2006 15:04:05\")": "38719.6278356481", // Conditional Functions // IF - "=IF(1=1)": "TRUE", - "=IF(1<>1)": "FALSE", - "=IF(5<0, \"negative\", \"positive\")": "positive", - "=IF(-2<0, \"negative\", \"positive\")": "negative", - `=IF(1=1, "equal", "notequal")`: "equal", - `=IF(1<>1, "equal", "notequal")`: "notequal", - `=IF("A"="A", "equal", "notequal")`: "equal", - `=IF("A"<>"A", "equal", "notequal")`: "notequal", - `=IF(FALSE,0,ROUND(4/2,0))`: "2", - `=IF(TRUE,ROUND(4/2,0),0)`: "2", + "=IF(1=1)": "TRUE", + "=IF(1<>1)": "FALSE", + "=IF(5<0, \"negative\", \"positive\")": "positive", + "=IF(-2<0, \"negative\", \"positive\")": "negative", + "=IF(1=1, \"equal\", \"notequal\")": "equal", + "=IF(1<>1, \"equal\", \"notequal\")": "notequal", + "=IF(\"A\"=\"A\", \"equal\", \"notequal\")": "equal", + "=IF(\"A\"<>\"A\", \"equal\", \"notequal\")": "notequal", + "=IF(FALSE,0,ROUND(4/2,0))": "2", + "=IF(TRUE,ROUND(4/2,0),0)": "2", + "=IF(A4>0.4,\"TRUE\",\"FALSE\")": "FALSE", // Excel Lookup and Reference Functions // ADDRESS "=ADDRESS(1,1,1,TRUE)": "$A$1", @@ -1855,6 +1865,7 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(INT(F2),F3:F9,1,TRUE)": "32080", "=VLOOKUP(MUNIT(3),MUNIT(3),1)": "0", "=VLOOKUP(A1,A3:B5,1)": "0", + "=VLOOKUP(A1:A2,A1:A1,1)": "1", "=VLOOKUP(MUNIT(1),MUNIT(1),1,FALSE)": "1", // INDEX "=INDEX(0,0,0)": "0", @@ -2556,13 +2567,13 @@ func TestCalcCellValue(t *testing.T) { "=MDETERM()": "MDETERM requires 1 argument", // MINVERSE "=MINVERSE()": "MINVERSE requires 1 argument", - "=MINVERSE(B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MINVERSE(B3:C4)": "#VALUE!", "=MINVERSE(A1:C2)": "#VALUE!", "=MINVERSE(A4:A4)": "#NUM!", // MMULT "=MMULT()": "MMULT requires 2 argument", - "=MMULT(A1:B2,B3:C4)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MMULT(B3:C4,A1:B2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=MMULT(A1:B2,B3:C4)": "#VALUE!", + "=MMULT(B3:C4,A1:B2)": "#VALUE!", "=MMULT(A1:A2,B1:B2)": "#VALUE!", // MOD "=MOD()": "MOD requires 2 numeric arguments", @@ -2593,7 +2604,8 @@ func TestCalcCellValue(t *testing.T) { "=POWER(0,-1)": "#DIV/0!", "=POWER(1)": "POWER requires 2 numeric arguments", // PRODUCT - `=PRODUCT("X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=PRODUCT(\"X\")": "strconv.ParseFloat: parsing \"X\": invalid syntax", + "=PRODUCT(\"\",3,6)": "strconv.ParseFloat: parsing \"\": invalid syntax", // QUOTIENT `=QUOTIENT("X",1)`: "strconv.ParseFloat: parsing \"X\": invalid syntax", `=QUOTIENT(1,"X")`: "strconv.ParseFloat: parsing \"X\": invalid syntax", @@ -2697,6 +2709,7 @@ func TestCalcCellValue(t *testing.T) { "=SUMPRODUCT(A1,D1)": "#VALUE!", "=SUMPRODUCT(A1:A3,D1:D3)": "#VALUE!", "=SUMPRODUCT(A1:A2,B1:B3)": "#VALUE!", + "=SUMPRODUCT(\"\")": "#VALUE!", "=SUMPRODUCT(A1,NA())": "#N/A", // SUMX2MY2 "=SUMX2MY2()": "SUMX2MY2 requires 2 arguments", @@ -2922,6 +2935,7 @@ func TestCalcCellValue(t *testing.T) { // FISHER "=FISHER()": "FISHER requires 1 numeric argument", "=FISHER(2)": "#N/A", + "=FISHER(\"2\")": "#N/A", "=FISHER(INT(-2)))": "#N/A", "=FISHER(F1)": "FISHER requires 1 numeric argument", // FISHERINV @@ -2984,7 +2998,8 @@ func TestCalcCellValue(t *testing.T) { // GEOMEAN "=GEOMEAN()": "GEOMEAN requires at least 1 numeric argument", "=GEOMEAN(0)": "#NUM!", - "=GEOMEAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=GEOMEAN(D1:D2)": "#NUM!", + "=GEOMEAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", // HARMEAN "=HARMEAN()": "HARMEAN requires at least 1 argument", "=HARMEAN(-1)": "#N/A", @@ -3184,7 +3199,7 @@ func TestCalcCellValue(t *testing.T) { // MEDIAN "=MEDIAN()": "MEDIAN requires at least 1 argument", "=MEDIAN(\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=MEDIAN(D1:D2)": "strconv.ParseFloat: parsing \"Month\": invalid syntax", + "=MEDIAN(D1:D2)": "#NUM!", // MIN "=MIN()": "MIN requires at least 1 argument", "=MIN(NA())": "#N/A", @@ -3407,8 +3422,9 @@ func TestCalcCellValue(t *testing.T) { // ISERROR "=ISERROR()": "ISERROR requires 1 argument", // ISEVEN - "=ISEVEN()": "ISEVEN requires 1 argument", - `=ISEVEN("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + "=ISEVEN()": "ISEVEN requires 1 argument", + "=ISEVEN(\"text\")": "#VALUE!", + "=ISEVEN(A1:A2)": "#VALUE!", // ISFORMULA "=ISFORMULA()": "ISFORMULA requires 1 argument", // ISLOGICAL @@ -3420,8 +3436,8 @@ func TestCalcCellValue(t *testing.T) { // ISNUMBER "=ISNUMBER()": "ISNUMBER requires 1 argument", // ISODD - "=ISODD()": "ISODD requires 1 argument", - `=ISODD("text")`: "strconv.Atoi: parsing \"text\": invalid syntax", + "=ISODD()": "ISODD requires 1 argument", + "=ISODD(\"text\")": "#VALUE!", // ISREF "=ISREF()": "ISREF requires 1 argument", // ISTEXT @@ -3717,7 +3733,7 @@ func TestCalcCellValue(t *testing.T) { "=SUBSTITUTE(\"\",\"\",\"\",0)": "instance_num should be > 0", // TEXTJOIN "=TEXTJOIN()": "TEXTJOIN requires at least 3 arguments", - "=TEXTJOIN(\"\",\"\",1)": "strconv.ParseBool: parsing \"\": invalid syntax", + "=TEXTJOIN(\"\",\"\",1)": "#VALUE!", "=TEXTJOIN(\"\",TRUE,NA())": "#N/A", "=TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": "TEXTJOIN accepts at most 252 arguments", "=TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": "TEXTJOIN function exceeds 32767 characters", @@ -3804,7 +3820,6 @@ func TestCalcCellValue(t *testing.T) { "=VLOOKUP(D2,D1,1,FALSE)": "VLOOKUP requires second argument of table array", "=VLOOKUP(D2,D:D,FALSE,FALSE)": "VLOOKUP requires numeric col argument", "=VLOOKUP(D2,D:D,1,FALSE,FALSE)": "VLOOKUP requires at most 4 arguments", - "=VLOOKUP(A1:A2,A1:A1,1)": "VLOOKUP no result found", "=VLOOKUP(D2,D10:D10,1,FALSE)": "VLOOKUP no result found", "=VLOOKUP(D2,D:D,2,FALSE)": "VLOOKUP has invalid column index", "=VLOOKUP(D2,C:C,1,FALSE)": "VLOOKUP no result found", @@ -4455,7 +4470,7 @@ func TestCalcISBLANK(t *testing.T) { }) fn := formulaFuncs{} result := fn.ISBLANK(argsList) - assert.Equal(t, result.String, "TRUE") + assert.Equal(t, "TRUE", result.Value()) assert.Empty(t, result.Error) } @@ -4520,6 +4535,7 @@ func TestCalcMatchPattern(t *testing.T) { assert.True(t, matchPattern("", "")) assert.True(t, matchPattern("file/*", "file/abc/bcd/def")) assert.True(t, matchPattern("*", "")) + assert.False(t, matchPattern("?", "")) assert.False(t, matchPattern("file/?", "file/abc/bcd/def")) } @@ -4574,15 +4590,14 @@ func TestCalcVLOOKUP(t *testing.T) { } func TestCalcBoolean(t *testing.T) { - cellData := [][]interface{}{ - {0.5, "TRUE", -0.5, "FALSE"}, - } + cellData := [][]interface{}{{0.5, "TRUE", -0.5, "FALSE", true}} f := prepareCalcData(cellData) formulaList := map[string]string{ "=AVERAGEA(A1:C1)": "0.333333333333333", "=MAX(0.5,B1)": "0.5", "=MAX(A1:B1)": "0.5", - "=MAXA(A1:B1)": "1", + "=MAXA(A1:B1)": "0.5", + "=MAXA(A1:E1)": "1", "=MAXA(0.5,B1)": "1", "=MIN(-0.5,D1)": "-0.5", "=MIN(C1:D1)": "-0.5", @@ -4600,6 +4615,23 @@ func TestCalcBoolean(t *testing.T) { } } +func TestCalcMAXMIN(t *testing.T) { + cellData := [][]interface{}{{"1"}, {"2"}, {true}} + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=MAX(A1:A3)": "0", + "=MAXA(A1:A3)": "1", + "=MIN(A1:A3)": "0", + "=MINA(A1:A3)": "1", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "B1", formula)) + result, err := f.CalcCellValue("Sheet1", "B1") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } +} + func TestCalcAVERAGEIF(t *testing.T) { f := prepareCalcData([][]interface{}{ {"Monday", 500}, @@ -4822,28 +4854,29 @@ func TestCalcGROWTHandTREND(t *testing.T) { calcError := map[string]string{ "=GROWTH()": "GROWTH requires at least 1 argument", "=GROWTH(B2:B5,A2:A5,A8:A10,TRUE,0)": "GROWTH allows at most 4 arguments", - "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=GROWTH(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", + "=GROWTH(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", + "=GROWTH(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", "=GROWTH(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=GROWTH(A2:B3,A4:B4)": "#REF!", "=GROWTH(A4:B4,A2:A2)": "#REF!", "=GROWTH(A2:A2,A4:A5)": "#REF!", - "=GROWTH(C1:C1,A2:A3)": "#NUM!", + "=GROWTH(C1:C1,A2:A3)": "#VALUE!", "=GROWTH(D1:D1,A2:A3)": "#NUM!", - "=GROWTH(A2:A3,C1:C1)": "#NUM!", + "=GROWTH(A2:A3,C1:C1)": "#VALUE!", "=TREND()": "TREND requires at least 1 argument", "=TREND(B2:B5,A2:A5,A8:A10,TRUE,0)": "TREND allows at most 4 arguments", - "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", - "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "strconv.ParseFloat: parsing \"known_x's\": invalid syntax", + "=TREND(A1:B1,A2:A5,A8:A10,TRUE)": "#VALUE!", + "=TREND(B2:B5,A1:B1,A8:A10,TRUE)": "#VALUE!", + "=TREND(B2:B5,A2:A5,A1:B1,TRUE)": "#VALUE!", "=TREND(B2:B5,A2:A5,A8:A10,\"\")": "strconv.ParseBool: parsing \"\": invalid syntax", "=TREND(A2:B3,A4:B4)": "#REF!", "=TREND(A4:B4,A2:A2)": "#REF!", "=TREND(A2:A2,A4:A5)": "#REF!", - "=TREND(C1:C1,A2:A3)": "#NUM!", + "=TREND(C1:C1,A2:A3)": "#VALUE!", "=TREND(D1:D1,A2:A3)": "#REF!", - "=TREND(A2:A3,C1:C1)": "#NUM!", + "=TREND(A2:A3,C1:C1)": "#VALUE!", + "=TREND(C1:C1,C1:C1)": "#VALUE!", } for formula, expected := range calcError { assert.NoError(t, f.SetCellFormula("Sheet1", "C1", formula)) @@ -5586,8 +5619,8 @@ func TestCalcTTEST(t *testing.T) { "=TTEST()": "TTEST requires 4 arguments", "=TTEST(\"\",B1:B12,1,1)": "#NUM!", "=TTEST(A1:A12,\"\",1,1)": "#NUM!", - "=TTEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=TTEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=TTEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", + "=TTEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", "=TTEST(A1:A12,B1:B12,0,1)": "#NUM!", "=TTEST(A1:A12,B1:B12,1,0)": "#NUM!", "=TTEST(A1:A2,B1:B1,1,1)": "#N/A", @@ -5598,8 +5631,8 @@ func TestCalcTTEST(t *testing.T) { "=T.TEST()": "T.TEST requires 4 arguments", "=T.TEST(\"\",B1:B12,1,1)": "#NUM!", "=T.TEST(A1:A12,\"\",1,1)": "#NUM!", - "=T.TEST(A1:A12,B1:B12,\"\",1)": "strconv.ParseFloat: parsing \"\": invalid syntax", - "=T.TEST(A1:A12,B1:B12,1,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=T.TEST(A1:A12,B1:B12,\"\",1)": "#VALUE!", + "=T.TEST(A1:A12,B1:B12,1,\"\")": "#VALUE!", "=T.TEST(A1:A12,B1:B12,0,1)": "#NUM!", "=T.TEST(A1:A12,B1:B12,1,0)": "#NUM!", "=T.TEST(A1:A2,B1:B1,1,1)": "#N/A", @@ -5618,8 +5651,8 @@ func TestCalcTTEST(t *testing.T) { func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { cellData := [][]interface{}{ - {"05/01/2019", 43586}, - {"09/13/2019", 43721}, + {"05/01/2019", 43586, "text1"}, + {"09/13/2019", 43721, "text2"}, {"10/01/2019", 43739}, {"12/25/2019", 43824}, {"01/01/2020", 43831}, @@ -5651,6 +5684,7 @@ func TestCalcNETWORKDAYSandWORKDAY(t *testing.T) { "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",17)": "219", "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,A1:A12)": "178", "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,B1:B12)": "178", + "=NETWORKDAYS.INTL(\"01/01/2020\",\"09/12/2020\",1,C1:C2)": "183", "=WORKDAY(\"12/01/2015\",25)": "42374", "=WORKDAY(\"01/01/2020\",123,B1:B12)": "44006", "=WORKDAY.INTL(\"12/01/2015\",0)": "42339", @@ -5813,3 +5847,27 @@ func TestNestedFunctionsWithOperators(t *testing.T) { assert.Equal(t, expected, result, formula) } } + +func TestFormulaArgToToken(t *testing.T) { + assert.Equal(t, + efp.Token{ + TType: efp.TokenTypeOperand, + TSubType: efp.TokenSubTypeLogical, + TValue: "TRUE", + }, + formulaArgToToken(newBoolFormulaArg(true)), + ) +} + +func TestPrepareTrendGrowth(t *testing.T) { + assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxX([][]float64{{0, 0}, {0, 0}})) + assert.Equal(t, [][]float64(nil), prepareTrendGrowthMtxY(false, [][]float64{{0, 0}, {0, 0}})) + info, err := prepareTrendGrowth(false, [][]float64{{0, 0}, {0, 0}}, [][]float64{{0, 0}, {0, 0}}) + assert.Nil(t, info) + assert.Equal(t, newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM), err) +} + +func TestCalcColRowQRDecomposition(t *testing.T) { + assert.False(t, calcRowQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) + assert.False(t, calcColQRDecomposition([][]float64{{0, 0}, {0, 0}}, []float64{0, 0}, 1, 0)) +} diff --git a/cell.go b/cell.go index 992a7412..caae7747 100644 --- a/cell.go +++ b/cell.go @@ -519,8 +519,12 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { // string. func (c *xlsxC) setCellDefault(value string) { if ok, _, _ := isNumeric(value); !ok { - c.setInlineStr(value) - c.IS.T.Val = value + if value != "" { + c.setInlineStr(value) + c.IS.T.Val = value + return + } + c.T, c.V, c.IS = value, value, nil return } c.V = value diff --git a/excelize_test.go b/excelize_test.go index a1857fe4..63d96f4f 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -253,7 +253,8 @@ func TestOpenReader(t *testing.T) { for _, defaultXMLPath := range []string{ defaultXMLPathCalcChain, defaultXMLPathStyles, - defaultXMLPathWorkbookRels} { + defaultXMLPathWorkbookRels, + } { _, err = OpenReader(preset(defaultXMLPath)) assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") } diff --git a/go.mod b/go.mod index aaa5a824..b6c63e8b 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/stretchr/testify v1.8.0 github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 - golang.org/x/crypto v0.4.0 + golang.org/x/crypto v0.5.0 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 - golang.org/x/net v0.4.0 - golang.org/x/text v0.5.0 + golang.org/x/net v0.5.0 + golang.org/x/text v0.6.0 ) require github.com/richardlehane/msoleps v1.0.3 // indirect diff --git a/go.sum b/go.sum index 65f6bfd6..7e2848d9 100644 --- a/go.sum +++ b/go.sum @@ -22,17 +22,16 @@ github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY= golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -40,15 +39,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=