【问题标题】:VLookup with Multiple Criteria with VBA and the array formula method使用 VBA 和数组公式方法的 VLookup with Multiple Criteria
【发布时间】:2022-01-11 16:34:25
【问题描述】:

因此,当需要在 VBA 中创建具有多个条件的 VLookUp 时,我们的想法是利用漂亮的数组公式方法及其背后的想法。

问题: 我们可以把它翻译成 VBA:

{=INDEX(range1,MATCH(1,(A1=range2)*(B1=range3)*(C1=range4),0))}

根本不使用 Excel 中的公式?例如,不这样做:

=AGGREGATE(15, 6, '[TUR Master Report.xlsm]Archive'!$B$2:$B$13/
                  (('[TUR Master Report.xlsm]Archive'!$B$2:$B$13>=DO2)*
                   ('[TUR Master Report.xlsm]Archive'!$B$2:$B$13<=DP2)*
                   ('[TUR Master Report.xlsm]Archive'!$A$2:$A$13=A2)), 1)

或任何类似的东西(.ArrayFormula.Formula 等)。

我在想这样的事情 foo = Match(1,(A1=rangeA)*(B1=rangeB)*(C1=rangeC),0),不过当然不行,虽然在Excel公式的逻辑里。到目前为止,我已经创建了以下解决方法:

Function GetLookupDataTriple(tableName As String, lookIntoColumn As String, myArray As Variant) As Variant
    
    Dim lo As ListObject
    Set lo = Sheet1.ListObjects(tableName)
    
    Dim i As Long
    For i = 2 To lo.ListColumns(myArray(0)).Range.Rows.Count
        If lo.ListColumns(myArray(0)).Range.Cells(RowIndex:=i) = myArray(1) Then
            If lo.ListColumns(myArray(2)).Range.Cells(RowIndex:=i) = myArray(3) Then
                If lo.ListColumns(myArray(4)).Range.Cells(RowIndex:=i) = myArray(5) Then
                    GetLookupDataTriple = lo.ListColumns(lookIntoColumn).Range.Cells(RowIndex:=i)
                    Exit Function
                End If
            End If
        End If
    Next i
    
    GetLookupDataTriple = -1
    
End Function

使用 3 个过滤器效果很好,但这个想法是有点花哨,例如就像在excel原始公式中一样。这是一个示例数据,它使上述功能起作用:

?GetLookupDataTriple("Table1","To",array("From","Bulgaria","Cost",200,"Currency","USD"))

【问题讨论】:

  • 我想这取决于您所说的“更高级”是什么意思。您列出的是我的典型函数样式(尽管我倾向于使用 And 来满足两个标准,但当我有 3 个以上条件时坚持使用多个 If 语句)。如果你想避免嵌入 if 语句,你可以有一个子循环来验证每个条件在它自己的数组中是否满足,但这可能只会延长你已经简洁的代码。
  • 作为一种幻想,我正在寻找类似foo = Match(1,(A1=rangeA)*(B1=rangeB)*(C1=rangeC),0) 的东西,小巧而易于理解。我猜应该有一个内置的方法在 VBA 中做到这一点。
  • 在 VBA AFAIK 中没有内置方法可以做到这一点。可能使用 3 个类似大小的 Variant 数组并循环是一种选择。
  • 我知道的唯一方法是使用 worksheet().Evaluate()。 vba 不能将数组相乘并返回一个数组,除非您自己编写一个函数来执行此操作,这是它工作所必需的。
  • 您是否担心需要两个、三个、四个或更多布尔值并返回匹配项?如果是这样,使用 paramArray 的正确编写的函数应该足够有弹性。

标签: excel vba vlookup


【解决方案1】:

A) VBA “VLookup” 基于 ListObject 数据

当您在 OP 中提到 ListObject 时,我专注于一种基于完全 listobject 数据 的方法。

作为一个实际的盈余,通过现有的表标题索引号来识别列引用将是简洁。因此,下面的函数 multCrit() 会返回具有任意数量的列条件的给定列 (retCol) 的值。

“作为一种幻想,我正在寻找类似 @​​987654326@ 的东西, 小而善解人意。”

ParamArray 中组织输入可能至少有助于保持函数调用 smallclear,例如通过以下伪语法

multCrit(lo, ReturnColumn, ParamArray:{Col1, search1, Col2, search2,...})  

