Printing the Name of Dictionaries inside a Collection - vba

I have created a collection which consists of several dictionaries.
As I try to loop through the collection to print the names of the dictionaries, I get the following error message: 450 - Wrong number of arguments or invalid property argument.
My code reads as follows:
First, create the individual dictionaries and add the data to them:
Dim Cows, Dogs, Goats As Object
Set Cows = CreateObject("scripting.dictionary")
Set Dogs = CreateObject("scripting.dictionary")
Set Goats = CreateObject("scripting.dictionary")
[...Load the dictionaries with the data...]
Once this is done, create the collection of dictionaries and start to loop through it to see the names of each dictionary (the result should give us "Cows, Dogs, Goats" in the immediate window):
Dim TotalAnimals As New Collection
TotalAnimals.Add Cows
TotalAnimals.Add Dogs
TotalAnimals.Add Swans
Here lies the problem:
Dim AnimalType As Variant
For Each AnimalType In TotalAnimals
Debug.Print AnimalType
Next AnimalType
Any help would be greatly appreciated!!

What you are trying to achieve is called 'Reflection'. Unfortunately, the VBA language does not have reflection so you cannot directly achieve what you want.
You could emulate what you want using a 'wrapper' class to allow a name to be associated with a specific dictionary.
The example below implements a simple wrapper class which allows the name to be set, but not changed, and exposes the scripting.dictionary via the Host property.
Class AnimalType
Option Explicit
Private Type Properties
Name As String
Host As Scripting.Dictionary
End Type
Private p As Properties
Private Sub Class_Initialize()
Set p.Host = New Scripting.Dictionary
End Sub
Public Property Get Name() As String
Name = p.Name
End Property
Public Property Let Name(ByVal ipName As String)
If VBA.Len(p.Name) = 0 Then
p.Name = ipName
Else
Err.Raise 17 ' Can't perform the requested action
End If
End Property
Public Property Get Host() As Scripting.Dictionary
Set Host = p.Host
End Property
Thus
Dim Cows, Dogs, Goats As Object
Set Cows = CreateObject("scripting.dictionary")
Set Dogs = CreateObject("scripting.dictionary")
Set Goats = CreateObject("scripting.dictionary")
would become
Edited 30 Jan 2020 to correct the code below
Dim Cows as AnimalType
Dim Dogs as AnimalType
Dim Goats as AnimalType
Set Cows = new AnimalType
Cows.name="Cows"
Set Dogs = New AnimalType
Dogs.Name="Dogs"
Set Goats = New AnimalType
Goats.Name="Goats"
and then
Dim myAnimalType As Variant
For Each myAnimalType In TotalAnimals
Debug.Print myAnimalType.Name
Next

Please adapt your code in the next way. You can give to the dictionary a Name (in fact a Collection key) when add it to Collection:
Sub testDictNameInCollection()
Dim Cows As Object, Dogs As Object, Goats As Object
Dim TotalAnimals As New Collection, i As Long, arrK
Set Cows = CreateObject("scripting.dictionary")
Set Dogs = CreateObject("scripting.dictionary")
Set Goats = CreateObject("scripting.dictionary")
'load here the dictionaries...
TotalAnimals.Add Cows, "Cows"
TotalAnimals.Add Dogs, "Dogs"
TotalAnimals.Add Goats, "Goats"
arrK = Array("Cows", "Dogs", "Goats")
For i = 0 To UBound(arrK)
Debug.Print TotalAnimals.item(arrK(i)).count
Next i
TotalAnimals.item("Cows").Add "Cow 1 ", "a lot of milk"
Debug.Print TotalAnimals("Cows").Items()(TotalAnimals("Cows").count - 1) 'last item of the "Cow" dictionary
End Sub
Since Scripting.Dictionary does not expose a Name property, you can use a class able to wrap a name, in order to use the object and its name:
Copy the next code in a class and name it "AnimalClass":
Option Explicit
Private dictName As String
Private dict As Object
Private Sub Class_Initialize()
Set dict = CreateObject("Scripting.Dictionary")
End Sub
Public Property Get Name() As String
Name = dictName
End Property
Public Property Let obj(dic As Object)
Set dict = dic
End Property
Public Property Let Name(strName As String)
dictName = strName
End Property
Public Property Get obj() As Object
Set obj = dict
End Property
Copy the next code in a standard module:
Sub testDictionaryName()
Dim Cows As Object, Dogs As Object, Goats As Object, i As Long
Dim TotalAnimals As New Collection, animT As AnimalClass
Set animT = New AnimalClass
Set Cows = CreateObject("scripting.dictionary")
For i = 1 To 2: Cows(i) = "Cows " & i: Next i 'load the dictionary
animT.obj = Cows: animT.Name = "Cows"
TotalAnimals.Add animT 'add the class in Collection
Set animT = New AnimalClass
Set Dogs = CreateObject("scripting.dictionary")
For i = 1 To 3: Dogs(i) = "Dog " & i: Next i
animT.obj = Dogs: animT.Name = "Dogs"
TotalAnimals.Add animT
Set animT = New AnimalClass
Set Goats = CreateObject("scripting.dictionary")
For i = 1 To 4: Goats(i) = "Goat " & i: Next i
animT.obj = Goats: animT.Name = "Goats"
TotalAnimals.Add animT
Dim myAnimalType As Variant
For Each myAnimalType In TotalAnimals
Debug.Print myAnimalType.Name, myAnimalType.obj.count, myAnimalType.obj.Items()(myAnimalType.obj.count - 1)
Next
End Sub

