Can I loop through key/value pairs in a VBA collection? - vba

In VB.NET, I can iterate through a dictionary's key/value pairs:
Dictionary<string, string> collection = new Dictionary<string, string>();
collection.Add("key1", "value1");
collection.Add("key2", "value2");
foreach (string key in collection.Keys)
{
MessageBox.Show("Key: " + key + ". Value: " + collection[key]);
}
I know in VBA I can iterate through the values of a Collection object:
Dim Col As Collection
Set Col = New Collection
Dim i As Integer
Col.Add "value1", "key1"
Col.Add "value2", "key2"
For i = 1 To Col.Count
MsgBox (Col.Item(i))
Next I
I also know that I do this with a Scripting.Dictionary VBA object, but I was wondering if this is possible with collections.
Can I iterate through key/value pairs in a VBA collection?

you cannot retrieve the name of the key from a collection. Instead, you'd need to use a Dictionary Object:
Sub LoopKeys()
Dim key As Variant
'Early binding: add reference to MS Scripting Runtime
Dim dic As Scripting.Dictionary
Set dic = New Scripting.Dictionary
'Use this for late binding instead:
'Dim dic As Object
'Set dic = CreateObject("Scripting.Dictionary")
dic.Add "Key1", "Value1"
dic.Add "Key2", "Value2"
For Each key In dic.Keys
Debug.Print "Key: " & key & " Value: " & dic(key)
Next
End Sub

This answwer is not iterating over keys of a collection - which seems to be impossible, but gives some more workarounds if you do not want to use a Dictionary.
You can do a collection of KeyValues as outlined in https://stackoverflow.com/a/9935108/586754 . (Create keyvalue class and put those into the collection.)
In my (non Excel but SSRS) case I could not add a class and did not want to add a .net reference.
I used 2 collections, 1 to store keys and 1 to store values, and then kept them in sync when adding or deleting.
The following shows the add as an example - though it is limited to string/int key/value, and the int value s not stored but added to previous values, which was needed for me aggregating values in SSRS. This could be easily modified though to not add but store values.
ck key collection, cv value collection.
Private Sub StoreAdd(ck As Collection, cv As Collection, k As String, v As Integer)
Dim i As Integer
Dim found As Boolean = false
Dim val As Integer = v
For i = 1 to ck.Count
if k = ck(i)
' existing, value is present
val = val + cv(i)
' remove, will be added later again
ck.Remove(i)
cv.Remove(i)
End If
if i <= ck.Count
' relevant for ordering
If k > ck(i)
' insert at appropriate place
ck.Add(k, k, i)
cv.Add(val, k, i)
found = true
Exit For
End If
End If
Next i
if not found
' insert at end
ck.Add(k, k)
cv.Add(val, k)
End If
End Sub

Related

Test if Item of a VBA dictionary is equal to a given value without adding key and item to the dictionary

I need to test if the Item of a VBA Dictionary is equal to a given value, without adding a new key and value to the dictionary.
dict.RemoveAll
MsgBox dict.Exists(key)
MsgBox dict.Exists(key) returns false, the dict is empty.
var = "Hello"
MsgBox var = dict(key)
The MsgBox returns false, because var and the item returned by dict(key) is not equal. But when I again check if the key exists in the dictionary, dict.Exists(key) now returns true.
MsgBox dict.Exists(keys)
Seems to me the equal-Operator not only compares var with the item, but also assigns a new key and item pair to the dict, but i need the dictionary still to be empty. How can i achieve this?
Use exists() to check if a key exists:
Sub foo()
Dim dict As Scripting.Dictionary
Dim key As String
key = "key1"
Set dict = New Dictionary
Debug.Print dict(key) ' -- create a value of Empty for the key
Debug.Print dict.Count ' -- 1 (yikes!)
Set dict = New Dictionary
Debug.Print dict.Exists(key) ' -- no side effects
Debug.Print dict.Count '-- 0 (great!)
End Sub
See also (thanks to Cameron Critchlow):
scripting-dictionary-lookup-add-if-not-present-with-only-one-key-search
Note in your problem you are creating a variable then checking if it is equal to the value of some key (if that key exists!) - so by extension using the method described above, first check if the key exists, then check if it is the same as your variable:
Dim other_key As String
Dim result As Boolean
other_key = "key2"
If dict.Exists(key) Then
If other_key = dict(key) Then
result = True
End If
End If
Debug.Print result
Something along these lines will test all keys and items for a match, while not adding any extra keys.
Dim Dict As Object
Set Dict = CreateObject("Scripting.Dictionary")
Dict.Add "test", "this"
Dim Var As String
Dim var2 As String
Var = "test"
var2 = "this"
Dim Key As Variant
For Each Key In Dict
Debug.Print "var - item", Var = Dict(Key)
Debug.Print "var - key", Var = Key
Debug.Print "var2 - item", var2 = Dict(Key)
Debug.Print "var2 - key", var2 = Key
Next Key

