65.9K
CodeProject 正在变化。 阅读更多。
Home

Excel 和 VBA - 直接查询 ListObject

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

Nov 9, 2014

Ms-PL

8分钟阅读

viewsIcon

40628

downloadIcon

774

本指南介绍如何通过创建由类似查询语言的合成对象组成的状态机来直接查询 Excel ListObjects(表格)。

引言

    如果您花了很多时间在数据分析领域,您很可能会以某种方式使用 Excel。  当您需要快速整合数据时,有时 Excel 能提供最快的方式从点 A 到点 B。  您可以启动一个 SQL 服务器之类的东西,但如果您是在从数据存储中提取数据后与数据交互,或者您没有这些工具,有时您就必须凑合着用您手头的东西。

这就是查询 ListObjects 的用武之地。  Excel 2007+ 为我们提供了表格,这简化了 Excel 公式(因为您不再处理 $A$2:$A$403,而是使用 TableName[ColumnName] 并且范围会自动更新以适应扩展)的整合。

    您可以在原地过滤您的表格,但如果您需要对该数据的子集进行操作,隐藏列、行、复制可见内容、创建新的工作表/工作簿等等可能会变得非常繁琐;尤其是当某些过滤器变得复杂时。

背景

    如果您之前使用过查询语言,您就会知道它们提供了一种方便的方式来从预先存在的来源收集数据。  您了解结构,并且它相当稳定,结果使您能够进行进一步的处理。

    这里介绍的是 Excel 中伪查询的第一步,它还不能执行 JOIN,但随着时间的推移可能会发展到那个阶段。  在 VBA 中编写查询而不是通过 ADO 加载表结构的一个好处是智能感知(intellisense)。

代码解析

    提供的示例有点牵强,由于这种基础设施的复杂性,它被自动化了。

    未包含的是一个“非常隐藏”的 <Document_Structure> 工作表,它有助于(相当愚蠢的)重构过程。  在我这边,当列、表或工作表重命名时,它会利用这个“先前状态”来知道它以前是什么,以便移动到它现在是什么。它不处理重叠的上下文,所以如果我有一个与列同名的表,如果其中一个被重命名,它稍后可能会失败。  这个功能被删除了,因为它与文章的重点无关,只是使文件变大了。

    进入代码,设置一个真正的类查询语法是一项相当复杂的任务,但一旦自动化,我相信结果是值得付出的努力的。  为了在此模型中提供相当有用的智能感知,与行和列的交互已被简化。  让我们从整体示例模型概述开始。

  • ExampleData
  • ListOrderingTable
  • ListOrderingTableRow
  • ListOrderingTableRows
  • OrderDetailsData
  • OrderDetailsQuery
  • OrderDetailsQueryCondition
  • OrderDetailsQueryHead
  • OrderDetailsQueryOpPart
  • OrderDetailsQueryOrderByParent
  • OrderDetailsQueryOrderByPart
  • OrderDetailsQueryPart
  • OrderDetailsSelectParent
  • OrderDetailsSelectPart
  • OrderingTableOrderByBuilder
  • OrderingTableQuery
  • OrderingTableQueryBuilder
  • OrderingTableSelectBuilder
  • OrderingTableWhereBuilder

    您可以看到,这是一个艰巨的任务!

    就像任何事情一样,无论大小,分解后就会变得更容易。

    ExampleData - VBA 项目中关键的主工作表称为“Example”。  ExampleData 是代表其关注数据点的类。在这种情况下,它维护一个名为 OrderingTable 的属性,类型为 ListOrderingTable。  在工作表方面,实际的表格正如您所料,名为“OrderingTable”。

   您的第一个问题可能是:为什么还要维护一个与实际工作表分开的类数据?

    如果您像我一样处理了很多 Excel 数据,您就会明白有时您可能希望您的代码与数据分开。  所以在这种情况下,如果我构建了模型,我可能想在一个*单独的*工作簿,或多个共享通用数据模型的工作簿上操作。

    举个例子:检查给定工作簿的自动化模型会注入自己的状态跟踪工作表。  如果我将其嵌入工作表中,我就失去了实例化并关联到不同数据源的能力。

回到数据,ListOrderingTable 维护着实际工作表中出现的列。

ID 名称 描述 类别 价格 数量 总价

    ListOrderingTable 上的属性如下:

Public Property Get ID() As ListColumn
    Set ID = Source.ListColumns("ID")
End Property

Public Property Get Name() As ListColumn
    Set Name = Source.ListColumns("Name")
End Property

Public Property Get Description() As ListColumn
    Set Description = Source.ListColumns("Description")