Related

Nested VBA collection of classes returning "Argument not optional" error

I am trying to represent the idea of nested classes with collections of child classes with the natural example of Grandmother - Married Daughters - Little kids case, so I have 3 classes as following:
' Class GrandMother:
Private pMarriedDaughter As Collection
Public Property Get MarriedDaughter() As Collection
MarriedDaughter = pMarriedDaughter
End Property
Public Property Set MarriedDaughter(C As Collection)
Set pMarriedDaughter = C
End Property
' Class MarriedMom:
Private pChildren As Collection
Public Property Get Children() As Collection ' ERROR HERE!
Children = pChildren
End Property
Public Property Set Children(C As Collection)
Set pChildren = C
End Property
' Child Class:
Private pName As String
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(s As String)
Let pName = s
End Property
And the Main Routine that tries to populate the classes:
Sub TestGrandMother()
' Create 3 Childs
Dim Child_1a As New Child: Child_1a.Name = "Bill"
Dim Child_1b As New Child: Child_1b.Name = "Sam"
Dim Child_2a As New Child: Child_2a.Name = "Sahar"
' Create 2 Married Daughters:
Dim Mamy1 As New MarriedMom
Dim Mamy2 As New MarriedMom
' Add the the children to the married daughters
Set Mamy1.Children = New Collection
Mamy1.Children.Add Child_1a
Mamy1.Children.Add Child_1b
Set Mamy2.Children = New Collection
Mamy2.Children.Add Child_2a
' Create Grandmother
Dim GrandMa As GrandMother: Set GrandMa = New GrandMother
Set GrandMa.MarriedDaughter = New Collection
GrandMa.MarriedDaughter.Add Mamy1
GrandMa.MarriedDaughter.Add Mamy2
' Now cycle childs Name and debug:
Dim aChild As New Child
For Each aChild In GrandMa.MarriedDaughter.Children
Debug.Print GrandMa.MarriedDaughter.Children.Name
Next aChild
End Sub
In both cases where that error occurs you need to use the Set keyword as working with an object. That is just for the error type you comment on.
e.g.
Set Children = pChildren
Set MarriedDaughter = pMarriedDaughter
The following GrandMa.MarriedDaughter does not expose a .Children btw.
Perhaps
Dim aChild As MarriedMom, nextChild As Child
For Each aChild In GrandMa.MarriedDaughter
For Each nextChild In aChild.Children
Debug.Print nextChild.Name
Next
Next aChild

Is there a dictionary-like object which allows me to store an array as the key?

