diff --git a/calc.go b/calc.go index 0ce5aec..682435a 100644 --- a/calc.go +++ b/calc.go @@ -57,6 +57,17 @@ const ( criteriaG criteriaErr criteriaRegexp + + matchModeExact = 0 + matchModeMinGreater = 1 + matchModeMaxLess = -1 + matchModeWildcard = 2 + + searchModeLinear = 1 + searchModeReverseLinear = -1 + searchModeAscBinary = 2 + searchModeDescBinary = -2 + maxFinancialIterations = 128 financialPercision = 1.0e-08 // Date and time format regular expressions @@ -860,7 +871,7 @@ func (f *File) evalInfixExpFunc(sheet, cell string, token, nextToken efp.Token, opftStack.Pop() // remove current function separator opfStack.Pop() if opfStack.Len() > 0 { // still in function stack - if nextToken.TType == efp.TokenTypeOperatorInfix || opftStack.Len() > 1 { + if nextToken.TType == efp.TokenTypeOperatorInfix || (opftStack.Len() > 1 && opfdStack.Len() > 0) { // mathematics calculate in formula function opfdStack.Push(efp.Token{TValue: arg.Value(), TType: efp.TokenTypeOperand, TSubType: efp.TokenSubTypeNumber}) } else { @@ -5227,7 +5238,7 @@ func (fn *formulaFuncs) COUNTIF(argsList *list.List) formulaArg { return newNumberFormulaArg(count) } -// formulaIfsMatch function returns cells reference array which match criterias. +// formulaIfsMatch function returns cells reference array which match criteria. func formulaIfsMatch(args []formulaArg) (cellRefs []cellRef) { for i := 0; i < len(args)-1; i += 2 { match := []cellRef{} @@ -9013,7 +9024,7 @@ func matchPattern(pattern, name string) (matched bool) { // compareFormulaArg compares the left-hand sides and the right-hand sides // formula arguments by given conditions such as case sensitive, if exact // match, and make compare result as formula criteria condition type. -func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArg(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if lhs.Type != rhs.Type { return criteriaErr } @@ -9031,35 +9042,26 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte if !caseSensitive { ls, rs = strings.ToLower(ls), strings.ToLower(rs) } - if exactMatch { - match := matchPattern(rs, ls) - if match { + if matchMode.Number == matchModeWildcard { + if matchPattern(rs, ls) { return criteriaEq } - return criteriaG - } - switch strings.Compare(ls, rs) { - case 1: - return criteriaG - case -1: - return criteriaL - case 0: - return criteriaEq + } - return criteriaErr + return map[int]byte{1: criteriaG, -1: criteriaL, 0: criteriaEq}[strings.Compare(ls, rs)] case ArgEmpty: return criteriaEq case ArgList: - return compareFormulaArgList(lhs, rhs, caseSensitive, exactMatch) + return compareFormulaArgList(lhs, rhs, matchMode, caseSensitive) case ArgMatrix: - return compareFormulaArgMatrix(lhs, rhs, caseSensitive, exactMatch) + return compareFormulaArgMatrix(lhs, rhs, matchMode, caseSensitive) } return criteriaErr } // compareFormulaArgList compares the left-hand sides and the right-hand sides // list type formula arguments. -func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArgList(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if len(lhs.List) < len(rhs.List) { return criteriaL } @@ -9067,7 +9069,7 @@ func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) return criteriaG } for arg := range lhs.List { - criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], caseSensitive, exactMatch) + criteria := compareFormulaArg(lhs.List[arg], rhs.List[arg], matchMode, caseSensitive) if criteria != criteriaEq { return criteria } @@ -9077,7 +9079,7 @@ func compareFormulaArgList(lhs, rhs formulaArg, caseSensitive, exactMatch bool) // compareFormulaArgMatrix compares the left-hand sides and the right-hand sides // matrix type formula arguments. -func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte { +func compareFormulaArgMatrix(lhs, rhs, matchMode formulaArg, caseSensitive bool) byte { if len(lhs.Matrix) < len(rhs.Matrix) { return criteriaL } @@ -9094,7 +9096,7 @@ func compareFormulaArgMatrix(lhs, rhs formulaArg, caseSensitive, exactMatch bool return criteriaG } for arg := range left { - criteria := compareFormulaArg(left[arg], right[arg], caseSensitive, exactMatch) + criteria := compareFormulaArg(left[arg], right[arg], matchMode, caseSensitive) if criteria != criteriaEq { return criteria } @@ -9190,7 +9192,7 @@ func (fn *formulaFuncs) COLUMNS(argsList *list.List) formulaArg { // checkHVLookupArgs checking arguments, prepare extract mode, lookup value, // and data for the formula functions HLOOKUP and VLOOKUP. -func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, wasExact, exactMatch bool, lookupValue, tableArray, errArg formulaArg) { +func checkHVLookupArgs(name string, argsList *list.List) (idx int, lookupValue, tableArray, matchMode, errArg formulaArg) { unit := map[string]string{ "HLOOKUP": "row", "VLOOKUP": "col", @@ -9214,7 +9216,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was errArg = newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("%s requires numeric %s argument", name, unit)) return } - idx, matchIdx = int(arg.Number)-1, -1 + idx, matchMode = int(arg.Number)-1, newNumberFormulaArg(matchModeMaxLess) if argsList.Len() == 4 { rangeLookup := argsList.Back().Value.(formulaArg).ToBool() if rangeLookup.Type == ArgError { @@ -9222,7 +9224,7 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was return } if rangeLookup.Number == 0 { - exactMatch = true + matchMode = newNumberFormulaArg(matchModeWildcard) } } return @@ -9235,35 +9237,16 @@ func checkHVLookupArgs(name string, argsList *list.List) (idx, matchIdx int, was // HLOOKUP(lookup_value,table_array,row_index_num,[range_lookup]) // func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { - rowIdx, matchIdx, wasExact, exactMatch, - lookupValue, tableArray, errArg := checkHVLookupArgs("HLOOKUP", argsList) + rowIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("HLOOKUP", argsList) if errArg.Type == ArgError { return errArg } - row := tableArray.Matrix[0] - if exactMatch || len(tableArray.Matrix) == TotalRows { - start: - for idx, mtx := range row { - lhs := mtx - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx.ToNumber() - if lhs.Type == ArgError { - lhs = mtx - } - } - case ArgMatrix: - lhs = tableArray - } - if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { - matchIdx = idx - wasExact = true - break start - } - } + var matchIdx int + var wasExact bool + if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows { + matchIdx, wasExact = lookupLinearSearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear)) } else { - matchIdx, wasExact = hlookupBinarySearch(row, lookupValue) + matchIdx, wasExact = lookupBinarySearch(false, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary)) } if matchIdx == -1 { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") @@ -9271,8 +9254,8 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg { if rowIdx < 0 || rowIdx >= len(tableArray.Matrix) { return newErrorFormulaArg(formulaErrorNA, "HLOOKUP has invalid row index") } - row = tableArray.Matrix[rowIdx] - if wasExact || !exactMatch { + row := tableArray.Matrix[rowIdx] + if wasExact || matchMode.Number == matchModeWildcard { return row[matchIdx] } return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found") @@ -9391,6 +9374,43 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { return newMatrixFormulaArg(mtx) } +// lookupLinearSearch sequentially checks each look value of the lookup array until +// a match is found or the whole list has been searched. +func lookupLinearSearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (int, bool) { + tableArray := []formulaArg{} + if vertical { + for _, row := range lookupArray.Matrix { + tableArray = append(tableArray, row[0]) + } + } else { + tableArray = lookupArray.Matrix[0] + } + matchIdx, wasExact := -1, false +start: + for i, cell := range tableArray { + lhs := cell + if lookupValue.Type == ArgNumber { + if lhs = cell.ToNumber(); lhs.Type == ArgError { + lhs = cell + } + } else if lookupValue.Type == ArgMatrix { + lhs = lookupArray + } + if compareFormulaArg(lhs, lookupValue, matchMode, false) == criteriaEq { + matchIdx = i + wasExact = true + if searchMode.Number == searchModeLinear { + break start + } + } + if matchMode.Number == matchModeMinGreater || matchMode.Number == matchModeMaxLess { + matchIdx = int(calcMatch(int(matchMode.Number), formulaCriteriaParser(lookupValue.Value()), tableArray).Number) + continue + } + } + return matchIdx, wasExact +} + // VLOOKUP function 'looks up' a given value in the left-hand column of a // data array (or table), and returns the corresponding value from another // column of the array. The syntax of the function is: @@ -9398,34 +9418,16 @@ func (fn *formulaFuncs) TRANSPOSE(argsList *list.List) formulaArg { // VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup]) // func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { - colIdx, matchIdx, wasExact, exactMatch, - lookupValue, tableArray, errArg := checkHVLookupArgs("VLOOKUP", argsList) + colIdx, lookupValue, tableArray, matchMode, errArg := checkHVLookupArgs("VLOOKUP", argsList) if errArg.Type == ArgError { return errArg } - if exactMatch || len(tableArray.Matrix) == TotalRows { - start: - for idx, mtx := range tableArray.Matrix { - lhs := mtx[0] - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx[0].ToNumber() - if lhs.Type == ArgError { - lhs = mtx[0] - } - } - case ArgMatrix: - lhs = tableArray - } - if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq { - matchIdx = idx - wasExact = true - break start - } - } + var matchIdx int + var wasExact bool + if matchMode.Number == matchModeWildcard || len(tableArray.Matrix) == TotalRows { + matchIdx, wasExact = lookupLinearSearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeLinear)) } else { - matchIdx, wasExact = vlookupBinarySearch(tableArray, lookupValue) + matchIdx, wasExact = lookupBinarySearch(true, lookupValue, tableArray, matchMode, newNumberFormulaArg(searchModeAscBinary)) } if matchIdx == -1 { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") @@ -9434,67 +9436,52 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg { if colIdx < 0 || colIdx >= len(mtx) { return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index") } - if wasExact || !exactMatch { + if wasExact || matchMode.Number == matchModeWildcard { return mtx[colIdx] } return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found") } -// vlookupBinarySearch finds the position of a target value when range lookup +// lookupBinarySearch finds the position of a target value when range lookup // is TRUE, if the data of table array can't guarantee be sorted, it will // return wrong result. -func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasExact bool) { - var low, high, lastMatchIdx int = 0, len(tableArray.Matrix) - 1, -1 +func lookupBinarySearch(vertical bool, lookupValue, lookupArray, matchMode, searchMode formulaArg) (matchIdx int, wasExact bool) { + tableArray := []formulaArg{} + if vertical { + for _, row := range lookupArray.Matrix { + tableArray = append(tableArray, row[0]) + } + } else { + tableArray = lookupArray.Matrix[0] + } + var low, high, lastMatchIdx int = 0, len(tableArray) - 1, -1 + count := high for low <= high { mid := low + (high-low)/2 - mtx := tableArray.Matrix[mid] - lhs := mtx[0] - switch lookupValue.Type { - case ArgNumber: - if !lookupValue.Boolean { - lhs = mtx[0].ToNumber() - if lhs.Type == ArgError { - lhs = mtx[0] - } + cell := tableArray[mid] + lhs := cell + if lookupValue.Type == ArgNumber { + if lhs = cell.ToNumber(); lhs.Type == ArgError { + lhs = cell } - case ArgMatrix: - lhs = tableArray + } else if lookupValue.Type == ArgMatrix && vertical { + lhs = lookupArray } - result := compareFormulaArg(lhs, lookupValue, false, false) + result := compareFormulaArg(lhs, lookupValue, matchMode, false) if result == criteriaEq { matchIdx, wasExact = mid, true + if searchMode.Number == searchModeDescBinary { + matchIdx = count - matchIdx + } return } else if result == criteriaG { high = mid - 1 } else if result == criteriaL { - matchIdx, low = mid, mid+1 + matchIdx = mid if lhs.Value() != "" { lastMatchIdx = matchIdx } - } else { - return -1, false - } - } - matchIdx, wasExact = lastMatchIdx, true - return -} - -// vlookupBinarySearch finds the position of a target value when range lookup -// is TRUE, if the data of table array can't guarantee be sorted, it will -// return wrong result. -func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int, wasExact bool) { - var low, high, lastMatchIdx int = 0, len(row) - 1, -1 - for low <= high { - mid := low + (high-low)/2 - mtx := row[mid] - result := compareFormulaArg(mtx, lookupValue, false, false) - if result == criteriaEq { - matchIdx, wasExact = mid, true - return - } else if result == criteriaG { - high = mid - 1 - } else if result == criteriaL { - low, lastMatchIdx = mid+1, mid + low = mid + 1 } else { return -1, false } @@ -9542,7 +9529,7 @@ func iterateLookupArgs(lookupValue, lookupVector formulaArg) ([]formulaArg, int, } } } - compare := compareFormulaArg(lhs, col, false, false) + compare := compareFormulaArg(lhs, col, newNumberFormulaArg(matchModeMaxLess), false) // Find exact match if compare == criteriaEq { matchIdx = idx @@ -9584,6 +9571,139 @@ func (fn *formulaFuncs) index(array formulaArg, rowIdx, colIdx int) formulaArg { return newListFormulaArg(cells) } +// validateMatchMode check the number of match mode if be equal to 0, 1, -1 or +// 2. +func validateMatchMode(mode float64) bool { + return mode == matchModeExact || mode == matchModeMinGreater || mode == matchModeMaxLess || mode == matchModeWildcard +} + +// validateSearchMode check the number of search mode if be equal to 1, -1, 2 +// or -2. +func validateSearchMode(mode float64) bool { + return mode == searchModeLinear || mode == searchModeReverseLinear || mode == searchModeAscBinary || mode == searchModeDescBinary +} + +// prepareXlookupArgs checking and prepare arguments for the formula function +// XLOOKUP. +func (fn *formulaFuncs) prepareXlookupArgs(argsList *list.List) formulaArg { + if argsList.Len() < 3 { + return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP requires at least 3 arguments") + } + if argsList.Len() > 6 { + return newErrorFormulaArg(formulaErrorVALUE, "XLOOKUP allows at most 6 arguments") + } + lookupValue := argsList.Front().Value.(formulaArg) + lookupArray := argsList.Front().Next().Value.(formulaArg) + returnArray := argsList.Front().Next().Next().Value.(formulaArg) + ifNotFond := newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + matchMode, searchMode := newNumberFormulaArg(matchModeExact), newNumberFormulaArg(searchModeLinear) + if argsList.Len() > 3 { + ifNotFond = argsList.Front().Next().Next().Next().Value.(formulaArg) + } + if argsList.Len() > 4 { + if matchMode = argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber(); matchMode.Type != ArgNumber { + return matchMode + } + } + if argsList.Len() > 5 { + if searchMode = argsList.Back().Value.(formulaArg).ToNumber(); searchMode.Type != ArgNumber { + return searchMode + } + } + if lookupArray.Type != ArgMatrix || returnArray.Type != ArgMatrix { + return newErrorFormulaArg(formulaErrorNA, formulaErrorNA) + } + if !validateMatchMode(matchMode.Number) || !validateSearchMode(searchMode.Number) { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + return newListFormulaArg([]formulaArg{lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode}) +} + +// xlookup is an implementation of the formula function XLOOKUP. +func (fn *formulaFuncs) xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx int, + condition1, condition2, condition3, condition4 bool, returnArray formulaArg) formulaArg { + result := [][]formulaArg{} + for rowIdx, row := range returnArray.Matrix { + for colIdx, cell := range row { + if condition1 { + if condition2 { + result = append(result, []formulaArg{cell}) + continue + } + if returnArrayRows > 1 && returnArrayCols > 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + } + if condition3 { + if returnArrayCols != lookupCols { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if colIdx == matchIdx { + result = append(result, []formulaArg{cell}) + continue + } + } + if condition4 { + if returnArrayRows != lookupRows { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + if rowIdx == matchIdx { + if len(result) == 0 { + result = append(result, []formulaArg{cell}) + continue + } + result[0] = append(result[0], cell) + } + } + } + } + array := newMatrixFormulaArg(result) + cells := array.ToList() + if len(cells) == 1 { + return cells[0] + } + return array +} + +// XLOOKUP function searches a range or an array, and then returns the item +// corresponding to the first match it finds. If no match exists, then +// XLOOKUP can return the closest (approximate) match. The syntax of the +// function is: +// +// XLOOKUP(lookup_value,lookup_array,return_array,[if_not_found],[match_mode],[search_mode]) +// +func (fn *formulaFuncs) XLOOKUP(argsList *list.List) formulaArg { + args := fn.prepareXlookupArgs(argsList) + if args.Type != ArgList { + return args + } + lookupValue, lookupArray, returnArray, ifNotFond, matchMode, searchMode := args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5] + lookupRows, lookupCols := len(lookupArray.Matrix), 0 + if lookupRows > 0 { + lookupCols = len(lookupArray.Matrix[0]) + } + if lookupRows != 1 && lookupCols != 1 { + return newErrorFormulaArg(formulaErrorVALUE, formulaErrorVALUE) + } + verticalLookup := lookupRows >= lookupCols + var matchIdx int + switch searchMode.Number { + case searchModeLinear, searchModeReverseLinear: + matchIdx, _ = lookupLinearSearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode) + default: + matchIdx, _ = lookupBinarySearch(verticalLookup, lookupValue, lookupArray, matchMode, searchMode) + } + if matchIdx == -1 { + return ifNotFond + } + returnArrayRows, returnArrayCols := len(returnArray.Matrix), len(returnArray.Matrix[0]) + condition1 := lookupRows == 1 && lookupCols == 1 + condition2 := returnArrayRows == 1 || returnArrayCols == 1 + condition3 := lookupRows == 1 && lookupCols > 1 + condition4 := lookupRows > 1 && lookupCols == 1 + return fn.xlookup(lookupRows, lookupCols, returnArrayRows, returnArrayCols, matchIdx, condition1, condition2, condition3, condition4, returnArray) +} + // INDEX function returns a reference to a cell that lies in a specified row // and column of a range of cells. The syntax of the function is: // diff --git a/calc_test.go b/calc_test.go index 97a7588..be2f185 100644 --- a/calc_test.go +++ b/calc_test.go @@ -3497,17 +3497,17 @@ func TestCalcToList(t *testing.T) { } func TestCalcCompareFormulaArg(t *testing.T) { - assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), false, false), criteriaEq) + assert.Equal(t, compareFormulaArg(newEmptyFormulaArg(), newEmptyFormulaArg(), newNumberFormulaArg(matchModeMaxLess), false), criteriaEq) lhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg()}) rhs := newListFormulaArg([]formulaArg{newEmptyFormulaArg(), newEmptyFormulaArg()}) - assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaL) - assert.Equal(t, compareFormulaArg(rhs, lhs, false, false), criteriaG) + assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaL) + assert.Equal(t, compareFormulaArg(rhs, lhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaG) lhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) rhs = newListFormulaArg([]formulaArg{newBoolFormulaArg(true)}) - assert.Equal(t, compareFormulaArg(lhs, rhs, false, false), criteriaEq) + assert.Equal(t, compareFormulaArg(lhs, rhs, newNumberFormulaArg(matchModeMaxLess), false), criteriaEq) - assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, false, false), criteriaErr) + assert.Equal(t, compareFormulaArg(formulaArg{Type: ArgUnknown}, formulaArg{Type: ArgUnknown}, newNumberFormulaArg(matchModeMaxLess), false), criteriaErr) } func TestCalcMatchPattern(t *testing.T) { @@ -3779,6 +3779,113 @@ func TestCalcXIRR(t *testing.T) { } } +func TestCalcXLOOKUP(t *testing.T) { + cellData := [][]interface{}{ + {}, + {nil, nil, "Quarter", "Gross Profit", "Net profit", "Profit %"}, + {nil, nil, "Qtr1", nil, 19342, 29.30}, + {}, + {nil, "Income Statement", "Qtr1", "Qtr2", "Qtr3", "Qtr4", "Total"}, + {nil, "Total sales", 50000, 78200, 89500, 91250, 308.95}, + {nil, "Cost of sales", -25000, -42050, -59450, -60450, -186950}, + {nil, "Gross Profit", 25000, 36150, 30050, 30800, 122000}, + {}, + {nil, "Depreciation", -899, -791, -202, -412, -2304}, + {nil, "Interest", -513, -853, -150, -956, -2472}, + {nil, "Earnings before Tax", 23588, 34506, 29698, 29432, 117224}, + {}, + {nil, "Tax", -4246, -6211, -5346, -5298, 21100}, + {}, + {nil, "Net profit", 19342, 28295, 24352, 24134, 96124}, + {nil, "Profit %", 0.293, 0.278, 0.234, 0.276, 0.269}, + } + f := prepareCalcData(cellData) + formulaList := map[string]string{ + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2))": "87272.293", + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$G6,NA(),0,-2))": "309258.95", + "=SUM(XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,-2))": "87272.293", + "=SUM(XLOOKUP($C3,$C5:$G5,$C6:$G17,NA(),0,2))": "87272.293", + "=SUM(XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,2))": "244000", + "=XLOOKUP(D2,$B6:$B17,C6:C17)": "25000", + "=XLOOKUP(D2,$B6:$B17,XLOOKUP($C3,$C5:$G5,$C6:$G17))": "25000", + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "25000", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) + result, err := f.CalcCellValue("Sheet1", "D3") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError := map[string]string{ + "=XLOOKUP()": "XLOOKUP requires at least 3 arguments", + "=XLOOKUP($C3,$C5:$C5,$C6:$C17,NA(),0,2,1)": "XLOOKUP allows at most 6 arguments", + "=XLOOKUP($C3,$C5,$C6,NA(),0,2)": "#N/A", + "=XLOOKUP($C3,$C4:$D5,$C6:$C17,NA(),0,2)": "#VALUE!", + "=XLOOKUP($C3,$C5:$C5,$C6:$G17,NA(),0,-2)": "#VALUE!", + "=XLOOKUP($C3,$C5:$G5,$C6:$F7,NA(),0,2)": "#VALUE!", + "=XLOOKUP(D2,$B6:$B17,$C6:$G16,NA(),0,2)": "#VALUE!", + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),3,2)": "#VALUE!", + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,0)": "#VALUE!", + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax", + "=XLOOKUP(D2,$B6:$B17,$C6:$G17,NA(),0,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) + result, err := f.CalcCellValue("Sheet1", "D3") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } + + cellData = [][]interface{}{ + {"Salesperson", "Item", "Amont"}, + {"B", "Apples", 30, 25, 15, 50, 45, 18}, + {"L", "Oranges", 25, "D3", "E3"}, + {"C", "Grapes", 15}, + {"L", "Lemons", 50}, + {"L", "Oranges", 45}, + {"C", "Peaches", 18}, + {"B", "Pears", 40}, + {"B", "Apples", 55}, + } + f = prepareCalcData(cellData) + formulaList = map[string]string{ + // Test match mode with partial match (wildcards) + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),2)": "30", + // Test match mode with approximate match in vertical (next larger item) + "=XLOOKUP(32,B2:B9,C2:C9,NA(),1)": "30", + // Test match mode with approximate match in horizontal (next larger item) + "=XLOOKUP(30,C2:F2,C3:F3,NA(),1)": "25", + // Test match mode with approximate match in vertical (next smaller item) + "=XLOOKUP(40,C2:C9,B2:B9,NA(),-1)": "Pears", + // Test match mode with approximate match in horizontal (next smaller item) + "=XLOOKUP(29,C2:F2,C3:F3,NA(),-1)": "D3", + // Test search mode + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,1)": "25", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-1)": "45", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,2)": "50", + "=XLOOKUP(\"L\",A2:A9,C2:C9,NA(),0,-2)": "45", + // Test match mode and search mode + "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,-1)": "D3", + "=XLOOKUP(29,C2:H2,C3:H3,NA(),-1,1)": "D3", + } + for formula, expected := range formulaList { + assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) + result, err := f.CalcCellValue("Sheet1", "D3") + assert.NoError(t, err, formula) + assert.Equal(t, expected, result, formula) + } + calcError = map[string]string{ + // Test match mode with exact match + "=XLOOKUP(\"*p*\",B2:B9,C2:C9,NA(),0)": "#N/A", + } + for formula, expected := range calcError { + assert.NoError(t, f.SetCellFormula("Sheet1", "D3", formula)) + result, err := f.CalcCellValue("Sheet1", "D3") + assert.EqualError(t, err, expected, formula) + assert.Equal(t, "", result, formula) + } +} + func TestCalcXNPV(t *testing.T) { cellData := [][]interface{}{{nil, 0.05}, {"01/01/2016", -10000, nil}, diff --git a/excelize_test.go b/excelize_test.go index a15e801..4c136b6 100644 --- a/excelize_test.go +++ b/excelize_test.go @@ -590,7 +590,16 @@ func TestSetCellStyleBorder(t *testing.T) { var style int // Test set border on overlapping area with vertical variants shading styles gradient fill. - style, err = f.NewStyle(`{"border":[{"type":"left","color":"0000FF","style":2},{"type":"top","color":"00FF00","style":12},{"type":"bottom","color":"FFFF00","style":5},{"type":"right","color":"FF0000","style":6},{"type":"diagonalDown","color":"A020F0","style":9},{"type":"diagonalUp","color":"A020F0","style":8}]}`) + style, err = f.NewStyle(&Style{ + Border: []Border{ + {Type: "left", Color: "0000FF", Style: 3}, + {Type: "top", Color: "00FF00", Style: 4}, + {Type: "bottom", Color: "FFFF00", Style: 5}, + {Type: "right", Color: "FF0000", Style: 6}, + {Type: "diagonalDown", Color: "A020F0", Style: 7}, + {Type: "diagonalUp", Color: "A020F0", Style: 8}, + }, + }) if !assert.NoError(t, err) { t.FailNow() }