【问题标题】:Excel: Count unique comma-delimited strings in a column with countifs-style criteria from other columnsExcel:使用来自其他列的 countifs 样式标准计算列中唯一的逗号分隔字符串
【发布时间】:2017-02-22 18:56:31
【问题描述】:

希望帮助形成一个 Excel/VBA 向导来解决这个问题。我对自己的需求有一个可能的愿景,但缺乏实现它的专业知识。

本质上,这个问题结合了 countifs 公式(具有多个条件)的使用以及计算包含逗号分隔字符串的列中的唯一字符串,如下所示:

Criteria1 | Criteria2 |Names
A         | X         |Bob
B         | Y         |Cam;Bob
A         | Y         |Dan;Ava
A         | Y         |Ava;Cam

^在这个超级简化的示例中,这就像计算唯一名称,其中 Criteria1 = A & criteria2 = Y。答案 = 3 (Cam, Dan, Ava)

到目前为止,我已经能够找到一个 VBA 解决方案(来自 here),它计算给定列中的唯一字符串,如上面的“名称”,但我不知道如何将它与 countifs 样式结合起来仅将名称范围的某些部分传递给该函数的条件。

我创建了一个 xlsm 电子表格,通过更好的示例数据、预期结果和我目前拥有的部分 VBA 解决方案进一步阐述了这个问题:

xlsx

编辑:我使用的是 Excel 2013

edit2:除了 xlsm 之外还上传了 xlsx。我目前使用的 VBA 代码如下。请注意,我将此表单复制到另一个来源,但我并不真正了解 scripting.dictionary 的工作原理:/

Function cntunq(ByVal rng As Range)

' http://www.mrexcel.com/forum/excel-questions/437952-counting-unique-values-seperate-comma.html

Dim cl As Range, i As Integer
Dim dic1, ar
ar = Split(Replace(Join(Application.Transpose(rng), ";"), vbLf, ""), ";")
Debug.Print Join(ar, ";")
Set dic1 = CreateObject("Scripting.Dictionary")
dic1.CompareMode = vbTextCompare
For i = 0 To UBound(ar)
    dic1(ar(i)) = ""
Next i
cntunq = dic1.Count

End Function

Edit3:上面的代码只是用 ; 分隔的字符串计算给定范围内的唯一值。我不知道的部分是如何修改它以获取 paramArray 条件

【问题讨论】:

  • 没有多少人会下载 xslm 文件。它可能包含有害代码。请直接在原始帖子中发布您尝试过的代码,并解释它具体在做什么是错误的。
  • 我会使用 split 和字典来获取唯一名称列表。然后返回字典中的项目数作为答案。
  • 如果我有时间我会玩这个 :-)
  • @ScottCraner 感谢您对有风险的 xslm 下载的评论。我已经上传了一个 xlsx,并将通过编辑将 VBA 发布到原始文件。至于你的建议,我真的不明白如何实现它,但听起来这实现了计数唯一字符串部分,没有来自其他列部分的多个条件......

标签: vba excel


【解决方案1】:

这里是使用字典的 UDF:

Function MyCount(critRng As Range, crit As String, critRng2 As Range, crit2 As String, cntRng As Range, delim As String) As Long
Dim critarr(), critarr2(), cntarr()
Set dict = CreateObject("Scripting.Dictionary")

critarr = critRng.Value
cntarr = cntRng.Value
critarr2 = critRng2.Value
If UBound(critarr, 1) <> UBound(cntarr, 1) Then Exit Function
For i = LBound(critarr, 1) To UBound(critarr, 1)
    If critarr(i, 1) = crit And critarr2(i, 1) = crit2 Then
        splt = Split(cntarr(i, 1), delim)
        For j = LBound(splt) To UBound(splt)
            On Error Resume Next
            dict.Add splt(j), splt(j)
            On Error GoTo 0
        Next j
    End If
Next i
MyCount = dict.Count
End Function

把它放在一个模块中,你会像公式一样调用它:

=MyCount($A$2:$A$5,"A",$B$2:$B$5,"Y",$C$2:$C$5,";")


