Wildcard search of dictionary - vba

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

Related

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

Some sort of meta data for Dictionary

I have set up a dictionary with a long list of keys and associated items. The user enters a string of keywords via an input box and vba splits each word into an array, finds the key and adds the item to an output string. For example:
User types in "Dinner Applebee's Restaurant", the code splits the string into three words, then makes a number code where each word is represented by a two-digit number "10 08 70" (spaces added to emphasize number schema).
Here's the problem, "Applebee's Restaurant" flows a little better in English than "Restaurant Applebee's" so the user would most likely type the words in that order but the number should really reflect "Restaurant Applebee's." Like it should be category then place instead of place then category. The number code should actually be "10 70 08."
Is there a way to group dictionary definitions without making a separate dictionary? Or evaluate each word and sort them into a particular order? As in the first two digits will always be greater than 10, the second two digits will also be greater than 10 or will be 00. The third two digits will always be less than 10. So as long as I know what word has less than 10, that number can be last.
It would be easy enough to just type the words in the correct order, but I fear the lay person hardly ever takes the easy route and there is a number of people who will use this, each at their own questionable level of computer-literacy.
An example of code was requested so here it is:
Dim dict As New Scripting.Dictionary
Dim InStr As String ' Input string
Dim Split() As String ' Array to hold words
Dim SegNum As String ' Output string
Dim I As Integer
' Adding searchable items to dictionary
' First two digits
dict.Add Key:="Dinner", Item:=10
dict.Add Key:="Lunch", Item:=15
dict.Add Key:="Breakfast", Item:=20
dict.Add Key:="Snack", Item:=50
' Second two digits
dict.Add Key:="Restaurant", Item:=70
dict.Add Key:="Home Cooked", Item:=80
' Third two digits
dict.Add Key:="Home", Item:=Format(0, "00)
dict.Add Key:="McDonald's", Item:=Format(1, "00")
dict.Add Key:="Burger King", Item:=Format(2,"00")
dict.Add Key:="Wendy's", Item:=Format(3, "00")
..
dict.Add Key:="Applebee's", Item:=Format(8, "00")
SplitStr = Split(InputBox("Please use some keywords to detail what you ate"), " ")
For I = LBound(SplitStr) To UBound(SplitStr)
SegNum = SegNum & dict(SplitStr(I))
Next
MsgBox SegNum
Here's an approach. You'd need to add in checks for two entries from the same category though.
And you have "home cooked" as a value - that will get broken up by Split()
Sub Tester()
Dim dict As New Scripting.Dictionary
Dim InStr As String ' Input string
Dim SplitStr() As String ' Array to hold words
Dim SegNum As String ' Output string
Dim I As Long, srt As Long, wd
dict.CompareMode = TextCompare
srt = 1
dict.Add Key:="Dinner", Item:=Array(10, srt)
dict.Add Key:="Lunch", Item:=Array(15, srt)
dict.Add Key:="Breakfast", Item:=Array(20, srt)
dict.Add Key:="Snack", Item:=Array(50, srt)
' Second two digits
srt = 2
dict.Add Key:="Restaurant", Item:=Array(70, srt)
dict.Add Key:="HomeCooked", Item:=Array(80, srt)
' Third two digits
srt = 3
dict.Add Key:="Home", Item:=Array(0, srt)
dict.Add Key:="McDonald's", Item:=Array(1, srt)
dict.Add Key:="Burger King", Item:=Array(2, srt)
dict.Add Key:="Wendy's", Item:=Array(3, srt)
dict.Add Key:="Applebee's", Item:=Array(8, srt)
SegNum = String(6, " ")
SplitStr = Split("dinner home homecooked")
'SplitStr = Split(InputBox("Please use some keywords to detail what you ate"), " ")
For I = LBound(SplitStr) To UBound(SplitStr)
wd = Trim(SplitStr(I))
If dict.Exists(wd) Then
srt = dict(wd)(1)
Mid(SegNum, 1 + ((srt - 1) * 2), 2) = Format(dict(wd)(0), "00")
End If
Next
MsgBox SegNum
End Sub

VBA Dictionary behaving wildly

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

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

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.