请注意,我只颠倒了 ParamArray 中的输入顺序。

需要的参数

  • 第一个参数data 标识一个ListObject
  • 第二个参数retCol 标识要返回的列(标题或索引),
  • 第三个基于 0 的参数 ParamArray arr() 允许按以下顺序进行多个输入:
       - even inputs identify column (by header string or index number)
       - odd  inputs define a search value (e.g. explicitly or as cell reference)

“我猜应该有一个内置的方法在 VBA 中执行此操作。”

这种方法有条不紊地尝试

  • 为每个条件获取列数组块(一次性通过Application.Index() -注意 两个 数组参数的使用!)
  • 在临时数组容器tmp(又名锯齿状数组)内,并且
  • 在每个标准块中显示结果值 1(对于未发现结果显示 #NV 错误 2042)。

这允许在所有指示的列blocks中识别值1序列,即使这种方法处理内置检查,如 Excel 函数中的布尔值相乘。 - 当然还有一些改进的机会(例如找到下一个可能的项目而不是逐行循环),但它说明了方法。

函数multCrit()

Function multCrit(data As ListObject, ByVal retCol, ParamArray crit() As Variant) As Variant

'0) provide for 0-based temporary array container (aka jagged critay)
    Dim critCnt As Long: critCnt = (UBound(crit) + 1) \ 2
    Dim tmp: ReDim tmp(0 To critCnt - 1)
    
'1) include an array/column in one go into temporary array container
    Dim c As Long
    For c = LBound(crit) To UBound(crit) Step 2
        '~~~~~~~~~~~~~~~~~~~
        'execute 1 Match/col ~~> found elements receive value 1 (non-findings error 2042)
        '~~~~~~~~~~~~~~~~~~~
        tmp(c \ 2) = Application.Match(getCol(data, crit(c)), Array(crit(c + 1)), 0)
        'Debug.Print "tmp(" & c \ 2 & ")", "header: " & crit(c), data.ListColumns(crit(c)).Index, crit(c + 1)
    Next
    
'2) get lookup value as soon as all column values in a given row equal 1
    Dim r As Long
    For r = 1 To UBound(tmp(0))
        For c = 0 To UBound(tmp)
            'check next row, if no value 1 found
            If IsError(tmp(c)(r, 1)) Then Exit For  ' escape to check next row
            If c = UBound(tmp) Then                 ' struggled through to last element
                'get result value of found row from referenced retCol
                multCrit = getCol(data, retCol)(r, 1): Exit Function
            End If
        Next c
    Next r
End Function

帮助功能getCol()

返回由 ListObject 的 header nameindex number 标识的列数据:

Function getCol(data As ListObject, header)
'Purp:  get listobject column data via header (either string or index number)
    getCol = data.DataBodyRange.Columns(data.ListColumns(header).Index)
End Function

调用示例

请注意,该函数允许标题(和搜索项)输入的任何顺序,无论是显式还是作为范围引用;所以这个例子也演示了一个修改的列顺序和范围输入:

Sub ExampleCall()
    Dim lo As ListObject
    Set lo = Sheet1.ListObjects("Table1")
    'example display in VB Editor's immediate window: ~~> EN
    Debug.Print "*~~>", multCrit(lo, "lang", "Col2", "two", "Col3", "three", "Col1", Sheet1.Range("B1"))
End Sub

可能的代码扩展 // 编辑于 2021-12-12

如果您不坚持返回一个 (对于VLookUp 解决方案很典型),而是返回找到的数据 row 作为进一步的选择,您可以

  • 提供例如用于将零输入 (0) 传递给参数 retCol
  • 将函数MultCrit()的最后一段代码修改如下:
                'get result value of found row from referenced retCol
                If retCol = 0 Then                  ' special arg 0: return row
                    multCrit = r
                Else                                ' default: return value
                    multCrit = getCol(data, retCol)(r, 1): Exit Function
                End If

然后通过Debug.Print "*~~&gt;", multCrit(lo, 0, "Col2", "two", "Col3", "three", "Col1", Sheet1.Range("B1")) 显示将显示例如第二行作为数字结果:~~&gt; 2


B) 通过.Value(12)中的 XlRangeValueDataType 枚举的简短替代方法 // ►late Edit as of 2021-12-13◄