根据评论编辑

这将允许一个数组条目,这将允许许多条件:

Function MyCount2(delim As String, rsltArr()) As Long
Set dict = CreateObject("Scripting.Dictionary")
Dim splt() As String
Dim i&, j&
For i = LBound(rsltArr, 1) To UBound(rsltArr, 1)
    If rsltArr(i, 1) <> "False" And rsltArr(i, 1) <> "" Then
        splt = Split(rsltArr(i, 1), delim)
        For j = LBound(splt) To UBound(splt)
            On Error Resume Next
            dict.Add splt(j), splt(j)
            On Error GoTo 0
        Next j
    End If
Next i
MyCount2 = dict.Count
End Function

然后将其输入为以下数组公式:

=MyCount2(";",IF(($A$2:$A$5="A")*($B$2:$B$5="Y"),$C$2:$C$5))

作为数组公式,退出编辑模式时需要使用 Ctrl-Shift-Enter 确认,而不是 Enter。如果操作正确,Excel 会在公式周围加上{}

如果您想要更多条件,则在 IF() 语句的第一个条件中添加另一个布尔乘法。因此,如果您想测试 Z 列是否大于 0,您可以在 B 列测试之后添加 * ($Z$2:$Z$5&gt;0)


这是一个使用 ParamArray 的非数组公式。

Function MyCount3(cntrng As Range, delim As String, ParamArray t()) As Long
Set dict = CreateObject("Scripting.Dictionary")
Dim cntArr As Variant
cntArr = cntrng.Value
Dim tArr() As Boolean
Dim splt() As String
Dim I&, l&
Dim tpe As String
ReDim tArr(1 To t(0).Rows.Count)

For l = 1 To t(0).Rows.Count
    For I = LBound(t) To UBound(t) Step 2
        If Not tArr(l) Then
            If InStr("<>=", Left(t(I + 1), 1)) = 0 Then t(I + 1) = "=" & t(I + 1)
            If InStr("<>=", Mid(t(I + 1), 2, 1)) > 0 Then Z = 2 Else Z = 1
            tArr(l) = Application.Evaluate("NOT(""" & t(I).Item(l).Value & """" & Left(t(I + 1), Z) & """" & Mid(t(I + 1), Z + 1) & """)")
      End If
    Next I
Next l

For l = 1 To UBound(tArr)
    If Not tArr(l) Then
        splt = Split(cntArr(l, 1), delim)
        For j = LBound(splt) To UBound(splt)
            On Error Resume Next
            dict.Add splt(j), splt(j)
            On Error GoTo 0
        Next j
    End If
Next l
MyCount3 = dict.Count
End Function

类似于 SUMIFS,COUNTIFS 的输入。

第一个标准是需要拆分和统计的范围。

第二个是它应该分割的分隔符。

然后其余的成对输入。

=MyCount3($C$2:$C$5,";",$A$2:$A$5,"A",$B$2:$B$5,"Y")

【讨论】:

  • 感谢您的帮助...有趣的解决方案。是否有一种方法可以修改它以采用额外的标准,而无需在更多的 critrng 和 crit 参数中硬编码并使用 if 语句对它们进行处理?有点像 countifs 如何让您提供任意数量的条件对...
  • 可以修改为使用数组公式IF。如果我有时间我会修改它。
  • @CBrown19 见编辑。您可能需要刷新屏幕。请记住通过单击答案旁边的复选标记将其中一个答案标记为正确。选择您使用的答案。
  • 太棒了!第二个功能真的很好用。我完全理解 if 语句数组公式是如何工作的……希望我能想到它,尽管我肯定会在 vba 逻辑上绊倒来处理数组。非常感谢!
  • @ScottCraner 你知道我是新来的,但通常如果发布并接受了答案,我们不会发布新答案?
【解决方案2】:

考虑:

