Skip to content

Commit ec45d67

Browse files
committed
binary search in range lookup and new formula function: LOOKUP
1 parent 3648335 commit ec45d67

File tree

3 files changed

+315
-89
lines changed

3 files changed

+315
-89
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
BSD 3-Clause License
22

3-
Copyright (c) 2016-2020 The excelize Authors.
3+
Copyright (c) 2016-2021 The excelize Authors.
44
All rights reserved.
55

66
Redistribution and use in source and binary forms, with or without

calc.go

+179-36
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ var tokenPriority = map[string]int{
223223
// FLOOR.MATH
224224
// FLOOR.PRECISE
225225
// GCD
226+
// HLOOKUP
226227
// IF
227228
// INT
228229
// ISBLANK
@@ -239,6 +240,7 @@ var tokenPriority = map[string]int{
239240
// LN
240241
// LOG
241242
// LOG10
243+
// LOOKUP
242244
// LOWER
243245
// MDETERM
244246
// MEDIAN
@@ -275,6 +277,7 @@ var tokenPriority = map[string]int{
275277
// TRIM
276278
// TRUNC
277279
// UPPER
280+
// VLOOKUP
278281
//
279282
func (f *File) CalcCellValue(sheet, cell string) (result string, err error) {
280283
var (
@@ -2335,8 +2338,8 @@ func (fn *formulaFuncs) MUNIT(argsList *list.List) (result formulaArg) {
23352338
return newErrorFormulaArg(formulaErrorVALUE, "MUNIT requires 1 numeric argument")
23362339
}
23372340
dimension := argsList.Back().Value.(formulaArg).ToNumber()
2338-
if dimension.Type == ArgError {
2339-
return dimension
2341+
if dimension.Type == ArgError || dimension.Number < 0 {
2342+
return newErrorFormulaArg(formulaErrorVALUE, dimension.Error)
23402343
}
23412344
matrix := make([][]formulaArg, 0, int(dimension.Number))
23422345
for i := 0; i < int(dimension.Number); i++ {
@@ -3607,8 +3610,7 @@ func matchPattern(pattern, name string) (matched bool) {
36073610
if pattern == "*" {
36083611
return true
36093612
}
3610-
rname := make([]rune, 0, len(name))
3611-
rpattern := make([]rune, 0, len(pattern))
3613+
rname, rpattern := make([]rune, 0, len(name)), make([]rune, 0, len(pattern))
36123614
for _, r := range name {
36133615
rname = append(rname, r)
36143616
}
@@ -3636,11 +3638,9 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte
36363638
}
36373639
return criteriaG
36383640
case ArgString:
3639-
ls := lhs.String
3640-
rs := rhs.String
3641+
ls, rs := lhs.String, rhs.String
36413642
if !caseSensitive {
3642-
ls = strings.ToLower(ls)
3643-
rs = strings.ToLower(rs)
3643+
ls, rs = strings.ToLower(ls), strings.ToLower(rs)
36443644
}
36453645
if exactMatch {
36463646
match := matchPattern(rs, ls)
@@ -3649,7 +3649,15 @@ func compareFormulaArg(lhs, rhs formulaArg, caseSensitive, exactMatch bool) byte
36493649
}
36503650
return criteriaG
36513651
}
3652-
return byte(strings.Compare(ls, rs))
3652+
switch strings.Compare(ls, rs) {
3653+
case 1:
3654+
return criteriaG
3655+
case -1:
3656+
return criteriaL
3657+
case 0:
3658+
return criteriaEq
3659+
}
3660+
return criteriaErr
36533661
case ArgEmpty:
36543662
return criteriaEq
36553663
case ArgList:
@@ -3739,16 +3747,29 @@ func (fn *formulaFuncs) HLOOKUP(argsList *list.List) formulaArg {
37393747
}
37403748
}
37413749
row := tableArray.Matrix[0]
3742-
start:
3743-
for idx, mtx := range row {
3744-
switch compareFormulaArg(mtx, lookupValue, false, exactMatch) {
3745-
case criteriaL:
3746-
matchIdx = idx
3747-
case criteriaEq:
3748-
matchIdx = idx
3749-
wasExact = true
3750-
break start
3750+
if exactMatch || len(tableArray.Matrix) == TotalRows {
3751+
start:
3752+
for idx, mtx := range row {
3753+
lhs := mtx
3754+
switch lookupValue.Type {
3755+
case ArgNumber:
3756+
if !lookupValue.Boolean {
3757+
lhs = mtx.ToNumber()
3758+
if lhs.Type == ArgError {
3759+
lhs = mtx
3760+
}
3761+
}
3762+
case ArgMatrix:
3763+
lhs = tableArray
3764+
}
3765+
if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq {
3766+
matchIdx = idx
3767+
wasExact = true
3768+
break start
3769+
}
37513770
}
3771+
} else {
3772+
matchIdx, wasExact = hlookupBinarySearch(row, lookupValue)
37523773
}
37533774
if matchIdx == -1 {
37543775
return newErrorFormulaArg(formulaErrorNA, "HLOOKUP no result found")
@@ -3795,11 +3816,51 @@ func (fn *formulaFuncs) VLOOKUP(argsList *list.List) formulaArg {
37953816
exactMatch = true
37963817
}
37973818
}
3798-
start:
3799-
for idx, mtx := range tableArray.Matrix {
3800-
if len(mtx) == 0 {
3801-
continue
3819+
if exactMatch || len(tableArray.Matrix) == TotalRows {
3820+
start:
3821+
for idx, mtx := range tableArray.Matrix {
3822+
lhs := mtx[0]
3823+
switch lookupValue.Type {
3824+
case ArgNumber:
3825+
if !lookupValue.Boolean {
3826+
lhs = mtx[0].ToNumber()
3827+
if lhs.Type == ArgError {
3828+
lhs = mtx[0]
3829+
}
3830+
}
3831+
case ArgMatrix:
3832+
lhs = tableArray
3833+
}
3834+
if compareFormulaArg(lhs, lookupValue, false, exactMatch) == criteriaEq {
3835+
matchIdx = idx
3836+
wasExact = true
3837+
break start
3838+
}
38023839
}
3840+
} else {
3841+
matchIdx, wasExact = vlookupBinarySearch(tableArray, lookupValue)
3842+
}
3843+
if matchIdx == -1 {
3844+
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
3845+
}
3846+
mtx := tableArray.Matrix[matchIdx]
3847+
if col < 0 || col >= len(mtx) {
3848+
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index")
3849+
}
3850+
if wasExact || !exactMatch {
3851+
return mtx[col]
3852+
}
3853+
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
3854+
}
3855+
3856+
// vlookupBinarySearch finds the position of a target value when range lookup
3857+
// is TRUE, if the data of table array can't guarantee be sorted, it will
3858+
// return wrong result.
3859+
func vlookupBinarySearch(tableArray, lookupValue formulaArg) (matchIdx int, wasExact bool) {
3860+
var low, high, lastMatchIdx int = 0, len(tableArray.Matrix) - 1, -1
3861+
for low <= high {
3862+
var mid int = low + (high-low)/2
3863+
mtx := tableArray.Matrix[mid]
38033864
lhs := mtx[0]
38043865
switch lookupValue.Type {
38053866
case ArgNumber:
@@ -3812,24 +3873,106 @@ start:
38123873
case ArgMatrix:
38133874
lhs = tableArray
38143875
}
3815-
switch compareFormulaArg(lhs, lookupValue, false, exactMatch) {
3816-
case criteriaL:
3817-
matchIdx = idx
3818-
case criteriaEq:
3876+
result := compareFormulaArg(lhs, lookupValue, false, false)
3877+
if result == criteriaEq {
3878+
matchIdx, wasExact = mid, true
3879+
return
3880+
} else if result == criteriaG {
3881+
high = mid - 1
3882+
} else if result == criteriaL {
3883+
matchIdx, low = mid, mid+1
3884+
if lhs.Value() != "" {
3885+
lastMatchIdx = matchIdx
3886+
}
3887+
} else {
3888+
return -1, false
3889+
}
3890+
}
3891+
matchIdx, wasExact = lastMatchIdx, true
3892+
return
3893+
}
3894+
3895+
// vlookupBinarySearch finds the position of a target value when range lookup
3896+
// is TRUE, if the data of table array can't guarantee be sorted, it will
3897+
// return wrong result.
3898+
func hlookupBinarySearch(row []formulaArg, lookupValue formulaArg) (matchIdx int, wasExact bool) {
3899+
var low, high, lastMatchIdx int = 0, len(row) - 1, -1
3900+
for low <= high {
3901+
var mid int = low + (high-low)/2
3902+
mtx := row[mid]
3903+
result := compareFormulaArg(mtx, lookupValue, false, false)
3904+
if result == criteriaEq {
3905+
matchIdx, wasExact = mid, true
3906+
return
3907+
} else if result == criteriaG {
3908+
high = mid - 1
3909+
} else if result == criteriaL {
3910+
low, lastMatchIdx = mid+1, mid
3911+
} else {
3912+
return -1, false
3913+
}
3914+
}
3915+
matchIdx, wasExact = lastMatchIdx, true
3916+
return
3917+
}
3918+
3919+
// LOOKUP function performs an approximate match lookup in a one-column or
3920+
// one-row range, and returns the corresponding value from another one-column
3921+
// or one-row range. The syntax of the function is:
3922+
//
3923+
// LOOKUP(lookup_value,lookup_vector,[result_vector])
3924+
//
3925+
func (fn *formulaFuncs) LOOKUP(argsList *list.List) formulaArg {
3926+
if argsList.Len() < 2 {
3927+
return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at least 2 arguments")
3928+
}
3929+
if argsList.Len() > 3 {
3930+
return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires at most 3 arguments")
3931+
}
3932+
lookupValue := argsList.Front().Value.(formulaArg)
3933+
lookupVector := argsList.Front().Next().Value.(formulaArg)
3934+
if lookupVector.Type != ArgMatrix && lookupVector.Type != ArgList {
3935+
return newErrorFormulaArg(formulaErrorVALUE, "LOOKUP requires second argument of table array")
3936+
}
3937+
cols, matchIdx := lookupCol(lookupVector), -1
3938+
for idx, col := range cols {
3939+
lhs := lookupValue
3940+
switch col.Type {
3941+
case ArgNumber:
3942+
lhs = lhs.ToNumber()
3943+
if !col.Boolean {
3944+
if lhs.Type == ArgError {
3945+
lhs = lookupValue
3946+
}
3947+
}
3948+
}
3949+
if compareFormulaArg(lhs, col, false, false) == criteriaEq {
38193950
matchIdx = idx
3820-
wasExact = true
3821-
break start
3951+
break
38223952
}
38233953
}
3824-
if matchIdx == -1 {
3825-
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
3954+
column := cols
3955+
if argsList.Len() == 3 {
3956+
column = lookupCol(argsList.Back().Value.(formulaArg))
38263957
}
3827-
mtx := tableArray.Matrix[matchIdx]
3828-
if col < 0 || col >= len(mtx) {
3829-
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP has invalid column index")
3958+
if matchIdx < 0 || matchIdx >= len(column) {
3959+
return newErrorFormulaArg(formulaErrorNA, "LOOKUP no result found")
38303960
}
3831-
if wasExact || !exactMatch {
3832-
return mtx[col]
3961+
return column[matchIdx]
3962+
}
3963+
3964+
// lookupCol extract columns for LOOKUP.
3965+
func lookupCol(arr formulaArg) []formulaArg {
3966+
col := arr.List
3967+
if arr.Type == ArgMatrix {
3968+
col = nil
3969+
for _, r := range arr.Matrix {
3970+
if len(r) > 0 {
3971+
col = append(col, r[0])
3972+
continue
3973+
}
3974+
col = append(col, newEmptyFormulaArg())
3975+
}
38333976
}
3834-
return newErrorFormulaArg(formulaErrorNA, "VLOOKUP no result found")
3977+
return col
38353978
}

0 commit comments

Comments
 (0)