Home > Enterprise >  VBA filtering and counting values
VBA filtering and counting values

Time:02-16

Let consider a simplified database as shown in the picture attached below. Basically, I need to count how many "Type" match with a given "Letter". It would be extremely simple to perform this task by means of a Pivot table, but column C in my database contains multiple values, so I need to proceed in a different way.

enter image description here

A first idea is to:

  • filter the column B of my database if column C corresponds to a specific "Letter"
  • count the number of row containing the unique vales of "Type"

Example:

  • filter the column B if column C contains the letter "Gamma" (only two occurrences)
  • count the unique values in column B (since both occurrences have Type="French", we will count only 1 value in column B).

The desired output is shown in the cell E2.

enter image description here

Given that ExcelFILTER()function doesn't allow for wildcards, I used the following formula from cell E2 and down below:

=ROW(UNIQUE(FILTER($B$2:$B$15),ISNUMBER(SEARCH($D1,$C$2:$C$15))))

and it works properly, as shown in the second picture (column E). I would like, though, to perform this calculation automatically, in VBA, and I should also account for column C containing empty values. I was wondering what is the simplest and fastest way to do this. I've tried to use VBA WorksheetFunction and perform a simple For cycle, but the ROW() function is not available, since VBA has its own ROW-method. Another method would be creating a collection/dictionary object and perform the counting, but I'm sure there is a simpler way. Any idea? Thank you!