Sub poiuyt()
    Dim N As Long, i As Long, c As Collection
    Set c = New Collection

    N = Cells(Rows.Count, 1).End(xlUp).Row
    For i = 2 To N
        If Cells(i, 1) = "A" And Cells(i, 2) = "Y" Then
            arr = Split(Cells(i, 3), ";")
                For Each a In arr
                    On Error Resume Next
                        c.Add a, CStr(a)
                    On Error GoTo 0
                Next a
        End If
    Next i      
    MsgBox c.Count      
End Sub

【讨论】:

  • 感谢您的帮助。为了解决我的问题,这需要从子函数更改为函数并修改为在函数参数中采用多个条件和条件范围,有点像工作表公式 countifs 或 sumifs。
  • @CBrown19 然后我会从 Scott's 代码开始,然后用 ParamArray 扩展它
  • ParamArray 听起来是正确的方法。不幸的是,我是一个新手,不知道如何实现它,所以我希望向 stackexchange 的专业人士学习如何做到这一点:)
【解决方案3】:

我采用了一种不同的、可能更复杂的方法。您可以直接在工作表上指定条件。

函数是UniqueNames(数据范围,名称范围,规则范围,可选AndRules = True,可选PrintNames = False)

这是我的样本表

我在
中使用了该功能 4 次 - 范围(“E16”)为UniqueNames(A1:F11,G1:G11,A13:B16,FALSE)
- 范围(“E17”)为UniqueNames(A1:F11,G1:G11,A13:B16)
- 范围(“F16”)为UniqueNames(A1:F11,G1:G11,A13:B16,FALSE,TRUE)
- Range("F17") as UniqueNames(A1:F11,G1:G11,A13:B16,,TRUE)