Let's say I have some sort of ListObject in Excel, like so:
KeyCol1 KeyCol2 KeyCol3 ValueCol1
Chevy Lumina 2003 $75
Chevy Camaro 2018 $50
Dodge Charger 2004 $13
Toyota Camry 2015 $35
I would like to create a dictionary-like object, like so (psuedocode):
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.Add [Chevy, Lumina, 2003], $75
dict.Add [Chevy, Camaro, 2018], $50
dict.Add [Dodge, Charger, 2004], $13
dict.Add [Toyota, Camry, 2015], $35
Essentially, I'd like key, value pairs of [KeyCol1, KeyCol2, KeyCol3], ValueCol1
But dictionaries can't have arrays for keys, so I'm a bit stuck. Is there anything out there that would allow me to get the O(1) performance of a dictionary, but with arrays as "keys"?
Thanks all.
You can just concatenate the array elements to one string and use that as a key. Depending on the actual keys you might need to use a delimiter so it is clear which part of the final string relates to which key.
Just for fun, you can also create a tree of dictionaries. For that you could use these functions:
Sub AddNested(dict As Object, keys As Variant, value As Variant)
Dim parent As Object
Dim i As Long
Dim key As String
Set parent = dict
For i = LBound(keys) To UBound(keys) - 1
key = keys(i)
If Not parent.Exists(key) Then
parent.Add key, CreateObject("Scripting.Dictionary")
End If
Set parent = parent(key)
Next
parent.Add keys(UBound(keys)), value
End Sub
Function GetNested(dict As Object, keys As Variant)
Dim parent As Object
Dim i As Long
Dim key As String
Set parent = dict
For i = LBound(keys) To UBound(keys) - 1
key = keys(i)
If Not parent.Exists(key) Then
Exit Function
End If
Set parent = parent(key)
Next
GetNested = parent(keys(UBound(keys)))
End Function
An example showing how to add to & read from this structure:
Dim dict As Object
Dim i As Long
Set dict = CreateObject("Scripting.Dictionary")
AddNested dict, Array("Chevy", "Lumina", 2003), 75
i = GetNested(dict, Array("Chevy", "Lumina", 2003))
Debug.Print i ' = 75
The advantage here is that individual keys keep their data type in the data structure: e.g. a numeric key remains numeric.
More Generic
If it is necessary to also associate values with partial composite keys, then the above will not suffice. In that case create a real tree where each node can have both a value and child nodes. That can be done by changing the above Sub and Function as follows:
Sub AddNested(dict As Object, keys As Variant, value As Variant)
Dim parent As Object
Dim key As String
Dim children As Object
Set parent = tree
For Each key In keys
If Not parent.Exists("Children") Then
parent.Add "Children", CreateObject("Scripting.Dictionary")
End If
Set children = parent("Children")
If Not children.Exists(key) Then
children.Add key, CreateObject("Scripting.Dictionary")
End If
Set parent = children(key)
Next
If parent.Exists("Value") Then parent.Remove "Value"
parent.Add "Value", value
End Sub
Function GetNested(dict As Object, keys As Variant)
Dim parent As Object
Dim key As String
Dim children As Object
Set parent = tree
For Each key In keys
If Not parent.Exists("Children") Then Exit Function
Set children = parent("Children")
If Not children.Exists(key) Then Exit Function
Set parent = children(key)
Next
GetNested = parent("Value")
End Function
Concatenate the 3 values to a string, using a ParamArray argument for the concatenation. As mentioned iby #trincot, the idea of a unique delimiter is a good one:
Option Explicit
Sub TestMe()
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.Add addToString("Chevy", "Lumina", "2003"), 75
dict.Add addToString("Chevy", "Camaro", "2018"), 50
dict.Add addToString("Dodge", "Charger", "2004"), 13
If dict.exists("uniqueChevyuniqueLuminaunique2003") Then
Debug.Print dict("uniqueChevyuniqueLuminaunique2003")
End If
End Sub
Public Function addToString(ParamArray myVar() As Variant) As String
Dim cnt As Long
Dim val As Variant
Dim delim As String: delim = "unique"
For cnt = LBound(myVar) To UBound(myVar)
addToString = addToString & delim & myVar(cnt)
Next cnt
End Function
Before adding to the dictionary it is considered a good practice to check whether the given key exists. dict.Exists(key).
The idea of ParamArray is that you can give as many parameters as you would like.

Assigning Entire Private Array via class properties