Removing Specific value (or item) from Key in Scripting Dictionary

I am comparing two VBA scripting dictionaries. Particularly, I want to know if the keys that have the same name (in this example, "Dogs") also have the same values/items assigned to them. If there is a mismatch (one key has more items than the other), I want to know where the difference comes from.
In this example, I have two identically named keys in two scripting dictionaries, but one has 3 values and the other has 4.
I want to see which values ("Mixed" and "Cat") are missing from the key in the first dictionary. I then want to make a string of the values that are missing.
Set Dictionary1 = CreateObject("scripting.dictionary")
Set Dictionary2 = CreateObject("scripting.dictionary")
Dictionary1.Add "Dogs", Array("Beagle", "Setter", "Chiuhuaha")
Dictionary2.Add "Dogs", Array("Beagle", "Setter", "Chiuhuaha", "Mixed", "Cat")
Objective:
MissingItems = Mixed &" "& Cat
MsgBox "The missing items in Dogs are" & MissingItems
Does anyone have an idea of how this could be achieved? I'd greatly appreciate it if you could suggest the code wording to use. I'm so stuck!
Try this:
Option Explicit
Sub Test()
Dim dictionary1 As Object: Set dictionary1 = CreateObject("scripting.dictionary")
Dim dictionary2 As Object: Set dictionary2 = CreateObject("scripting.dictionary")
dictionary1.Add "Dogs", Array("Beagle", "Setter", "Chiuhuaha")
dictionary2.Add "Dogs", Array("Beagle", "Setter", "Chiuhuaha", "Mixed", "Cat")
Const myKey As String = "Dogs"
'Exit if key is missing from any of the dictionaries
If Not dictionary1.Exists(myKey) Then Exit Sub
If Not dictionary2.Exists(myKey) Then Exit Sub
Dim elements1 As Object: Set elements1 = CreateObject("scripting.dictionary")
Dim v As Variant
Dim missingElements As Object: Set missingElements = CreateObject("scripting.dictionary")
'Create another dictionary with the elements of the first array
For Each v In dictionary1(myKey)
elements1(v) = Empty 'This creates the key if missing and makes sure you don't have duplicates
Next v
'Check all missing elements from the second array
For Each v In dictionary2(myKey)
If Not elements1.Exists(v) Then
missingElements(v) = Empty
End If
Next v
If missingElements.Count = 0 Then
MsgBox "No items missing in " & myKey, vbInformation, "Result"
Else
MsgBox "The missing items in " & myKey & " are: " & Join(missingElements.Keys, " ")
End If
End Sub

VBA in For Each oItem Loop, how can i access the oItem's id?