End Property

Public Property Get Category() As ListColumn
    Set Category = Source.ListColumns("Category")
End Property

Public Property Get Price() As ListColumn
    Set Price = Source.ListColumns("Price")
End Property

Public Property Get Quantity() As ListColumn
    Set Quantity = Source.ListColumns("Quantity")
End Property

Public Property Get TotalCost() As ListColumn
    Set TotalCost = Source.ListColumns("Total Cost")
End Property

    这些在查询的匹配部分稍后会被引用。

    此自定义结构中的行也模仿了上述结构。  它们为每列提供了一个调用点,并返回一个范围,这简化了使用迭代方法审问这些对象的工作。  这是在创建查询之前构建的垫脚石。

    查询的核心存在于 OrderDetailsQuery 中,它根据 Query Builders 提供的信息构建实际的查询。  之所以要分离关注点,是因为:如果我有多个表格想要提供查询,重复编写代码只会进一步臃肿文档。

    让我们来看一个在我们测试数据上的示例查询。

    Set otQuery = _
        DataModel.QueryExample.OrderingTable.GetQueryBuilder() _
            .Where(OD_OTC_ID, OD_MC_GreaterThan, 10000) _
                .AndAlso(OD_OTC_ID, OD_MC_LessThan, 40000) _
                .AndAlso(OD_OTC_Name, OD_MC_DoesNotStartWith, "Mashed") _
            .OrElse(OD_OTC_Name, OD_MC_Contains, "Mashed") _
                .AndAlsoOpenParen(OD_OTC_Price, OD_MC_LessThan, 4) _
                    .OrElse(OD_OTC_Price, OD_MC_GreaterThan, 7) _
                .CloseParen() _
            .OrderBy(OD_OTC_ID, OD_OD_Ascending) _
            .SelectColumn(OD_OTC_ID) _
                .AndAlso(OD_OTC_Name) _
                .AndAlso(OD_OTC_Price) _
                .AndAlso(OD_OTC_Quantity) _
                .AndAlso(OD_OTC_TotalCost).Build

    一旦您习惯了忽略 OD_OTC_、OD_MC_ 和 OD_OD_ 部分,这就相当直接了。  查询基本上是在查找表中 ID 大于 10000 且小于 40000 并且名称不以“Mashed”开头;或者包含“Mashed”且价格小于 4 或大于 7 的项目。  然后按 ID 升序排序,结果包含 ID、名称、价格、数量和总价。

    虽然自己遍历数据可以很容易地提取匹配项,但构建查询模型可以使添加、删除、排序和更改条件变得极其简单。  添加的智能感知是一个额外的奖励。

    “QueryBuilder”的目的是简化为构建查询收集必要信息的过程。  这是在不持续重建底层状态机的情况下做到这一点最有效的方法。  这也是为什么“Build”会被显式调用的原因。  如果您“SelectAll”,则构建方法是不必要的。


    现在我们有了查询,让我们看一下“Build”方法,它分为三个步骤,从 OrderingTableSelectBuilder 上的 Build 开始。

Public Function Build() As OrderingTableQuery
    Dim m_qry_Result As OrderingTableQuery
    Set m_qry_Result = New OrderingTableQuery
    m_qry_Result.Build Me
    Set Build = m_qry_Result
End Function

    然后是 OrderingTableQuery 上的 Build。

Friend Sub Build(vTarget As Variant)
    Set m_qry_Query = New OrderDetailsQuery
    Set m_lst_Source = m_qry_Query.Build(vTarget)
End Sub

    最后是 OrderDetailsQuery。