I have an array that is a fixed size as a Private variable for one of my classes. Is there a way to set up a get/let property that will allow me to pass the entire array to the object, or will I have to assign the values independently? What about with a public function?
I am not sure but maybe you are talking about sth like that.
A class with the name cArray
Option Explicit
Dim mArr(1 To 5) As String
Property Get aValue() as Variant
aValue = mArr
End Property
Private Sub Class_Initialize()
mArr(1) = "Test1"
mArr(2) = "Test2"
mArr(3) = "Test3"
mArr(4) = "Test4"
mArr(5) = "Test5"
End Sub`
And for testing
Option Explicit
Sub Test()
Dim c As cArray
Dim v As Variant
Dim i As Long
Set c = New cArray
v = c.aValue
For i = LBound(v) To UBound(v)
Debug.Print v(i)
Next i
End Sub
Update: for the let part you will need a loop
Property Let aValue(nVal As Variant)
Dim i As Long
For i = LBound(nVal) To UBound(nVal)
mArr(i) = nVal(i)
Next i
End Property

vba deep copy/clone issue with class object dictionary

I have a dictionary in my Main Sub (KEY = string; VALUE = Class Object). The Class Object consists of two dictionaries. As I collect data and check the values stored in the Dictionary Values (Class Object - dictionaries) I noticed that only the last values are getting stored. What I mean is that all the Values in my dictionary in my Main Sub are pointing to the same dictionary reference, hence, all the instances of my Class Objects contain the same data. This means that I need to make a clone of my Class Objects (deep copy?). I have successfully done this before with Class Objects that only stored simple values, but not with dictionaries. I need help cloning my Class Object that contains dictionaries.
MAIN SUB
Dim dGroup As New Scripting.Dictionary ' Main Dictionary
'
' loop thru a listbox
For i = 0 To UserForm1.ListBox1.ListCount - 1
Gname = UserForm1.ListBox1.List(i) ' get listbox names
' populate temp dictionary
Set dic = FNC.GET_SESSION_FILE_ELEMENTS(mySesFile, Gname)
'
' instantiate new Class Object
Dim NewCol As New cVM_Col
Call NewCol.INIT(dic) ' pass the dictionary to a 'constructor'
dGroup.Add Gname, NewCol.CLONE ' add to the MAIN SUB dictionary
'
Set dic = Nothing ' clear the temp dictionary
Next i
CLASS OBJECT
Private dElms As Scripting.Dictionary
Private dDat As Scripting.Dictionary
'
Private Sub Class_Initialize()
Set dElms = New Scripting.Dictionary
Set dDat = New Scripting.Dictionary
End Sub
'
Public Sub INIT(inp As Scripting.Dictionary)
Set dElms = inp
End Sub
'
Public Function CLONE()
Set CLONE = New cVM_Col
Set CLONE.dElms = dElms ' <-- THIS IS WHERE IT CRASHES
Set CLONE.dDat = dDat
End Function
Normally my CLONE function works when I am only cloning simple data types like String or Long or Double. I've never had to do this with a Dictionary.
To CLONE the dictionary objects in my CLASS Objects I had to make the following changes:
CLASS OBJECT
(Modified CLONE function)
Public Function CLONE()
Set CLONE = New cVM_Col
CLONE.Elms = dElms
CLONE.Dat = dDat
End Function
(Added Properties)
Public Property Get Elms() As Scripting.Dictionary
Set Elms = dElms
End Property
Public Property Let Elms(p As Scripting.Dictionary)
Set dElms = p
End Property
'
Public Property Get Dat() As Scripting.Dictionary
Set Dat = dDat
End Property
Public Property Let Dat(p As Scripting.Dictionary)
Set dDat = p
End Property

Looping through All collections in VBA

I have a program where I create several different collections in VBA. After the program completes, I need to delete the records in each collection. I have been able to delete the collections statically with the following code:
Sub Empty_Collections()
Dim Count As Integer
Dim i As Long
Count = Managers.Count
For i = 1 To Count
Managers.Remove (Managers.Count)
Next i
Count = FS.Count
For i = 1 To Count
FS.Remove (FS.Count)
Next i
Count = Staff.Count
For i = 1 To Count
Staff.Remove (Staff.Count)
Next i
Count = Clusters.Count
For i = 1 To Count
Clusters.Remove (Clusters.Count)
Next i
End Sub
However, as I may add additional Collections in the future, is it possible to have code similar to this:
Dim Item As Collection
Dim Count As Integer
Dim i As Long
For Each Item In Worksheets
Count = Item.Count
For i = 1 To Count
Item.Remove (Item.Count)
Next i
Next
While I hesitate to create globals like this, here is a possible solution:
In the ThisWorkbook Excel Object, add the following:
Private pCollections As Collection
Public Property Get Collections() As Collection
If pCollections Is Nothing Then: Set pCollections = New Collection
Set Collections = pCollections
End Property
Public Property Set Collections(Value As Collection)
Set pCollections = Value
End Property
Public Sub AddCollection(Name As String)
Dim Coll As New Collection
Me.Collections.Add Coll, Name
End Sub
Public Sub EmptyCollections()
Dim Coll As Collection
For Each Coll In Me.Collections
EmptyCollection Coll
Next Coll
End Sub
Private Sub EmptyCollection(ByRef Coll As Collection)
Do While Coll.Count > 0
' Remove items from the end of the collection
Coll.Remove Coll.Count
Loop
End Sub
Then add and work with collections as follows:
' Add collections
' (without helper)
Dim Managers As New Collection
ThisWorkbook.Collections.Add Managers, "Managers"
' (with helper)
ThisWorkbook.AddCollection "FS"
ThisWorkbook.AddCollection "Staff"
ThisWorkbook.AddCollection "Clusters"
' Add items to collection
' (access collection via named key for ease-of-use)
ThisWorkbook.Collections("Managers").Add "Alice"
ThisWorkbook.Collections("Managers").Add "Bob"
' (or original reference if desired
Managers.Add "Carl"
Debug.Print Managers.Count ' => 3
Debug.Print ThisWorkbook.Collections("Managers").Count ' => 3
' Add collection later
ThisWorkbook.AddCollection "FutureCollection"
' Empty all collections
ThisWorkbook.EmptyCollections
This may be a little more complex than what you are looking for, but it has the advantage of adding collections to a central location so that they can all be emptied later.