I am going to write some code to illustrate the question.
For Each oElement in myArray
MsgBox oElement
Next
This would print a message saying the value of "oElement" contained in "myArray" as many times as there is elements in "myArray".
However, what if i want to know the id of "oElement"? is there properties of "oElement" that i can access? something like printing the number of oelement instead of the value of the oelement?
For Each oElement in myArray
MsgBox oElement.ID
Next
Is it possible? is there properties that can be accessed?
Thanks in advance for your time and attention,
No, there's no way to get the index of the item in the array. You have to maintain a separate variable:
Dim Index As Integer
Index = 0
For Each oElement In myArray
Print Index
Index = Index + 1
Next
a workaround could be the use of a Dictionary object instead of a Variant array:
Sub main()
Dim myDict As Scripting.Dictionary
Dim key As Variant
Set myDict = GetDict '<--| get your "test" dictionary with "indexes" as 'keys' and "elements" as 'items'
For Each key In myDict.Keys '<--| iterate over keys (i.e. over your "indexes")
MsgBox key '<--| this will give you the "index"
MsgBox myDict(key) '<--| this will give you the "element"
Next key
End Sub
where it's used the following function to return a "test" dictionary
Function GetDict() As Scripting.Dictionary
'function to return a test dictionary
Dim i As Long
Dim myDict As New Scripting.Dictionary
For i = 1 To 10
myDict.Add i, "string-" & CStr(i) '<--| use the 'key' as your "index" and the 'item' as your element
Next i
Set GetDict = myDict
End Function
In truth, the following reverse approach could seem more similar to your initial code:
Sub main()
Dim myDict As Scripting.Dictionary
Dim oElement As Variant
Set myDict = GetDict2 '<--| get your "test" dictionary with "elements" as 'keys' and "indexes" as 'items'
For Each oElement In myDict.Keys '<--| iterate over dictionary keys (i.e. over your "elements")
MsgBox myDict(oElement) '<--| this will give you the "index"
MsgBox oElement '<--| this will give you the "element"
Next oElement
End Sub
where the following function is used:
Function GetDict2() As Scripting.Dictionary
Dim i As Long
Dim myDict As New Scripting.Dictionary
For i = 1 To 10
myDict.Add "string-" & CStr(i), i '<--| use the 'key' as your "element" and the 'item' as your key
Next i
Set GetDict2 = myDict
End Function
but it'd have the major drawback of using your "elements" as keys, thus possibly violating their uniqueness, while sequential integers would always comply this requirement

Multi-dimensional Dictionary class in VBA

This post is half to share a solution and half to ask if there's a better way to do it.
Problem: how to build a multi-dimensional dictionary in VBA.
It seems there are people out there looking for one, but there isn't an obvious neat solution around so I came up with some code, as follows.
Specific case: convert an ADO Recordset into a Dictionary, where several columns comprise the unique key for a row. Adding multiple records to the same Dictionary fails unless you come up with a key that concatenates all the columns that comprise the unique key.
General case: model a tree structure in an object hierarchy where there might not be the same number of branches across every node at the same level in the hierarchy.
The code below solves both problems. Performance untested but the VBA Scripting library's Dictionary class is apparently indexed with a hash table and I've seen very large systems built with it, so I doubt performance will be an issue. Maybe one of the giant brains out there will correct me on this.
Put this into a VBA class called multiDictionary:
Option Explicit
' generic multi-dimensional dictionary class
' each successive higher dimension dictionary is nested within a lower dimension dictionary
Private pDictionary As Dictionary
Private pDimensionKeys() As Variant
Private Const reservedItemName As String = "multiItem"
Public Function add(value As Variant, ParamArray keys() As Variant)
Dim searchDictionary As Dictionary
Dim newDictionary As Dictionary
Dim count As Long
If pDictionary Is Nothing Then Set pDictionary = New Dictionary
Set searchDictionary = pDictionary
For count = LBound(keys) To UBound(keys)
If keys(count) = reservedItemName Then Err.Raise -1, "multiDictionary.add", "'" & reservedItemName & "' is a reserved key and cannot be used"
If searchDictionary.Exists(keys(count)) Then
Set newDictionary = searchDictionary.item(keys(count))
Else
Set newDictionary = New Dictionary
searchDictionary.add key:=keys(count), item:=newDictionary
End If
Set searchDictionary = searchDictionary.item(keys(count))
Next
' each node can have only one item, otherwise it has dictionaries as children
searchDictionary.add item:=value, key:=reservedItemName
End Function
Public Function item(ParamArray keys() As Variant) As Variant
Dim count As Long
Dim searchDictionary As Dictionary
Set searchDictionary = pDictionary
For count = LBound(keys) To UBound(keys)
' un-nest iteratively
Set searchDictionary = searchDictionary.item(keys(count))
Next
' the item always has the key 'reservedItemName' (by construction)
If IsObject(searchDictionary.item(reservedItemName)) Then
Set item = searchDictionary.item(reservedItemName)
Else
item = searchDictionary.item(reservedItemName)
End If
End Function
And test it like this
Sub testMultiDictionary()
Dim MD As New multiDictionary
MD.add "Blah123", 1, 2, 3
MD.add "Blah124", 1, 2, 4
MD.add "Blah1234", 1, 2, 3, 4
MD.add "BlahXYZ", "X", "Y", "Z"
MD.add "BlahXY3", "X", "Y", 3
Debug.Print MD.item(1, 2, 3)
Debug.Print MD.item(1, 2, 4)
Debug.Print MD.item(1, 2, 3, 4)
Debug.Print MD.item("X", "Y", "Z")
Debug.Print MD.item("X", "Y", 3)
End Sub