Friend Function Build(target As Variant) As Variant
    Dim m_int_Index As Integer
    Dim m_col_Leafs As Collection
    Dim m_col_Orderings As Collection
    Dim m_col_Selects As Collection
    Dim m_col_Actions As Collection
    Dim m_var_Current As Variant
    Dim m_int_ParenLevel As Integer
    Dim m_qop_Part As OrderDetailsQueryOpPart
    Dim m_qpt_Part As OrderDetailsQueryPart
    Dim m_var_Source As Variant
    Set m_var_Source = Nothing
    If Not IsObject(target) Then _
        Exit Function
    Set m_col_Leafs = New Collection
    Set m_col_Orderings = New Collection
    Set m_col_Selects = New Collection
    Set m_col_Actions = New Collection
    Set m_var_Current = target
    While Not m_var_Current Is Nothing
        If TypeOf m_var_Current Is OrderDetailsSelectPart Then
            m_int_ParenLevel = 0
            Dim m_spt_Select As OrderDetailsSelectPart
            Set m_spt_Select = m_var_Current
            If m_col_Selects.Count = 0 Then
                m_col_Selects.Add m_spt_Select
            Else
                m_col_Selects.Add m_spt_Select, , 1
            End If
            Set m_var_Current = m_spt_Select.Parent
        ElseIf TypeOf m_var_Current Is OrderDetailsQueryOrderByPart Then
            m_int_ParenLevel = 0
            Dim m_obp_OrderBy As OrderDetailsQueryOrderByPart
            Set m_obp_OrderBy = m_var_Current
            If m_col_Orderings.Count = 0 Then
                m_col_Orderings.Add m_obp_OrderBy
            Else
                m_col_Orderings.Add m_obp_OrderBy, , 1
            End If
            Set m_var_Current = m_obp_OrderBy.Parent
        ElseIf TypeOf m_var_Current Is OrderDetailsQueryOpPart Then
            Set m_qop_Part = m_var_Current
            If Not m_qop_Part.InitialCondition Is Nothing Then
                If m_col_Leafs.Count = 0 Then
                    m_col_Leafs.Add m_qop_Part.InitialCondition
                Else
                    m_col_Leafs.Add m_qop_Part.InitialCondition, , 1
                End If
            End If
            If m_col_Actions.Count = 0 Then
                m_col_Actions.Add m_qop_Part.Action
            Else
                m_col_Actions.Add m_qop_Part.Action, Before:=1
            End If
            m_int_ParenLevel = 0
            Set m_var_Current = m_qop_Part.Parent
        ElseIf TypeOf m_var_Current Is OrderDetailsQueryHead Then
            Dim m_qrh_Head As OrderDetailsQueryHead
            Set m_qrh_Head = m_var_Current
            If Not IsEmpty(m_qrh_Head.Source) And IsObject(m_qrh_Head.Source) Then
                Set m_var_Source = m_qrh_Head.Source
            End If
            Set m_var_Current = Nothing
        End If
    Wend
    If m_col_Actions.Count > 0 Then
        m_col_Actions.Add OD_QueryPartAction.OD_QPA_Finish
        Dim m_qpa_Actions() As OD_QueryPartAction
        Dim m_cnd_Conditions() As OrderDetailsQueryCondition
        ReDim m_qpa_Actions(1 To m_col_Actions.Count)
        ReDim m_cnd_Conditions(1 To m_col_Leafs.Count)
    End If
    If m_col_Selects.Count > 0 Then
        m_int_SelectCount = m_col_Selects.Count
        ReDim m_ina_Selects(1 To m_int_SelectCount)
        For m_int_Index = 1 To m_col_Selects.Count
            Set m_spt_Select = m_col_Selects(m_int_Index)
            m_ina_Selects(m_int_Index) = m_spt_Select.Column
        Next
    Else
        m_int_SelectCount = 0
    End If
    If m_col_Orderings.Count > 0 Then
        m_int_OrderingCount = m_col_Orderings.Count
        ReDim m_ina_OrderingColumns(1 To m_int_OrderingCount)
        ReDim m_oda_OrderingDirections(1 To m_int_OrderingCount)
        For m_int_Index = 1 To m_int_OrderingCount
            Set m_obp_OrderBy = m_col_Orderings(m_int_Index)
            m_oda_OrderingDirections(m_int_Index) = m_obp_OrderBy.Direction
        Next
    Else
        m_int_OrderingCount = 0
    End If
    For m_int_Index = 1 To m_col_Actions.Count
        m_qpa_Actions(m_int_Index) = m_col_Actions(m_int_Index)
    Next
    For m_int_Index = 1 To m_col_Leafs.Count
        Set m_cnd_Conditions(m_int_Index) = m_col_Leafs(m_int_Index)
    Next
    Dim m_col_References As Collection
    Dim m_cnd_Current As OrderDetailsQueryCondition
    Dim m_int_ColumnIndex As Integer
    Dim m_int_CurrentTarget As Integer
    If m_col_Actions.Count > 0 Then
        ReDim m_ina_CriteriaTargets(1 To m_col_Leafs.Count)
        ReDim m_ina_FailJumps(1 To m_col_Leafs.Count)
        ReDim m_ina_PassJumps(1 To m_col_Leafs.Count)
        ReDim m_mca_Criteria(1 To m_col_Leafs.Count)
        ReDim m_vra_RHSCriteria(1 To m_col_Leafs.Count)
    End If
    Set m_col_References = New Collection
    For m_int_Index = 1 To m_col_Leafs.Count
        Set m_cnd_Current = m_col_Leafs(m_int_Index)
        m_mca_Criteria(m_int_Index) = m_cnd_Current.Operator
        If IsObject(m_cnd_Current.Value) Then
            Set m_vra_RHSCriteria(m_int_Index) = m_cnd_Current.Value
        Else
            m_vra_RHSCriteria(m_int_Index) = m_cnd_Current.Value
        End If
        Dim m_int_TargetIndex As Integer
        Dim m_boo_ColumnPresent As Boolean
        m_boo_ColumnPresent = False
        For m_int_TargetIndex = 1 To m_col_References.Count
            m_int_CurrentTarget = m_col_References(m_int_TargetIndex)
            If m_int_CurrentTarget = m_cnd_Current.Column Then
                m_int_ColumnIndex = m_int_TargetIndex
                m_boo_ColumnPresent = True
                Exit For
            End If
        Next
        If Not m_boo_ColumnPresent Then
            m_col_References.Add m_cnd_Current.Column
            m_int_ColumnIndex = m_col_References.Count
        End If
        m_ina_CriteriaTargets(m_int_Index) = m_int_ColumnIndex
    Next
    m_int_CriteriaReferenceCount = m_col_References.Count
    For m_int_Index = 1 To m_col_Orderings.Count
        Set m_obp_OrderBy = m_col_Orderings(m_int_Index)
        m_boo_ColumnPresent = False
        For m_int_TargetIndex = 1 To m_col_References.Count
            m_int_CurrentTarget = m_col_References(m_int_TargetIndex)
            If m_int_CurrentTarget = m_obp_OrderBy.Column Then
                m_int_ColumnIndex = m_int_TargetIndex
                m_boo_ColumnPresent = True
                Exit For
            End If
        Next
        If Not m_boo_ColumnPresent Then
            m_col_References.Add m_obp_OrderBy.Column
            m_int_ColumnIndex = m_col_References.Count
        End If
        m_ina_OrderingColumns(m_int_Index) = m_int_ColumnIndex
    Next
    m_int_AdditionalOrderingReferenceCount = m_col_References.Count - m_int_CriteriaReferenceCount
    For m_int_Index = 1 To m_col_Selects.Count
        Dim m_int_CurrentSelectColumn As Integer
        m_int_CurrentSelectColumn = m_ina_Selects(m_int_Index)
        m_boo_ColumnPresent = False
        For m_int_TargetIndex = 1 To m_col_References.Count
            m_int_CurrentTarget = m_col_References(m_int_TargetIndex)
            If m_int_CurrentSelectColumn = m_int_CurrentTarget Then
                m_boo_ColumnPresent = True
                m_int_ColumnIndex = m_int_TargetIndex
                Exit For
            End If
        Next
        If Not m_boo_ColumnPresent Then
            m_col_References.Add m_int_CurrentSelectColumn
            m_int_ColumnIndex = m_col_References.Count
        End If
        m_ina_Selects(m_int_Index) = m_int_ColumnIndex
    Next
    m_int_AdditionalSelectReferenceCount = m_col_References.Count - (m_int_CriteriaReferenceCount + m_int_AdditionalOrderingReferenceCount)
    m_int_ReferencedColumnCount = m_col_References.Count
    ReDim m_ina_Type(1 To m_int_ReferencedColumnCount)
    For m_int_Index = 1 To m_int_ReferencedColumnCount
        m_ina_Type(m_int_Index) = m_col_References(m_int_Index)
    Next
    If m_col_Actions.Count > 0 Then
        BuildQueryLogicalOrLeafs m_cnd_Conditions, m_qpa_Actions, 1, m_col_Actions.Count, 0, 0
    End If
    Set Build = m_var_Source