CodePudding user response:

  1. Highlight your table and click "Insert - Table". Name it "MyTable" for now.

  2. In the Data ribbon, in the "Get & Transform" section, click "From Table/Range" (for recent Excel, older will be very similar).

  3. A PowerQuery editor window should pop up, displaying a new "MyTable" query.

  4. My advice is to leave source queries alone, then create new queries that use them. So, to create a new query that starts with your "MyTable" query, click the (horribly named) "Manage - Reference" button from the Home ribbon of the PowerQuery editor.

  5. This new query will be your details query exploding each individual letter into separate rows. On the top of the right pane is the query name, rename it to "Detail"

  6. Click on the first column title (#) and then click the "Delete" button to remove that column.

  7. Click on the "Letter" column title because this is the one we're going to work on next.

  8. In the Transform ribbon, click "Split Column\By Delimiter". A dialog box should pop up.

  9. In the Advanced Options section of the dialog box, change to split into rows, not columns.

  10. Click "Ok" in the dialog box. You should be really close to the details table you want except for the leading spaces. With the "Letter" column still highlighted, click "Format\Trim" in the Transform ribbon.

  11. Highlight both columns (e.g. since the second column is still highlighted, shift-click the title of the first column to select them both) and then, from the Home ribbon, click "Remove Rows\Remove Duplicates"

If you did it right (and I explained right), then clicking "Advanced Editor" from the Home ribbon should display:

let
    Source = MyTable,
    #"Removed Columns" = Table.RemoveColumns(Source,{"#"}),
    #"Split Column by Delimiter" = Table.ExpandListColumn(Table.TransformColumns(#"Removed Columns", {{"Letter", Splitter.SplitTextByDelimiter(",", QuoteStyle.Csv), let itemType = (type nullable text) meta [Serialized.Text = true] in type {itemType}}}), "Letter"),
    #"Changed Type" = Table.TransformColumnTypes(#"Split Column by Delimiter",{{"Letter", Text.Trim, type text}}),
    #"Removed Duplicates" = Table.Distinct(#"Trimmed Text")
in
    #"Removed Duplicates

For now, click the "Close & Load" button (the first one in the Home ribbon). A new worksheet should appear for each of your new queries. If you were to delete them, the stored results of your two new queries will be removed but the actual queries themselves will remain as "Connection Only" queries, i.e. 'just instructions'.

Okay, so now you have options!

  • If you want to get your counts table via Pivot Table, you can either use your new Details worksheet, or you can change Details query to load to the data model and create a pivot table that reads from the data model.

  • If you want to get your counts table via PowerQuery, edit the "Details" query, click "Manage - Reference" from the Home ribbon, and then in this new query, click the first column header to highlight the column, then in the Transform ribbon click "Group By".

Either way, as with pivot tables, you'll need to refresh the data (in the Data ribbon) to re-execute the queries.

CodePudding user response:

Split to Unique Count

Option Explicit

Sub SplitToUniqueCount()
    
    Const sName As String = "Sheet1"
    Const sfRow As Long = 2
    Const uCol As Long = 3 ' Unique Column
    Const vCol As Long = 2 ' Value Column
    Const uDelimiter As String = ", "
    
    Const dName As String = "Sheet1"
    Const dfcAddress As String = "D1"
    
    Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
    
    Dim sws As Worksheet: Set sws = wb.Worksheets(sName)
    Dim srg As Range: Set srg = sws.Range("A1").CurrentRegion
    Dim srCount As Long: srCount = srg.Rows.Count
    If srCount < sfRow Then Exit Sub
    
    Dim uData As Variant: uData = srg.Columns(uCol).Value
    Dim vData As Variant: vData = srg.Columns(vCol).Value
    
    Dim uArr() As String
    Dim uKey As Variant
    Dim vKey As Variant
    Dim r As Long
    Dim u As Long
    Dim uString As String
    
    Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
    dict.CompareMode = vbTextCompare
    
    For r = sfRow To srCount
        uKey = uData(r, 1)
        If IsValidKey(uKey) Then
            vKey = vData(r, 1)
            If IsValidKey(vKey) Then
                uArr = Split(uKey, uDelimiter)
                For u = 0 To UBound(uArr)
                    uString = uArr(u)
                    If dict.Exists(uString) Then
                        dict(uString)(vKey) = Empty
                    Else
                        Set dict(uString) = AddToNewDictionary(vKey)
                    End If
                Next u
            End If
        End If
    Next r
    If dict.Count = 0 Then Exit Sub
    
    Dim dData As Variant: ReDim dData(1 To dict.Count, 1 To 2)
    r = 0
        
    For Each uKey In dict.Keys
        r = r   1
        dData(r, 1) = uKey
        dData(r, 2) = dict(uKey).Count
    Next uKey
    Set dict = Nothing
    
    Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
    With dws.Range(dfcAddress).Resize(, 2)
        .Resize(r).Value = dData
        .Resize(r).Sort .Resize(r, 1), xlAscending, , , , , , xlNo
        .Resize(dws.Rows.Count - .Row - r   1).Offset(r).Clear
    End With
    
    MsgBox "Done.", vbInformation
    
End Sub

Function IsValidKey( _
    ByVal Key As Variant) _
As Boolean
    If Not IsError(Key) Then
        If Len(Key) > 0 Then
            IsValidKey = True
        End If
    End If
End Function

Function AddToNewDictionary( _
    ByVal Key As Variant) _
As Object
    Dim dict As Object: Set dict = CreateObject("Scripting.Dictionary")
    dict.CompareMode = vbTextCompare
    dict(Key) = Empty
    Set AddToNewDictionary = dict
End Function

Edit

Using a Formula

  • Here is a formula version. Note that I don't have 365 so your feedback is welcome.
  • This will require the unique values already written in column D.
Sub SplitToUniqueFormula()
    
    Const wsName As String = "Sheet1"
    Const sfRow As Long = 2
    Const uCol As Long = 3
    Const vCol As Long = 2
    Const dfcAddress As String = "D1"
    
    Dim wb As Workbook: Set wb = ThisWorkbook
    
    Dim ws As Worksheet: Set ws = wb.Worksheets(wsName)
    Dim srg As Range: Set srg = ws.Range("A1").CurrentRegion
    Dim srCount As Long: srCount = srg.Rows.Count
    If srCount < sfRow Then Exit Sub
    
    Dim urg As Range: Set urg = srg.Columns(uCol)
    Dim vrg As Range: Set vrg = srg.Columns(vCol)
    
    Dim dFormula As String
    dFormula = "=ROW(UNIQUE(FILTER(" & vrg.Address & ",ISNUMBER(SEARCH(" _
        & dfcAddress & "," & urg.Address & "))))"
    
    Dim dfCell As Range: Set dfCell = ws.Range(dfcAddress)
    With dfCell
        Dim dlCell As Range: Set dlCell = .Resize(ws.Rows.Count - .Row   1) _
            .Find("*", , xlFormulas, , , xlPrevious)
        If dlCell Is Nothing Then Exit Sub
        Dim drg As Range: Set drg = ws.Range(dfCell, dlCell).Offset(, 1)
        drg.Formula = dFormula ' write formula
        drg.Value = drg.Value ' convert to values
    End With
     
End Sub

CodePudding user response:

Just use CountIf with asterisks (*) in front of and at the end of the search expression...

e.g. Assuming "Alpha" is in cell D1...

=COUNTIF(C:C,"*" & D1 & "*")

...will return the number of rows that have "Alpha" in them.

CodePudding user response:

Try this?

E1: "Unique Types"

E2: =UNIQUE(B2:B15)

F1: "All Letters"

F2: =IF(E2="","",CONCAT(IF(B$2:B$15=E2,C$2:C$15 & ", ","")))

(drag F2 down)

... and now you have a format where @john-joseph's answer works! ...

H1: "Letter"

H2:H15: "Alpha", "Beta", etc... I don't know how to do this via formula but I'm assuming it's a fixed set of letters.

I1: "Count"

I2: =COUNTIF(F$2:F$6,"*"&H2&"*")

(drag I2 down)

  • Related