Does VBA have Dictionary Structure?

Does VBA have dictionary structure? Like key<>value array?
Yes.
Set a reference to MS Scripting runtime ('Microsoft Scripting Runtime'). As per #regjo's comment, go to Tools->References and tick the box for 'Microsoft Scripting Runtime'.
Create a dictionary instance using the code below:
Set dict = CreateObject("Scripting.Dictionary")
or
Dim dict As New Scripting.Dictionary
Example of use:
If Not dict.Exists(key) Then
dict.Add key, value
End If
Don't forget to set the dictionary to Nothing when you have finished using it.
Set dict = Nothing
VBA has the collection object:
Dim c As Collection
Set c = New Collection
c.Add "Data1", "Key1"
c.Add "Data2", "Key2"
c.Add "Data3", "Key3"
'Insert data via key into cell A1
Range("A1").Value = c.Item("Key2")
The Collection object performs key-based lookups using a hash so it's quick.
You can use a Contains() function to check whether a particular collection contains a key:
Public Function Contains(col As Collection, key As Variant) As Boolean
On Error Resume Next
col(key) ' Just try it. If it fails, Err.Number will be nonzero.
Contains = (Err.Number = 0)
Err.Clear
End Function
Edit 24 June 2015: Shorter Contains() thanks to #TWiStErRob.
Edit 25 September 2015: Added Err.Clear() thanks to #scipilot.
VBA does not have an internal implementation of a dictionary, but from VBA you can still use the dictionary object from MS Scripting Runtime Library.
Dim d
Set d = CreateObject("Scripting.Dictionary")
d.Add "a", "aaa"
d.Add "b", "bbb"
d.Add "c", "ccc"
If d.Exists("c") Then
MsgBox d("c")
End If
An additional dictionary example that is useful for containing frequency of occurence.
Outside of loop:
Dim dict As New Scripting.dictionary
Dim MyVar as String
Within a loop:
'dictionary
If dict.Exists(MyVar) Then
dict.Item(MyVar) = dict.Item(MyVar) + 1 'increment
Else
dict.Item(MyVar) = 1 'set as 1st occurence
End If
To check on frequency:
Dim i As Integer
For i = 0 To dict.Count - 1 ' lower index 0 (instead of 1)
Debug.Print dict.Items(i) & " " & dict.Keys(i)
Next i
Building off cjrh's answer, we can build a Contains function requiring no labels (I don't like using labels).
Public Function Contains(Col As Collection, Key As String) As Boolean
Contains = True
On Error Resume Next
err.Clear
Col (Key)
If err.Number <> 0 Then
Contains = False
err.Clear
End If
On Error GoTo 0
End Function
For a project of mine, I wrote a set of helper functions to make a Collection behave more like a Dictionary. It still allows recursive collections. You'll notice Key always comes first because it was mandatory and made more sense in my implementation. I also used only String keys. You can change it back if you like.
Set
I renamed this to set because it will overwrite old values.
Private Sub cSet(ByRef Col As Collection, Key As String, Item As Variant)
If (cHas(Col, Key)) Then Col.Remove Key
Col.Add Array(Key, Item), Key
End Sub
Get
The err stuff is for objects since you would pass objects using set and variables without. I think you can just check if it's an object, but I was pressed for time.
Private Function cGet(ByRef Col As Collection, Key As String) As Variant
If Not cHas(Col, Key) Then Exit Function
On Error Resume Next
err.Clear
Set cGet = Col(Key)(1)
If err.Number = 13 Then
err.Clear
cGet = Col(Key)(1)
End If
On Error GoTo 0
If err.Number <> 0 Then Call err.raise(err.Number, err.Source, err.Description, err.HelpFile, err.HelpContext)
End Function
Has
The reason for this post...
Public Function cHas(Col As Collection, Key As String) As Boolean
cHas = True
On Error Resume Next
err.Clear
Col (Key)
If err.Number <> 0 Then
cHas = False
err.Clear
End If
On Error GoTo 0
End Function
Remove
Doesn't throw if it doesn't exist. Just makes sure it's removed.
Private Sub cRemove(ByRef Col As Collection, Key As String)
If cHas(Col, Key) Then Col.Remove Key
End Sub
Keys
Get an array of keys.
Private Function cKeys(ByRef Col As Collection) As String()
Dim Initialized As Boolean
Dim Keys() As String
For Each Item In Col
If Not Initialized Then
ReDim Preserve Keys(0)
Keys(UBound(Keys)) = Item(0)
Initialized = True
Else
ReDim Preserve Keys(UBound(Keys) + 1)
Keys(UBound(Keys)) = Item(0)
End If
Next Item
cKeys = Keys
End Function
The scripting runtime dictionary seems to have a bug that can ruin your design at advanced stages.
If the dictionary value is an array, you cannot update values of elements contained in the array through a reference to the dictionary.
Yes. For VB6, VBA (Excel), and VB.NET
All the others have already mentioned the use of the scripting.runtime version of the Dictionary class. If you are unable to use this DLL you can also use this version, simply add it to your code.
https://github.com/VBA-tools/VBA-Dictionary/blob/master/Dictionary.cls
It is identical to Microsoft's version.
If by any reason, you can't install additional features to your Excel or don't want to, you can use arrays as well, at least for simple problems.
As WhatIsCapital you put name of the country and the function returns you its capital.
Sub arrays()
Dim WhatIsCapital As String, Country As Array, Capital As Array, Answer As String
WhatIsCapital = "Sweden"
Country = Array("UK", "Sweden", "Germany", "France")
Capital = Array("London", "Stockholm", "Berlin", "Paris")
For i = 0 To 10
If WhatIsCapital = Country(i) Then Answer = Capital(i)
Next i
Debug.Print Answer
End Sub
VBA can use the dictionary structure of Scripting.Runtime.
And its implementation is actually a fancy one - just by doing myDict(x) = y, it checks whether there is a key x in the dictionary and if there is not such, it even creates it. If it is there, it uses it.
And it does not "yell" or "complain" about this extra step, performed "under the hood". Of course, you may check explicitly, whether a key exists with Dictionary.Exists(key). Thus, these 5 lines:
If myDict.exists("B") Then
myDict("B") = myDict("B") + i * 3
Else
myDict.Add "B", i * 3
End If
are the same as this 1 liner - myDict("B") = myDict("B") + i * 3. Check it out:
Sub TestMe()
Dim myDict As Object, i As Long, myKey As Variant
Set myDict = CreateObject("Scripting.Dictionary")
For i = 1 To 3
Debug.Print myDict.Exists("A")
myDict("A") = myDict("A") + i
myDict("B") = myDict("B") + 5
Next i
For Each myKey In myDict.keys
Debug.Print myKey; myDict(myKey)
Next myKey
End Sub
You can access a non-Native HashTable through System.Collections.HashTable.
HashTable
Represents a collection of key/value pairs that are organized based on
the hash code of the key.
Not sure you would ever want to use this over Scripting.Dictionary but adding here for the sake of completeness. You can review the methods in case there are some of interest e.g. Clone, CopyTo
Example:
Option Explicit
Public Sub UsingHashTable()
Dim h As Object
Set h = CreateObject("System.Collections.HashTable")
h.Add "A", 1
' h.Add "A", 1 ''<< Will throw duplicate key error
h.Add "B", 2
h("B") = 2
Dim keys As mscorlib.IEnumerable 'Need to cast in order to enumerate 'https://stackoverflow.com/a/56705428/6241235
Set keys = h.keys
Dim k As Variant
For Each k In keys
Debug.Print k, h(k) 'outputs the key and its associated value
Next
End Sub
This answer by #MathieuGuindon gives plenty of detail about HashTable and also why it is necessary to use mscorlib.IEnumerable (early bound reference to mscorlib) in order to enumerate the key:value pairs.