End Function

    正如您所见,它采取的第一步是解构查询构建器,从最右边的节点一直到查询构建器头部。  当查询中存在条件时,它就知道从最高阶组件“OrElse”开始构建查询。  在普通语言中,这将是最高阶的优先级(或最后评估点,取决于您如何看待它)。

    您会注意到大部分内容仅关注引用跟踪。  为了避免在查询评估期间多次加载同一列,它会将所有内容重新索引到一个基线。  因此,如果 Select、Orderby 和 Where 都引用 ID,它们都将使用相同的索引,从一 (1) 到引用的总数。

    从“OrElse”运算符开始,让我们看看它在做什么。

Private Sub BuildQueryLogicalOrLeafs(cndConditions() As OrderDetailsQueryCondition, qpaActions() As OD_QueryPartAction, iStart As Integer, iEnd As Integer, iFailTarget As Integer, iPassTarget As Integer)
    Dim m_col_OrPoints As Collection
    Dim m_int_Index As Integer
    Dim m_int_ParenDepth As Integer
    Dim m_boo_AddingOr As Boolean
    Dim m_int_CurrentStart As Integer
    Dim m_int_CurrentEnd As Integer
    Set m_col_OrPoints = New Collection
    If iStart <> iEnd Then
        For m_int_Index = iStart To iEnd - 1
            Select Case qpaActions(m_int_Index)
                Case OD_QPA_OpenParen
                    m_int_ParenDepth = m_int_ParenDepth + 1
                Case OD_QPA_CloseParen
                    m_int_ParenDepth = m_int_ParenDepth - 1
                Case OD_QPA_OrElse
                    If m_int_ParenDepth = 0 Then _
                        m_col_OrPoints.Add m_int_Index
            End Select
        Next
    End If
    m_int_CurrentStart = iStart
    If m_col_OrPoints.Count = 0 Then
        m_int_CurrentEnd = iEnd
        BuildQueryLogicalAndLeafs cndConditions, qpaActions, CorrectTarget(m_int_CurrentStart, qpaActions), CorrectTarget(m_int_CurrentEnd, qpaActions), iFailTarget, iPassTarget
    Else
        For m_int_Index = 1 To m_col_OrPoints.Count
            m_int_CurrentEnd = m_col_OrPoints(m_int_Index)
            Dim m_int_CorrectedFailTarget As Integer
            m_int_CorrectedFailTarget = CorrectTarget((m_int_CurrentEnd + 1) + CorrectJumpTarget(qpaActions, m_int_CurrentEnd + 1) - CorrectJumpTarget(qpaActions, m_int_CurrentEnd), qpaActions)
            While qpaActions(m_int_CorrectedFailTarget) = OD_QPA_CloseParenPadding Or qpaActions(m_int_CorrectedFailTarget) = OD_QPA_CloseParen
                m_int_CorrectedFailTarget = m_int_CorrectedFailTarget + 1
                If m_int_CorrectedFailTarget > UBound(qpaActions) Then
                    m_int_CorrectedFailTarget = UBound(qpaActions)
                    GoTo ExitWend
                End If
            Wend
