VBA Dictionary behaving wildly - vba

I am having a lot of trouble understanding the output for the below code snippet
Sub TestDictionary
Dim d as dictionary
set d = new dictionary
debug.print d.count
debug.print d(1)
debug.print d.count
End Sub
The above snippet gives the o/p as below
0
`I presume this line being the empty string
1
I expected subscript out of range for line debug.print d(1), But to my horror it returned an empty string.
Can anyone enlighten me why this is happening?

That's the correct behavior. The Scripting.Dictionary object is made in such a way that dict(x) = something either assigns to an existing entry or creates the entry if none.
Also reading dict(x) creates the entry with an empty Variant if no entry already exists.
This is how the Scripting.Dictionary is specified, and this behavior is useful in many situations.
Notice that you can change this behavior in your code, by simply checking if the entry exists before accessing it:
If dict.exists(x) then
do something with dict(x)...
End If
So your code above could be written this way:
Sub TestDictionary
Dim d as dictionary
set d = new dictionary
debug.print d.count
if d.Exists("1") then Debug.Print d("1")
debug.print d.count
End Sub
Also notice that the key is a String. You dont expect to have the integer as an index like an array. d(1) is just an entry that could be placed anywhere, with the key string "1".

It's because with d(1) you're directly accessing the dictionary item corresponding to "1" key and if there's no such key then VBScript creates it under the hood. That's why the subsequent d.count returns 1
You can have some deeper testing as follows:
Sub TestDictionary()
Dim d As Dictionary
Set d = New Dictionary
Debug.Print d.count '--> returns 0
Debug.Print d.keys(0) '--> returns an error, there are no keys, yet
Debug.Print d(4) '--> returns "", i.e. the not defined item associated with the newly created key (which is "4")
Debug.Print d.keys(0) '--> returns "4", i.e. the first (and only) dictionary key
Debug.Print d.keys(1) '--> returns an error, since there's only one item in the dictionary
Debug.Print d.Exists(1) '--> returns False, since there's no "1" key in the dictionary
Debug.Print d.Exists(4) '--> returns True, since there's a "4" key in the dictionary
Debug.Print d.count '--> 1, since the third statement created a dictionary item
End Sub
Bottom Line: Use Dictionary Count property to know if it has any item and Exist(key) property if you're looking for a specific key

Related

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

Wildcard search of dictionary

After searching google and SO, I see that there is a way for me to search a dictionary for an existing key:
dict.exists("search string")
My question is how can I search a dictionary using a wildcard:
dict.exists("search*")
I want to search the dictionary for a term first because my macro has the user select a group of files (the file name as dictionary key and the full path as the value) and I want to determine if files of a certain naming convention are present in the group BEFORE I iterate the dictionary elements to apply the file processing.
If a certain naming convention is found, X processing is run on each file in the dictionary instead of Y processing. The trick is that if ANY of the elements follow the certain naming convention, then they all need to be processed accordingly. That is to say, if elements 1-19 fail to meet the convention but 20 passes, then all elements 1-20 need specific processing. This is the reason I cant just check each name as I go and process selectively one file at a time.
My current solution is to iterate the entire dictionary once searching for the naming convention, then reiterating the dictionary after I know which method to use in processing the files. I am looping through all the elements twice and that doesn't seem efficient...
Do you guys have a reasonable solution for wildcard searching the dictionary keys?
The Dictionary Items method returns an array of all the items. You can Join those into a big string then use Instr() to determine if your search string is in the big string.
From your example, you have the asterisk at the end, so I'm assuming you care how an item starts, not that a sub-string exists anywhere. So I look for delimiter+substring and add the delimiter to the front of the Join (for the sake of the first item). If you have different requirements, you'll have to adjust, but the theory is the same.
I used two pipes as a delimiter because it's unlikely to be in the data and return a false positive. That may not be appropriate for your data.
Public Function WildExists(ByRef dc As Scripting.Dictionary, ByVal sSearch As String) As Boolean
Const sDELIM As String = "||"
WildExists = InStr(1, sDELIM & Join(dc.Keys, sDELIM), sDELIM & sSearch) > 0
End Function
test code
Sub Test()
Dim dc As Scripting.Dictionary
Set dc = New Scripting.Dictionary
dc.Add "Apple", "Apple"
dc.Add "Banana", "Banana"
dc.Add "Pear", "Pear"
Debug.Print WildExists(dc, "App") 'true
Debug.Print WildExists(dc, "Ora") 'false
End Sub
You can use Filter combined with the array of dictionary keys to return an array of matching keys.
Function getMatchingKeys(DataDictionary As Dictionary, MatchString As String, Optional Include As Boolean = True, Optional Compare As VbCompareMethod = vbTextCompare) As String()
getMatchingKeys = Filter(DataDictionary.Keys, MatchString, Include, Compare)
End Function
Here are some examples of what can be done when you apply a filter to the dictionary's keys.
Option Explicit
Sub Examples()
Dim dict As Dictionary
Dim arrKeys() As String
Dim key
Set dict = New Dictionary
dict.Add "Red Delicious apples", 10
dict.Add "Golden Delicious Apples", 5
dict.Add "Granny Smith apples", 66
dict.Add "Gala Apples", 20
dict.Add "McIntosh Apples", 30
dict.Add "Apple Pie", 40
dict.Add "Apple Sauce", 50
dict.Add "Anjuo Pears", 60
dict.Add "Asian Pears", 22
dict.Add "Bartlett Pears", 33
dict.Add "Bosc Pears", 44
dict.Add "Comice Pears", 3
arrKeys = getMatchingKeys(dict, "Apple")
Debug.Print "Keys that contain Apple"
Debug.Print Join(arrKeys, ",")
Debug.Print
arrKeys = getMatchingKeys(dict, "Apple", False)
Debug.Print "Keys that do not contain Apple"
Debug.Print Join(arrKeys, ",")
Debug.Print
arrKeys = getMatchingKeys(DataDictionary:=dict, MatchString:="Apple", Include:=True, Compare:=vbBinaryCompare)
Debug.Print "Keys that contain matching case Apple"
Debug.Print Join(arrKeys, ",")
Debug.Print
arrKeys = getMatchingKeys(DataDictionary:=dict, MatchString:="Pears", Include:=True, Compare:=vbTextCompare)
Debug.Print "We can also use the array of keys to find the values in the dictionary"
Debug.Print "We have " & (UBound(arrKeys) + 1) & " types of Pears"
For Each key In arrKeys
Debug.Print "There are " & dict(key) & " " & key
Next
End Sub
Output:
this method can help you with wildcard searching in Dictionary
Sub test()
Dim Dic As Object: Set Dic = CreateObject("Scripting.Dictionary")
Dim KeY, i&: i = 1
For Each oCell In Range("A1:A10")
Dic.Add i, Cells(i, 1).Value: i = i + 1
Next
For Each KeY In Dic
If LCase(Dic(KeY)) Like LCase("Search*") Then
MsgBox "Wildcard exist!"
Exit For
End If
Next
End Sub
If you want to use a wildcard to search in dictionary keys you can use the method [yourdictionary].Keys and the function Application.Match
For example:
Dim position As Variant 'It will return the position for the first occurrence
position = Application.Match("*Gonzalez", phoneBook.Keys, 0)
If phoneBook has Keys: (JuanCarlos, LuisGonzalez, PedroGonzalez)
It will return the position for LuisGonzalez

Keeping a count in a dictionary, bad result when running the code, good result adding inspections

Weird problem. Stepping through the code with inspections gives me correct answers. Just running it doesn't.
This program loops through each cell in a column, searching for a regex match. When it finds something, checks in a adjacent column to which group it belongs and keeps a count in a dictonary. Ex: Group3:7, Group5: 2, Group3:8
Just stepping through the code gives me incorrect results at the end, but adding and inspection for each known item in the dictionary does the trick. Using Debug.Print for each Dictionary(key) to check how many items I got in each loop also gives me a good output.
Correct // What really hapens after running the code
Group1:23 // Group1:23
Group3:21 // Group3:22
Group6:2 // Group6:2
Group7:3 // Group7:6
Group9:8 // Group9:8
Group11:1 // Group11:12
Group12:2 // Group12:21
Sub Proce()
Dim regEx As New VBScript_RegExp_55.RegExp
Dim matches
Dim Rango, RangoJulio, RangoAgosto As String
Dim DictContador As New Scripting.Dictionary
Dim j As Integer
Dim conteo As Integer
Dim Especialidad As String
regEx.Pattern = "cop|col"
regEx.Global = False 'True matches all occurances, False matches the first occurance
regEx.IgnoreCase = True
i = 3
conteo = 1
RangoJulio = "L3:L283"
RangoAgosto = "L3:L315"
Julio = Excel.ActiveWorkbook.Sheets("Julio")
Rango = RangoJulio
Julio.Activate
For Each celda In Julio.Range(Rango)
If regEx.Test(celda.Value) Then
Set matches = regEx.Execute(celda.Value)
For Each Match In matches
j = 13 'column M
Especialidad = Julio.Cells(i, j).Value
If (Not DictContador.Exists(Especialidad)) Then
Call DictContador.Add(Especialidad, conteo)
GoTo ContinueLoop
End If
conteo = DictContador(Especialidad)
conteo = CInt(conteo) + 1
DictContador(Especialidad) = conteo
Next
End If
ContinueLoop:
i = i + 1
'Debug.Print DictContador(key1)
'Debug.Print DictContador(key2)
'etc
Next
'Finally, write the results in another sheet.
End Sub
It's like VBA saying "I'm going to dupe you if I got a chance"
Thanks
Seems like your main loop can be reduced to this:
For Each celda In Julio.Range(Rango)
If regEx.Test(celda.Value) Then
Especialidad = celda.EntireRow.Cells(13).Value
'make sure the key exists: set initial count=0
If (Not DictContador.Exists(Especialidad)) Then _
DictContador.Add Especialidad, 0
'increment the count
DictContador(Especialidad) = DictContador(Especialidad) +1
End If
Next
You're getting different results stepping through the code because there's a bug/feature with dictionaries that if you inspect items using the watch or immediate window the items will be created if they don't already exist.
To see this put a break point at the first line under the variable declarations, press F5 to run to the break point, then in the immediate window type set DictContador = new Dictionary so the dictionary is initialised empty and add a watch for DictContador("a"). You will see "a" added as an item in the locals window.
Collections offer an alternative method that don't have this issue, they also show values rather than keys which may be more useful for debugging. On the other hand an Exists method is lacking so you would either need to add on error resume next and test for errors instead or add a custom collection class with an exists method added. There are trade-offs with both approaches.

Finding the key corresponding to an item in a dictionary

Is there any way to find the key that corresponds to a given item in a VBA dictionary?
http://msdn.microsoft.com/en-us/library/aa164502%28v=office.10%29.aspx
MSDN suggests that the "Key" property can be used, but when I try using it I get an error ("Compile error: invalid use of property"). I've found in the past that the "Exists" method given here doesn't work for me either, so I assume that they were the commands in a previous version of Office and are now outdated. However I haven't been able to find an equivalent for the latest version of Office.
I could use a for each loop to create a new dictionary where the keys in the old dictionary are the items in the new dictionary (and vice versa) and then use ".Item", but I was wondering if there was an inbuilt command that would allow me to avoid this.
but I was wondering if there was an inbuilt command that would allow me to avoid this.
Nope there is no inbuilt command as such. You will have to resort to some kind of looping. Here is one example. I created a small function to get the key corresponding to an item in a dictionary.
Dim Dict As Dictionary
Sub Sample()
Set Dict = New Dictionary
With Dict
.CompareMode = vbBinaryCompare
For i = 1 To 10
.Add i, "Item " & i
Next i
End With
Debug.Print GetKey(Dict, "Item 3")
End Sub
Function GetKey(Dic As Dictionary, strItem As String) As String
Dim key As Variant
For Each key In Dic.Keys
If Dic.Item(key) = strItem Then
GetKey = CStr(key)
Exit Function
End If
Next
End Function
An alternate solution(..?)
Instead of going through each item in the dictionary for a match, how about you maintain 2 dictionary objects? The second one, using value as the key and key as its value. When u add an item, u add it both the dictionaries. If you have a key, you look it up in the first dictionary and if you have the value, u look it up in the second one.
Actually, there is an Exists method that will do exactly what you want. Here's how it works:
...
Dim dict
Set dict = CreateObject("Scripting.Dictionary")
dict.Add "utensil", "spork"
Debug.Print dict.Exists("utensil")
The above returns True.
From this, I first thought that .exists(key) is enterely useless.
But there IS an easy circumvention.
First, let me mention a futile attempt:
make sure the first time you refer to the value to check, you assign
.exists(key) to a boolean variable.
if the value of that boolean is FALSE, immediately remove the dictionary
entry that was inadvertently created when you tested the key
Well, that works, but the next time you test for existence again with this code
itExists = a.exists(key)
you may get a 424 error -- the implementor of .exists REALLY failed. But the following will work (or at least, for me it does... so far)
if isempty(a.item(key)) then ' checking on value of the object
a.remove(key)
a.add key, value
else ' you have a key duplicate
' do something about dupe, like quit
end if
For a little clarification, you can look at the following example code below
Sub aDict()
Dim a As Dictionary
Dim x As Long
Set a = New Dictionary
With a
On Error Resume Next
' first add
.Add 1, "sumpn"
' second add
.Add "dog", "beagle"
x = 66
' third add
.Add "sixty", x
printd a, 1, "added with numerical key"
printd a, 2, "added with string key = dog, using numeric key=2"
Stop ' look at count of items: we added 3, but have 4 in local vars
printd a, "2", "searching string key '2', not retrieving 2nd added"
printd a, 9, "should not exist, first try"
' but the .exists has created it!!
printd a, 9, "should not exist, second try, *** but now created ***"
printd a, 8, "never seen anywhere"
Stop ' look at a in local vars!! #8 exists now as item 7
a.Remove 8 ' so we kill it
' *************************** the great fixit *******
Stop ' observe that #8 (item 7) is gone again
printd a, "dog", "added as second position (Item 2)"
' fourth add
.Add 1, "else" ' doublette
printd a, 1, "position 1 is taken, Err=457 is correct"
' fifth add
.Add 3, "beagle"
printd a, "3", "string key='3' <> numeric 3"
' 6th add
.Add 5, "beagle"
printd a, "beagle", "value is already there with key 'dog'"
printd a, 5, "numeric key=5"
End With
End Sub
Sub printd(a As Dictionary, mkey, Optional msg As String)
Dim ex As Boolean
With a
If Err.number <> 0 Then
Debug.Print mkey, "error " & Err.number, Err.Description
End If
Err.clear
ex = .Exists(mkey) ' very first reference to a.Exists(mkey)
Debug.Print "key " & mkey, "a(" & mkey & ")" & a(mkey), _
"Exists", ex, "a.item " & .Item(mkey), msg
If Err.number <> 0 Then
Debug.Print mkey, "error " & Err.number, Err.Description
End If
End With
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.