以下条件运算符是可接受的=,&lt;,&gt;,&lt;=,&gt;=,!=
运算符后面必须跟一个空格,并且
- 一个常数值例如完成
- 一个值的函数,例如状态(项目#6)
空条件无效

代码如下:注意:还有一个私有函数

Public Function UniqueNames(DataSource As Range, ResultsSource As Range, RulesSource As Range, _
                            Optional AndRules As Boolean = True, Optional PrintNames As Boolean = False) As String
' Return N unique names and who

   ' Split Indexed Expressions
   Dim iChar As Integer
   ' Expression to eval
   Dim Expression() As String
   Dim expr As Variant
   ' Results
   Dim Results As Variant
   ' Get Data into variant array
   Dim Data As Variant
   ' Get Rules into variant array of NRows x 2
   Dim Rules As Variant

   iChar = 0
   Data = DataSource
   If RulesSource.Columns.Count = 1 Then
      Rules = Union(RulesSource, RulesSource.Offset(0, 1))
   ElseIf RulesSource.Columns.Count > 2 Then
      Rules = RulesSource.Resize(RulesSource.Rows.Count, 2)
   Else
      Rules = RulesSource
   End If

   Results = ResultsSource.Resize(ResultsSource.Rows.Count, UBound(Rules))

   For i = LBound(Rules) + 1 To UBound(Rules)
      For j = LBound(Data, 2) To UBound(Data, 2)
         If Rules(i, 1) = Data(1, j) Then
            ' rules must be "operator condition"
            Expression = Split(Rules(i, 2), " ", 2)
            Expression(1) = Trim(Expression(1))

            ' determine which expression is this
            ' Convert expression when an item of something e.g. EndDate(10)
            iChar = InStr(Expression(1), "(")
            If iChar > 0 Then
               expr = ExprToVal(Data, Left$(Expression(1), iChar - 1), _
                              Mid$(Expression(1), iChar + 1, Len(Expression(1)) - iChar - 1))
            Else
               expr = Expression(1)
            End If

            For k = LBound(Data, 1) + 1 To UBound(Data, 1)
               Results(k, i) = False
               Select Case (Expression(0))
                  Case "="
                     If Data(k, j) <> "" And LCase$(Data(k, j)) = LCase$(expr) Then Results(k, i) = True
                  Case "<"
                     If Data(k, j) <> "" And LCase$(Data(k, j)) < LCase$(expr) Then Results(k, i) = True
                  Case ">"
                     If Data(k, j) <> "" And LCase$(Data(k, j)) > LCase$(expr) Then Results(k, i) = True
                  Case "<="
                     If Data(k, j) <> "" And LCase$(Data(k, j)) <= LCase$(expr) Then Results(k, i) = True
                  Case ">="
                     If Data(k, j) <> "" And LCase$(Data(k, j)) >= LCase$(expr) Then Results(k, i) = True
                  Case "!="
                     If Data(k, j) <> "" And LCase$(Data(k, j)) <> LCase$(expr) Then Results(k, i) = True
               End Select
            Next k
         End If
      Next j
   Next i

   ' create one list where all three rules are true
   Data = Results
   Set Results = Nothing
   ReDim Results(LBound(Data, 1) + 1 To UBound(Data, 1), 1 To 2) As Variant

   ' results now has the names w/a number representing how many rules were met
   For i = LBound(Data, 1) + 1 To UBound(Data, 1)
      Results(i, 1) = Data(i, 1)
      Results(i, 2) = 0
      For j = LBound(Data, 2) + 1 To UBound(Data, 2)
         If Data(i, j) Then Results(i, 2) = Results(i, 2) + 1
      Next j
   Next i

   ' put that back into data
   Data = Results
   Set Results = Nothing
   Results = ""

   For i = LBound(Data, 1) + 1 To UBound(Data, 1)
      If Data(i, 2) = UBound(Rules, 1) - LBound(Rules, 1) Then
         Results = Results & Data(i, 1) & ";"
      ElseIf AndRules = False And Data(i, 2) > 0 Then
         Results = Results & Data(i, 1) & ";"
      End If
   Next i

   ' split that into expression
   Expression = Split(Results, ";")
   For i = LBound(Expression) To UBound(Expression)
      For j = i + 1 To UBound(Expression)
         If Expression(i) = Expression(j) Then Expression(j) = ""
      Next j
   Next i

   iChar = 0
   Results = ""
   For i = LBound(Expression) To UBound(Expression)
      If Expression(i) <> "" Then
         Results = Results & Expression(i) & ";"
         iChar = iChar + 1
      End If
   Next i

   UniqueNames = ""
   If PrintNames Then
      ' prints number of unique names and the names
      UniqueNames = Results
   Else
      ' prints number of unique names
      UniqueNames = CStr(iChar)
   End If

End Function

Private Function ExprToVal(Data As Variant, expr As String, Index As String) As Variant

   Dim Row As Integer
   Dim Col As Integer
   Dim sCol As Variant

   ' Get what type of data this is
   For i = LBound(Data, 2) To UBound(Data, 2)
      sCol = Replace(Index, Data(1, i), "", 1, 1, vbTextCompare)
      If IsNumeric(sCol) Then
         Col = i
         Exit For
      ElseIf LCase$(Left$(Index, Len(Data(1, i)))) = LCase$(Data(1, i)) Then
         Col = i
         Exit For
      End If
   Next i
   ' now find the row of the value
   For i = LBound(Data, 1) + 1 To UBound(Data, 1)
      If LCase$(Data(i, Col)) = LCase$(sCol) Then
         Row = i
         Exit For
      End If
   Next i
   ' find the column of the value
   For i = LBound(Data, 2) To UBound(Data, 2)
      If LCase$(Data(1, i)) = LCase$(expr) Then
         Col = i
         Exit For
      End If
   Next i

   If Row >= LBound(Data, 1) And Row <= UBound(Data, 1) And _
      Col >= LBound(Data, 2) And Col <= UBound(Data, 2) Then
      ExprToVal = Data(Row, Col)
   Else
      ExprToVal = ""
   End If
End Function

【讨论】:

  • 真的很有趣,谢谢。稍后我将不得不回来仔细查看整个代码。在单独的工作表范围和公式参数中指定标准是一个有趣的权衡。我最初的反应是,我会将条件运算符与值分开,以便该值可以链接到另一个单元格。另外,这会在条件中处理像*? 这样的通配符吗?
  • 我没有考虑添加通配符,不确定 Excel vba 是否有正则表达式,所以通配符将是另一项任务
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-17
  • 1970-01-01
相关资源
最近更新 更多