ExitWend:
            BuildQueryLogicalAndLeafs cndConditions, qpaActions, m_int_CurrentStart, m_int_CurrentEnd, m_int_CorrectedFailTarget, iPassTarget
            m_int_CurrentStart = m_int_CurrentEnd + 1
        Next
        m_int_CurrentEnd = iEnd
        BuildQueryLogicalAndLeafs cndConditions, qpaActions, CorrectTarget(m_int_CurrentStart, qpaActions), CorrectTarget(m_int_CurrentEnd, qpaActions), iFailTarget, iPassTarget
    End If
End Sub

    然后进入 AndAlso。

Private Sub BuildQueryLogicalAndLeafs(cndConditions() As OrderDetailsQueryCondition, qpaActions() As OD_QueryPartAction, iStart As Integer, iEnd As Integer, iFailTarget As Integer, iPassTarget As Integer)
    Dim m_col_AndPoints As Collection
    Dim m_int_Index As Integer
    Dim m_int_ParenDepth As Integer
    Dim m_int_CurrentStart As Integer, _
        m_int_CurrentEnd   As Integer
    Dim m_int_EndAdjusted As Integer
    Set m_col_AndPoints = New Collection
    If iStart <> iEnd Then
        For m_int_Index = iStart To iEnd - 1
            Select Case qpaActions(m_int_Index)
                Case OD_QPA_OpenParen
                    m_int_ParenDepth = m_int_ParenDepth + 1
                Case OD_QPA_CloseParen
                    m_int_ParenDepth = m_int_ParenDepth - 1
                Case OD_QPA_AndAlso
                    If m_int_ParenDepth = 0 Then _
                        m_col_AndPoints.Add m_int_Index
            End Select
        Next
    End If
    If m_col_AndPoints.Count = 0 Then
        If qpaActions(iStart) = OD_QPA_OpenParen Then
            BuildQueryLogicalParenLeafs cndConditions, qpaActions, iStart, iEnd, iFailTarget, iPassTarget
        Else
            'When we're nested as deep as we can go...
            m_int_EndAdjusted = iEnd - CorrectJumpTarget(qpaActions, iEnd)
            
            m_ina_FailJumps(m_int_EndAdjusted) = iFailTarget - CorrectJumpTarget(qpaActions, iFailTarget)
            m_ina_PassJumps(m_int_EndAdjusted) = iPassTarget - CorrectJumpTarget(qpaActions, iPassTarget)
        End If
    Else
        m_int_CurrentStart = iStart
        For m_int_Index = 1 To m_col_AndPoints.Count
            m_int_CurrentEnd = m_col_AndPoints(m_int_Index)
            Dim m_int_CorrectedPassTarget As Integer
            m_int_CorrectedPassTarget = CorrectTarget((m_int_CurrentEnd + 1) + CorrectJumpTarget(qpaActions, m_int_CurrentEnd + 1) - CorrectJumpTarget(qpaActions, m_int_CurrentEnd), qpaActions)
            While qpaActions(m_int_CorrectedPassTarget) = OD_QPA_CloseParenPadding Or qpaActions(m_int_CorrectedPassTarget) = OD_QPA_CloseParen
                m_int_CorrectedPassTarget = m_int_CorrectedPassTarget + 1
                If m_int_CorrectedPassTarget > UBound(qpaActions) Then
                    m_int_CorrectedPassTarget = UBound(qpaActions)
                    GoTo ExitWend
                End If
            Wend
