Caerus
Caerus

Reputation: 674

VBA Dictionaries

I'm trying to create a dictionary of key:value pairs in VBA. The keys will be account IDs and the values should be lists of integers. Given the following data:

enter image description here

the dictionary should look like this: {'123':[3,5],'456':[4,7],'789':[6]}

The array used as a value in the key:value pair will be dynamic; we won't know in advance how large it will be. I'm much more familiar with how to do this in Python, but not VBA, which requires dynamic resizing of arrays. Here's my code so far:

Sub test()
    Dim dict As New Scripting.Dictionary
    Dim sht As Worksheet
    Set sht = Sheets("Sheet1")
    Dim x As Integer
    Dim accountID As Variant
    Dim transaction As Variant

    For x = 2 To 5
        accountID = sht.Cells(x, 1).Value
        transaction = sht.Cells(x, 2).Value
        'Test whether the account exists in the dictionary
        'If the account does not exist, add it along with its transaction value
        If Not dict.Exists(accountID) Then
            Dim arr() As Variant
            arr(0) = transaction
            dict.Add accountID, arr
        'If the account exists, update its value array to include the transaction value
        ElseIf dict.Exists(accountID) Then
            arrLen = UBound(arr) - LBound(arr) + 1
            ReDim Preserve arr(arrLen + 1)
            dict(accountID)(arrLen + 1) = transaction
        End If
    Next x
End Sub

It's bugging on the portion that modifies the array. How should this be changed?

Upvotes: 0

Views: 479

Answers (1)

Tom
Tom

Reputation: 9878

Arrays stored as items in a dictionary don't like being directly written to. You need to write the dictionary item to a temporary array before setting the item back to the updated array.

Sub test()
    Dim dict As New Scripting.Dictionary
    Dim sht As Worksheet
    Dim x As Long
    Dim accountID As Variant, transaction As Variant
    Dim arr As Variant

    Set sht = Sheets("Sheet1")

    With sht
        For x = 2 To 6
            ReDim arr(0)
            accountID = .Cells(x, 1).Value2
            transaction = .Cells(x, 2).Value2
            With dict
                If Not .Exists(accountID) Then
                    ReDim arr(0)
                    arr(0) = transaction
                    .Add Key:=accountID, Item:=arr
                Else
                    arr = .Item(accountID)
                    ReDim Preserve arr(LBound(arr) To UBound(arr) + 1)
                    arr(UBound(arr)) = transaction
                    dict(accountID) = arr
                End If
            End With
        Next x
    End With


    ' Read back dictionary and array
    Dim k
    Dim dictStr As String
    dictStr = "{"
    For Each k In dict.Keys
        Debug.Print k, Join(dict(k), ", ")
        dictStr = dictStr & "'" & k & "':[" & Join(dict(k), ",") & "],"
    Next k
    dictStr = Left(dictStr, Len(dictStr) - 1) & "}"
    MsgBox dictStr

End Sub

This may be a more efficient way of achieving the same thing. This uses a couple of tricks using Index and Filter to Slice the arrays and filter a 2D array

Sub test()
    Dim arr As Variant, aKeys As Variant, aItems As Variant
    Dim dict As New Scripting.Dictionary
    Dim posArr As String
    Dim i As Long, k As Long

    With Sheets("Sheet1")
        arr = .Range(.Cells(2, 1), .Cells(.Cells(.Rows.Count, 1).End(xlUp).Row, 2))
    End With

    With Application
        ' Separates the arr array into two 1D arrays
        aKeys = .Transpose(.Index(arr, 0, 1))
        aItems = .Transpose(.Index(arr, 0, 2))

        ' Loops through aKeys array
        For i = LBound(aKeys) To UBound(aKeys)
            ' Reset the posArr string, this is used for indexing the arrays at the correct locations
            posArr = vbNullString
            ' Test if key already exists in dictionary. As we are getting all the possible values in one go we don't
            ' need to worry about if it does exist. Testing here means we skip the second loop if not needed
            If Not dict.Exists(aKeys(i)) Then
                ' Loop through aKeys to find all the positions of the items we want in the array
                For k = LBound(aKeys) To UBound(aKeys)
                    ' If the key in position k is equal to the key in position i then add it to posArr string
                    If aKeys(k) = aKeys(i) Then
                        ' We will late split this into an array itself on the "," delimiter
                        posArr = posArr & k & ","
                    End If
                Next k
                ' Add our key and all our matching items to the dictionary
                ' Here we split the posArr string on the "," delimiter and index the aItems array. This returns
                ' All the values from aItems that have a matching position in the aKeys array.
                ' For this to work it aKeys and aItems must have the exact same size (i.e. 1D and matching length)
                dict.Add Key:=aKeys(i), Item:=.Index(aItems, Split(Left(posArr, Len(posArr) - 1), ","))
            End If
        Next i
    End With


    ' Read back the dictionary
    ' Delete this and update with whatever you want to do with your data - This is currently just a check everything
    ' has worked as expected
    Dim dictKey
    Dim dictStr As String
    dictStr = "{"
    For Each dictKey In dict.Keys
        dictStr = dictStr & vbNewLine & vbTab & "'" & dictKey & "':[" & Join(dict(dictKey), ",") & "],"
    Next dictKey
    dictStr = Left(dictStr, Len(dictStr) - 1) & vbNewLine & "}"
    MsgBox dictStr
End Sub

Upvotes: 1

Related Questions