这种有条不紊的新方法完全基于.Value(xlRangeValueMSPersistXML)(也称为.Value(12))的字符串分析,它返回指定(ListObject ) 范围为 XML 格式 字符串。

  • 一个 sn-p 示例,其中包含列信息属性 Col1Col2 等的行节点可以是:
<xml><!-- omitting all namespace definitions -->
  <!-- omitted ... -->
  <rs:data>
   <z:row Col1="DE" Col2="eins" Col3="zwei" Col4="drei"/>
   <!-- etc... -->
  </rs:data>
 </x:PivotCache>
</xml> 

通过 XPath 搜索表达式将所有以编程方式设置标准条件,例如此处,例如

    "//zrow[@Col3='two' and @Col4='three' and @Col2='one']/@Col1"`

允许返回由参数retCol 传递的索引列值。 *(请注意,我对原始内容进行了转换,以便在没有命名空间问题的情况下进行更轻松的搜索,参见 zrow 而不是 z:row

在情况 A 的情况下,可以类似于 ExampleCall 调用此示例(不会返回 “可能的代码扩展”中建议的行索引)。

Function MultCrit12(lo As ListObject, ByVal retCol, ParamArray crit() As Variant) As Variant
'1) get FilterXML arguments
'   a) Arg1: wellformed xml content string (xlRangeValueMSPersistXML = 12)
    Dim content As String
    content = Replace(lo.Range.Value(12), ":", "")
    
'   b) Arg2: XPath by analyzing ParamArray crit()
    Dim c As Long
    Dim XPath As String: XPath = "//zrow["
    For c = LBound(crit) To UBound(crit) Step 2
        XPath = XPath & " and @Col" & lo.ListColumns(crit(c)).Index & "='" & crit(c + 1) & "'"
    Next
    If VarType(retCol) = vbString Then retCol = lo.ListColumns(retCol).Index   ' get column index of header
    XPath = Replace(XPath, "[ and ", "[") & "]/@Col" & retCol

'2) apply FilterXML upon above arguments
    With Application
        Dim ret
        ret = .FilterXML(content, XPath)   ' << FilterXML
        If VarType(ret) > vbArray Then
            MultCrit12 = ret(1, 1)
        Else
            MultCrit12 = ret
        End If
    End With
End Function

【讨论】:

  • 绝对是一个有趣的,但如果我在我的代码中实现它看起来很难立即掌握。
  • 感谢反馈。 - 仅供参考添加了一个可能的扩展以返回找到的行值作为进一步的选项@Vityata
  • 在 B) @Vityata 中发布了一种使用基于 XlRangeValueDataType 枚举的 FilterXML 的全新方法
【解决方案2】:

您想要查找 n 个条件的方法吗?

假设以下数据:

您可以使用 XLOOKUP:

=XLOOKUP(1&1&1,A1:A9&B1:B9&C1:C9,D1:D9,"Not Found";)

这将找到 a、b 和 c = 1 且结果为 8 的最后一行

【讨论】:

  • 尽管在 Excel 中使用方便,但该问题专门指 VBA 中的多个条件,而不使用 Application 函数。
  • 嗨,感谢您的解决方案,但这是我想“翻译”成 VBA 的东西。但不使用.Formula 或类似的东西。例如,如果可以将范围转换为 VBA 范围/数组等等......
【解决方案3】:

从 Ifs 迁移到 Select Case 至少会让事情变得更干净,以便以后添加更多标准(清洁就是花哨?);只需添加新案例,而不是搞乱If 和间距等。我的模型:

For i = 2 To lo.ListColumns(myArray(0)).Range.Rows.Count
    Select Case False
        Case lo.ListColumns(myArray(0)).Range.Cells(RowIndex:=i) = myArray(1)
        Case lo.ListColumns(myArray(2)).Range.Cells(RowIndex:=i) = myArray(3)
        Case lo.ListColumns(myArray(4)).Range.Cells(RowIndex:=i) = myArray(5)
        Case Else 
            GetLookupDataTriple = lo.ListColumns(lookIntoColumn).Range.Cells(RowIndex:=i)
            Exit For
    End Select
Next i

不过,这绝对不是你 foo = Match(1,(A1=rangeA)*(B1=rangeB)*(C1=rangeC),0) 的花哨/干净程度。

【讨论】:

  • 这个很漂亮,很好用,谢谢。尽管如此,仍在寻找被转换为布尔值的=Match(1,(condition)*(condition)...
  • 是的,我在等着看这整件事如何发展(已添加书签)......今天是一个很好的精神障碍,因为我默认为你所拥有的或以上的,所以谢谢你.
【解决方案4】:

如何邻接以下:

Sub Macro1()
'
' Macro1 Macro
'

'
Dim myArray(5) As Variant
myArray(0) = "a"
myArray(1) = 1
myArray(2) = "b"
myArray(3) = 1
myArray(4) = "c"
myArray(5) = 1

    MsgBox (GetLookupDataTriple2("Table1", "result", myArray))
End Sub


Function GetLookupDataTriple2(tableName As String, lookIntoColumn As String, myArray As Variant) As Variant
    
    Dim lo As ListObject
    Set lo = Sheet1.ListObjects(tableName)
    
    noOfSearchParam = UBound(myArray) - LBound(myArray)
    
    Dim found As Boolean
    
    For i = 2 To lo.ListColumns(myArray(0)).Range.Rows.Count
     found = True
        For s = 0 To noOfSearchParam Step 2
            If lo.ListColumns(myArray(s)).Range.Cells(RowIndex:=i) <> myArray(s + 1) Then
                 found = False
            End If
        Next s
        If found Then
            GetLookupDataTriple2 = lo.ListColumns(lookIntoColumn).Range.Cells(RowIndex:=i)
            Exit Function
        End If
    Next i
    GetLookupDataTriple2 = -1
End Function

【讨论】:

    【解决方案5】:

    工作表将允许数组的乘法/加法,但 vba 不允许。这就是问题的根源。至于为什么....我不知道,但我假设因为我们可以在 vba 中循环,但不能在公式中循环(至少在他们发明动态数组公式之前),他们在工作表中创建了能力,但在 vba 中没有。

    因此没有办法简单地使用 MATCH。我们需要编写自己的函数来简化输入。


    这里是 match 的 paramarray 版本,它接受任意数量的相同大小的垂直数组或相同大小的范围,并返回相对行号:

    Function myArrayMatch(ParamArray arr() As Variant) As Long
        If UBound(arr) Mod 2 <> 1 Then
            myArrayMatch = -1
            Exit Function
        End If
        Dim lgth As Long
        If TypeName(arr(LBound(arr))) = "Range" Then
            lgth = Intersect(arr(LBound(arr)).Parent.UsedRange, arr(LBound(arr))).Cells.Count
        Else
            lgth = UBound(arr(LBound(arr))) + LBound(arr(LBound(arr))) - 1
        End If
        Dim fnd() As Boolean
        ReDim fnd(1 To lgth) As Boolean
    
        Dim i As Long
        For i = LBound(arr) To UBound(arr) Step 2
            Dim rngarr As Variant
            If TypeName(arr(i)) = "Range" Then
                rngarr = Intersect(arr(i).Parent.UsedRange, arr(i))
            Else
                rngarr = arr(i)
            End If
            Dim j As Long
                For j = 1 To lgth
                If rngarr(j - IIf(LBound(rngarr, 1) = 0, 1, 0), 1) = arr(i + 1) Then
                    If i = LBound(arr) Then fnd(j) = True
                Else
                    fnd(j) = False
                End If
                If i = UBound(arr) - 1 And fnd(j) Then
                    myArrayMatch = j
                    Exit Function
                End If
            Next j
        Next i
        
                    
    End Function
    

    可以这样称呼:

    relRow = myArrayMatch(ActiveSheet.Range("A:A"),"X",ActiveSheet.Range("B:B"),"Y")
    

    范围/垂直数组是奇数标准,要搜索的值是偶数。

    【讨论】:

    • 赞许它同时适用于数组和范围,但我猜一定有(曾经?)更简单、更小的东西。
    • @Vityata 工作表将允许数组的乘法/加法,但 vba 不允许。这就是问题的根源。至于为什么....我不知道,但我假设因为我们可以在 vba 中循环,但不能在公式中循环(至少在他们发明动态数组公式之前),他们在工作表中创建了能力,但在 vba 中没有。
    猜你喜欢
    • 2016-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-01
    相关资源
    最近更新 更多