ExitWend:
            BuildQueryLogicalOrLeafs cndConditions, qpaActions, m_int_CurrentStart, m_int_CurrentEnd, iFailTarget, m_int_CorrectedPassTarget
            m_int_CurrentStart = m_int_CurrentEnd + 1
        Next
        m_int_CurrentEnd = iEnd
        If qpaActions(m_int_CurrentStart) = OD_QPA_OpenParen Then
            BuildQueryLogicalParenLeafs cndConditions, qpaActions, m_int_CurrentStart, m_int_CurrentEnd, iFailTarget, iPassTarget
        Else
            BuildQueryLogicalOrLeafs cndConditions, qpaActions, m_int_CurrentStart, m_int_CurrentEnd, iFailTarget, iPassTarget
        End If
    End If
End Sub

    最后是最低阶(或第一)运算符,括号。

Private Sub BuildQueryLogicalParenLeafs(cndConditions() As OrderDetailsQueryCondition, qpaActions() As OD_QueryPartAction, iStart As Integer, iEnd As Integer, iFailTarget As Integer, iPassTarget As Integer)
    Dim m_int_ParenDepth As Integer
    Dim m_int_Index As Integer
    Dim m_int_ZeroPoint As Integer
    If qpaActions(iStart) = OD_QPA_OpenParen Then
        For m_int_Index = iStart To iEnd
            Select Case qpaActions(m_int_Index)
                Case OD_QPA_OpenParen
                    m_int_ParenDepth = m_int_ParenDepth + 1
                Case OD_QPA_CloseParen
                    m_int_ParenDepth = m_int_ParenDepth - 1
                    If m_int_ParenDepth = 0 Then
                        m_int_ZeroPoint = m_int_Index
                        Exit For
                    End If
            End Select
        Next
        If m_int_ParenDepth <= 0 Then
            BuildQueryLogicalOrLeafs cndConditions, qpaActions, iStart + 1, m_int_ZeroPoint - 1, iFailTarget, iPassTarget
        End If
    End If
End Sub

    您会注意到 QPA(查询部分操作)散布各处,本质上,构建器正在合成常量,这些常量实际上代表了一串运算符。  将该流与操作数分离的早期设计选择导致了奇怪的“CloseParenPadding”以及纠正跳转目标的需求。

    在此查询状态机的上下文中,跳转目标指的是如何实现短路。  我选择了“AndAlso”和“OrElse”这两个词,因为它们存在于 VB.NET 中,并且具有预期的目标:仅评估到所需程度。

    在下面的 match 方法中,您会注意到每行的匹配仅仅是一组跳转目标,这些目标根据条件是否满足而被使用。  如果满足,则跳转到“p”条件,或跳转到“f”条件(表示失败)。  当任一指令指示跳转到零条件时,它要么完全通过匹配,要么未能匹配。

