Option Explicit
' JRXLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode])
' JRXLOOKUP(查找值,查找数组,返回数组,没找到,匹配模式,搜索模式)
' Author:杰瑞办公
' Brief Introduction: 基本用法和XLOOKUP一致,同时弥补了XLOOKUP不能同时支持查找值和返回值同为数组的情况
Public Function JRXLOOKUP( _
ByVal lookup_value As Variant, _
ByVal lookup_array As Variant, _
ByVal return_array As Variant, _
Optional ByVal if_not_found As Variant, _
Optional ByVal match_mode As Variant, _
Optional ByVal search_mode As Variant _
) As Variant
On Error GoTo Fail
Dim mm As Long, sm As Long
mm = 0: sm = 1
If Not IsMissing(match_mode) Then If Not IsEmpty(match_mode) Then mm = CLng(match_mode)
If Not IsMissing(search_mode) Then If Not IsEmpty(search_mode) Then sm = CLng(search_mode)
Dim L As Variant, R As Variant, K As Variant
L = To2DArray(lookup_array)
R = To2DArray(return_array)
K = To2DArray(lookup_value)
Dim lRows As Long, lCols As Long, rRows As Long, rCols As Long, kRows As Long, kCols As Long
lRows = UBound(L, 1) - LBound(L, 1) + 1
lCols = UBound(L, 2) - LBound(L, 2) + 1
rRows = UBound(R, 1) - LBound(R, 1) + 1
rCols = UBound(R, 2) - LBound(R, 2) + 1
kRows = UBound(K, 1) - LBound(K, 1) + 1
kCols = UBound(K, 2) - LBound(K, 2) + 1
' 查找值必须为一行或者一列, 和XLOOKUP函数相类似
Dim isVertical As Boolean
If lCols = 1 And lRows >= 1 Then
isVertical = True
ElseIf lRows = 1 And lCols >= 1 Then
isVertical = False
Else
JRXLOOKUP = CVErr(xlErrValue)
Exit Function
End If
' 验证返回数组的维度兼容
If isVertical Then
If rRows <> lRows Then
JRXLOOKUP = CVErr(xlErrValue)
Exit Function
End If
Else
If rCols <> lCols Then
JRXLOOKUP = CVErr(xlErrValue)
Exit Function
End If
End If
' ---- 如果查找值为标量值 -> 行为类似于 XLOOKUP ----
If kRows = 1 And kCols = 1 Then
JRXLOOKUP = LookupOne(K(LBound(K, 1), LBound(K, 2)), L, R, _
if_not_found, mm, sm, isVertical)
Exit Function
End If
' ---- 如果查找值是一个数组 -> 会将结果溢出 ----
' 为了简便起见(与 XLOOKUP 一样),将查找值视为一个向量(即一行或一列)
If Not ((kRows = 1 And kCols >= 1) Or (kCols = 1 And kRows >= 1)) Then
JRXLOOKUP = CVErr(xlErrValue)
Exit Function
End If
Dim outArr As Variant
Dim i As Long, pos As Long
If isVertical Then
' 每次查找都会返回 1 x rCols(即返回数组中的一行)
If kCols = 1 Then
' 垂直列表 -> 垂直排列:k行 x r列
ReDim outArr(1 To kRows, 1 To rCols)
For i = 1 To kRows
Dim keyV As Variant
keyV = K(LBound(K, 1) + i - 1, LBound(K, 2))
FillRowResult outArr, i, LookupOne(keyV, L, R, if_not_found, mm, sm, isVertical), rCols
Next i
Else
' 水平列表 -> 水平连接:1 x (kCols * rCols
ReDim outArr(1 To 1, 1 To (kCols * rCols))
For i = 1 To kCols
Dim keyH As Variant
keyH = K(LBound(K, 1), LBound(K, 2) + i - 1)
FillRowResult2 outArr, (i - 1) * rCols + 1, LookupOne(keyH, L, R, if_not_found, mm, sm, isVertical), rCols
Next i
End If
JRXLOOKUP = outArr
Exit Function
Else
' “lookup_array”是水平排列的;每次“lookup”操作都会返回一个 rRows×1 的结果(即返回数组中的一个列)
If kCols = 1 Then
' 垂直列表 -> 按列合并:r行 × k行
ReDim outArr(1 To rRows, 1 To kRows)
For i = 1 To kRows
Dim keyV2 As Variant
keyV2 = K(LBound(K, 1) + i - 1, LBound(K, 2))
FillColResult outArr, i, LookupOne(keyV2, L, R, if_not_found, mm, sm, isVertical), rRows
Next i
Else
' 水平列表 -> 水平堆叠:r行 x k列
ReDim outArr(1 To rRows, 1 To kCols)
For i = 1 To kCols
Dim keyH2 As Variant
keyH2 = K(LBound(K, 1), LBound(K, 2) + i - 1)
FillColResult outArr, i, LookupOne(keyH2, L, R, if_not_found, mm, sm, isVertical), rRows
Next i
End If
JRXLOOKUP = outArr
Exit Function
End If
Fail:
JRXLOOKUP = CVErr(xlErrValue)
End Function
' ---- 核心功能:查找一个键(标量值)并返回标量值或二维数组 ----
Private Function LookupOne( _
ByVal key As Variant, _
ByRef L As Variant, _
ByRef R As Variant, _
ByVal if_not_found As Variant, _
ByVal mm As Long, _
ByVal sm As Long, _
ByVal isVertical As Boolean _
) As Variant
Dim pos As Long
pos = FindPosition(L, key, mm, sm, isVertical)
If pos = 0 Then
If IsMissing(if_not_found) Then
LookupOne = CVErr(xlErrNA)
Else
LookupOne = if_not_found
End If
Exit Function
End If
LookupOne = ExtractReturn(R, pos, isVertical)
End Function
' ---- 用于将结果放入输出缓冲区的辅助程序 ----
Private Sub FillRowResult(ByRef outArr As Variant, ByVal outRow As Long, ByVal res As Variant, ByVal rCols As Long)
Dim j As Long
If IsArray(res) Then
For j = 1 To rCols
outArr(outRow, j) = res(1, j)
Next j
Else
' 如果返回数组有多个列,但结果值为标量,则填充第一列。
outArr(outRow, 1) = res
For j = 2 To rCols
outArr(outRow, j) = vbNullString
Next j
End If
End Sub
Private Sub FillRowResult2(ByRef outArr As Variant, ByVal startCol As Long, ByVal res As Variant, ByVal rCols As Long)
Dim j As Long
If IsArray(res) Then
For j = 1 To rCols
outArr(1, startCol + j - 1) = res(1, j)
Next j
Else
outArr(1, startCol) = res
For j = 2 To rCols
outArr(1, startCol + j - 1) = vbNullString
Next j
End If
End Sub
Private Sub FillColResult(ByRef outArr As Variant, ByVal outCol As Long, ByVal res As Variant, ByVal rRows As Long)
Dim i As Long
If IsArray(res) Then
For i = 1 To rRows
outArr(i, outCol) = res(i, 1)
Next i
Else
outArr(1, outCol) = res
For i = 2 To rRows
outArr(i, outCol) = vbNullString
Next i
End If
End Sub
' ---- 数组标准化----
Private Function To2DArray(ByVal v As Variant) As Variant
Dim arr As Variant
If TypeName(v) = "Range" Then
arr = v.Value2
If Not IsArray(arr) Then
ReDim arr(1 To 1, 1 To 1)
arr(1, 1) = v.Value2
End If
To2DArray = arr
Exit Function
End If
If IsArray(v) Then
Dim lb1 As Long, ub1 As Long, lb2 As Long, ub2 As Long
On Error GoTo OneD
lb1 = LBound(v, 1): ub1 = UBound(v, 1)
lb2 = LBound(v, 2): ub2 = UBound(v, 2)
To2DArray = v
Exit Function
OneD:
On Error GoTo 0
lb1 = LBound(v): ub1 = UBound(v)
Dim tmp As Variant, i As Long
ReDim tmp(1 To (ub1 - lb1 + 1), 1 To 1)
For i = lb1 To ub1
tmp(i - lb1 + 1, 1) = v(i)
Next i
To2DArray = tmp
Exit Function
End If
ReDim arr(1 To 1, 1 To 1)
arr(1, 1) = v
To2DArray = arr
End Function
' 查找逻辑
Private Function FindPosition(ByRef L As Variant, ByVal key As Variant, _
ByVal mm As Long, ByVal sm As Long, ByVal isVertical As Boolean) As Long
Select Case sm
Case 1, -1
FindPosition = LinearFind(L, key, mm, sm, isVertical)
Case 2, -2
FindPosition = BinaryFind(L, key, mm, sm, isVertical)
Case Else
FindPosition = LinearFind(L, key, mm, 1, isVertical)
End Select
End Function
Private Function LinearFind(ByRef L As Variant, ByVal key As Variant, _
ByVal mm As Long, ByVal sm As Long, ByVal isVertical As Boolean) As Long
Dim n As Long: n = VectorLength(L, isVertical)
Dim i As Long, bestPos As Long: bestPos = 0
Dim bestValSet As Boolean: bestValSet = False
Dim bestVal As Variant
Dim startI As Long, endI As Long, stepI As Long
If sm = -1 Then
startI = n: endI = 1: stepI = -1
Else
startI = 1: endI = n: stepI = 1
End If
For i = startI To endI Step stepI
Dim cur As Variant
cur = VectorValue(L, i, isVertical)
If IsError(cur) Then GoTo NextI
Select Case mm
Case 0
If ValuesEqual(cur, key) Then LinearFind = i: Exit Function
Case 2
If WildcardMatch(CStr(key), CStr(cur)) Then LinearFind = i: Exit Function
Case -1
If ValuesEqual(cur, key) Then LinearFind = i: Exit Function
If IsComparable(cur, key) Then
If cur <= key Then
If Not bestValSet Then
bestVal = cur: bestPos = i: bestValSet = True
ElseIf cur > bestVal Then
bestVal = cur: bestPos = i
End If
End If
End If
Case 1
If ValuesEqual(cur, key) Then LinearFind = i: Exit Function
If IsComparable(cur, key) Then
If cur >= key Then
If Not bestValSet Then
bestVal = cur: bestPos = i: bestValSet = True
ElseIf cur < bestVal Then
bestVal = cur: bestPos = i
End If
End If
End If
Case Else
If ValuesEqual(cur, key) Then LinearFind = i: Exit Function
End Select
NextI:
Next i
LinearFind = bestPos
End Function
Private Function BinaryFind(ByRef L As Variant, ByVal key As Variant, _
ByVal mm As Long, ByVal sm As Long, ByVal isVertical As Boolean) As Long
If mm = 2 Then BinaryFind = 0: Exit Function
Dim lo As Long, hi As Long, mid As Long
lo = 1: hi = VectorLength(L, isVertical)
Dim asc As Boolean: asc = (sm = 2)
Dim bestPos As Long: bestPos = 0
Do While lo <= hi
mid = (lo + hi) \ 2
Dim cur As Variant
cur = VectorValue(L, mid, isVertical)
If IsError(cur) Or Not IsComparable(cur, key) Then
BinaryFind = LinearFind(L, key, mm, 1, isVertical)
Exit Function
End If
If ValuesEqual(cur, key) Then BinaryFind = mid: Exit Function
If asc Then
If cur < key Then
lo = mid + 1
If mm = -1 Then bestPos = mid
Else
hi = mid - 1
If mm = 1 Then bestPos = mid
End If
Else
If cur > key Then
lo = mid + 1
If mm = -1 Then bestPos = mid
Else
hi = mid - 1
If mm = 1 Then bestPos = mid
End If
End If
Loop
BinaryFind = bestPos
End Function
Private Function ExtractReturn(ByRef R As Variant, ByVal pos As Long, ByVal isVertical As Boolean) As Variant
Dim rRows As Long, rCols As Long
rRows = UBound(R, 1) - LBound(R, 1) + 1
rCols = UBound(R, 2) - LBound(R, 2) + 1
Dim outArr As Variant, i As Long, j As Long
If isVertical Then
If rCols = 1 Then
ExtractReturn = R(LBound(R, 1) + pos - 1, LBound(R, 2))
Exit Function
End If
ReDim outArr(1 To 1, 1 To rCols)
For j = 1 To rCols
outArr(1, j) = R(LBound(R, 1) + pos - 1, LBound(R, 2) + j - 1)
Next j
ExtractReturn = outArr
Else
If rRows = 1 Then
ExtractReturn = R(LBound(R, 1), LBound(R, 2) + pos - 1)
Exit Function
End If
ReDim outArr(1 To rRows, 1 To 1)
For i = 1 To rRows
outArr(i, 1) = R(LBound(R, 1) + i - 1, LBound(R, 2) + pos - 1)
Next i
ExtractReturn = outArr
End If
End Function
Private Function VectorLength(ByRef L As Variant, ByVal isVertical As Boolean) As Long
If isVertical Then
VectorLength = UBound(L, 1) - LBound(L, 1) + 1
Else
VectorLength = UBound(L, 2) - LBound(L, 2) + 1
End If
End Function
Private Function VectorValue(ByRef L As Variant, ByVal idx As Long, ByVal isVertical As Boolean) As Variant
If isVertical Then
VectorValue = L(LBound(L, 1) + idx - 1, LBound(L, 2))
Else
VectorValue = L(LBound(L, 1), LBound(L, 2) + idx - 1)
End If
End Function
Private Function ValuesEqual(ByVal a As Variant, ByVal b As Variant) As Boolean
If IsEmpty(a) And IsEmpty(b) Then ValuesEqual = True: Exit Function
If IsNull(a) Or IsNull(b) Then ValuesEqual = False: Exit Function
If IsError(a) Or IsError(b) Then ValuesEqual = False: Exit Function
On Error GoTo SafeFail
ValuesEqual = (a = b)
Exit Function
SafeFail:
ValuesEqual = (CStr(a) = CStr(b))
End Function
Private Function IsComparable(ByVal a As Variant, ByVal b As Variant) As Boolean
If IsError(a) Or IsError(b) Then IsComparable = False: Exit Function
If IsNull(a) Or IsNull(b) Then IsComparable = False: Exit Function
IsComparable = True
End Function
Private Function WildcardMatch(ByVal pattern As String, ByVal text As String) As Boolean
Dim p As String
p = pattern
p = Replace(p, "~~", ChrW(0))
p = Replace(p, "~*", "[*]")
p = Replace(p, "~?", "[?]")
p = Replace(p, ChrW(0), "~")
WildcardMatch = (text Like p)
End Function