Friend Function MatchInternal(ByRef lCount As Long, vraDataSource() As Variant, qOperation As OD_QueryOperation) As Variant
    Dim m_lna_MatchingLines() As Long
    Dim m_lng_MatchingLineDimension As Long
    Dim m_lng_MatchingLineCount As Long
    Dim m_lng_LineIndex As Long
    Dim m_var_CurrentLHS As Variant, _
        m_var_CurrentRHS As Variant
    Dim m_boo_LastIsMatch As Boolean
    Dim m_var_BetMin As Variant
    Dim m_var_BetMax As Variant
    Dim m_var_CurrentRHSElement As Variant
    Dim m_lng_NewMatchLength As Long
    Dim m_int_Ordering As Long
    If m_int_CriteriaReferenceCount <> 0 Then
        m_lng_MatchingLineDimension = 4
        ReDim m_lna_MatchingLines(1 To m_lng_MatchingLineDimension)
        '**********************************
        ' Go through each line, on each go
        ' through the full criteria; with
        ' exception to terminal edges which
        ' yield a pass or fail.
        '**********************************
        For m_lng_LineIndex = 1 To lCount
            Dim m_int_CriteriaID As Long
            m_int_CriteriaID = 1
            While m_int_CriteriaID <> 0
                m_boo_LastIsMatch = False
                If lCount = 1 Then
                    m_var_CurrentLHS = vraDataSource(m_ina_CriteriaTargets(m_int_CriteriaID))
                Else
                    m_var_CurrentLHS = vraDataSource(m_ina_CriteriaTargets(m_int_CriteriaID))(m_lng_LineIndex, 1)
                End If
                m_var_CurrentRHS = m_vra_RHSCriteria(m_int_CriteriaID)
                Select Case m_mca_Criteria(m_int_CriteriaID)
                    Case OD_MC_HasFlag
                        m_boo_LastIsMatch = (m_var_CurrentLHS And m_var_CurrentRHS) = m_var_CurrentRHS
                    Case OD_MC_NotHasFlag
                        m_boo_LastIsMatch = (m_var_CurrentLHS And m_var_CurrentRHS) <> m_var_CurrentRHS
                    Case OD_MC_EqualTo
                        m_boo_LastIsMatch = m_var_CurrentLHS = m_var_CurrentRHS
                    Case OD_MC_LessThan
                        m_boo_LastIsMatch = m_var_CurrentLHS < m_var_CurrentRHS
                    Case OD_MC_GreaterThan
                        m_boo_LastIsMatch = m_var_CurrentLHS > m_var_CurrentRHS
                    Case OD_MC_GreaterThanOrEqualTo
                        m_boo_LastIsMatch = m_var_CurrentLHS >= m_var_CurrentRHS
                    Case OD_MC_LessThanOrEqualTo
                        m_boo_LastIsMatch = m_var_CurrentLHS <= m_var_CurrentRHS
                    Case OD_MC_Between
                        If IsArray(m_var_CurrentRHS) Then
                            m_var_BetMin = m_var_CurrentRHS(LBound(m_var_CurrentRHS))
                            m_var_BetMax = m_var_CurrentRHS(UBound(m_var_CurrentRHS))
                            m_boo_LastIsMatch = m_var_BetMin <= m_var_CurrentLHS And m_var_CurrentLHS <= m_var_BetMax
                        End If
                    Case OD_MC_Contains
                        m_boo_LastIsMatch = InStr(1, m_var_CurrentLHS, m_var_CurrentRHS) <> 0
                    Case OD_MC_DoesNotContain
                        m_boo_LastIsMatch = InStr(1, m_var_CurrentLHS, m_var_CurrentRHS) = 0
                    Case OD_MC_DoesNotEndWith
                        m_boo_LastIsMatch = InStrRev(m_var_CurrentLHS, m_var_CurrentRHS) <> Len(m_var_CurrentLHS) - (Len(m_var_CurrentRHS) - 1)
                    Case OD_MC_DoesNotStartWith
                        m_boo_LastIsMatch = InStr(1, m_var_CurrentLHS, m_var_CurrentRHS) <> 1
                    Case OD_MC_EndsWith
                        m_boo_LastIsMatch = InStrRev(m_var_CurrentLHS, m_var_CurrentRHS) = Len(m_var_CurrentLHS) - (Len(m_var_CurrentRHS) - 1)
                    Case OD_MC_In
                        If IsArray(m_var_CurrentRHS) Then
                            For Each m_var_CurrentRHSElement In m_var_CurrentRHS
                                If m_var_CurrentRHSElement = m_var_CurrentLHS Then
                                    m_boo_LastIsMatch = True
                                    Exit For
                                End If
                            Next
                        End If
                    Case OD_MC_Like
                        m_boo_LastIsMatch = m_var_CurrentLHS Like m_var_CurrentRHS
                    Case OD_MC_NotBetween
                        If IsArray(m_var_CurrentRHS) Then
                            m_var_BetMin = m_var_CurrentRHS(LBound(m_var_CurrentRHS))
                            m_var_BetMax = m_var_CurrentRHS(UBound(m_var_CurrentRHS))
                            m_boo_LastIsMatch = m_var_CurrentLHS > m_var_BetMin Or m_var_BetMax < m_var_CurrentLHS
                        End If
                    Case OD_MC_NotEqualTo
                        m_boo_LastIsMatch = m_var_CurrentLHS <> m_var_CurrentRHS
                    Case OD_MC_NotIn
                        If IsArray(m_var_CurrentRHS) Then
                            m_boo_LastIsMatch = True
                            For Each m_var_CurrentRHSElement In m_var_CurrentRHS
                                If m_var_CurrentRHSElement = m_var_CurrentLHS Then
                                    m_boo_LastIsMatch = False
                                    Exit For
                                End If
                            Next
                        End If
                    Case OD_MC_NotLike
                        m_boo_LastIsMatch = Not m_var_CurrentLHS Like m_var_CurrentRHS
                    Case OD_MC_StartsWith
                        m_boo_LastIsMatch = InStr(1, m_var_CurrentLHS, m_var_CurrentRHS) = 1
                End Select
                If m_boo_LastIsMatch Then
                    m_int_CriteriaID = m_ina_PassJumps(m_int_CriteriaID)
                Else
                    m_int_CriteriaID = m_ina_FailJumps(m_int_CriteriaID)
                End If
            Wend
            If m_boo_LastIsMatch Then
                Select Case qOperation
                    Case OD_QO_Any
                        MatchInternal = True
                        Exit Function
                    Case OD_QO_Count
                        m_lng_MatchingLineCount = m_lng_MatchingLineCount + 1
                    Case OD_QO_Select
                        If m_lng_MatchingLineCount >= m_lng_MatchingLineDimension Then
                            m_lng_NewMatchLength = m_lng_MatchingLineDimension * 2
                            ReDim Preserve m_lna_MatchingLines(1 To m_lng_NewMatchLength)
                            m_lng_MatchingLineDimension = m_lng_NewMatchLength
                        End If
                        m_lng_MatchingLineCount = m_lng_MatchingLineCount + 1
                        m_lna_MatchingLines(m_lng_MatchingLineCount) = m_lng_LineIndex
                End Select
            End If
        Next
        Select Case qOperation
            Case OD_QO_Any
                MatchInternal = False
            Case OD_QO_Count
                MatchInternal = m_lng_MatchingLineCount
            Case OD_QO_Select
                If m_lng_MatchingLineCount > 0 Then
                    ReDim Preserve m_lna_MatchingLines(1 To m_lng_MatchingLineCount)
                    If lCount > 1 And m_lng_MatchingLineCount > 1 And m_int_OrderingCount > 0 Then
                        QuickPivotSort vraDataSource, m_lna_MatchingLines, 1, m_lng_MatchingLineCount
                    End If
                Else
                    Dim m_lna_DummyResult() As Long
                    m_lna_MatchingLines = m_lna_DummyResult
                End If
                lCount = m_lng_MatchingLineCount
                MatchInternal = m_lna_MatchingLines
        End Select
    Else
        Select Case qOperation
            Case OD_QO_Any
                MatchInternal = lCount > 0
            Case OD_QO_Count
                MatchInternal = lCount
            Case OD_QO_Select
                If lCount > 0 Then
                    ReDim m_lna_MatchingLines(1 To lCount)
                    For m_lng_LineIndex = 1 To lCount
                        m_lna_MatchingLines(m_lng_LineIndex) = m_lng_LineIndex
                    Next
                    If lCount > 1 And m_int_OrderingCount > 0 Then
                        QuickPivotSort vraDataSource, m_lna_MatchingLines, 1, lCount
                    End If
                Else
                    m_lna_MatchingLines = m_lna_DummyResult
                End If
                MatchInternal = m_lna_MatchingLines
        End Select
    End If
End Function

    您会注意到查询的尾部会根据“QueryOperation”(qOperation)而变化,如果您只是询问是否存在匹配项,它会提前退出,或者如果不需要跟踪匹配项,则只关注计数。

关注点

    我从这个过程中了解到,将运算符与操作数分开只会带来比应有的更大的麻烦。 这在基本层面上说明了短路从逻辑流程角度是如何工作的。  如果将其展开成一个完整的编译器,它可能会构建具体的指令跳转到失败点,如果存在满足条件的替代路线;但是,我认为这是另一天的话题了!

    完整的源代码包含在 TestBook.zip 的“Test Book.xlsm”中。

历史

2014 年 11 月 9 日 - 初次发布。

2014 年 11 月 9 日 - 添加了下载链接。  我以为它是自动的!

2014 年 11 月 12 日 - 更新了代码块的格式,显示为“VB.NET”,因为 CodeProject 解析存在问题。  它无法识别 VBScript 中的“Friend”关键字,而 VBA 并不是有效的语言选择。

© . All rights reserved.