Clean Initialization of Scripting.Dictionary - vba

I'm using VBA in an application called ProcessBook and I'm looking to store some static information in a dictionary.
I'm trying to initialize this dictionary cleanly and without needing to call a separate procedure, but none of the methods I try seem to be expected.
Here's what I've tried so far:
Dim myDict As New Dictionary(Of String, String) (("key1", "item1"), ("key2", "item2"))
Dim myDict As Variant
Set myDict = New Dictionary( ("key1", "item1"), ("key2", "item2") )
And basically a bunch of guessing involving forms like that.
So far the best I've been able to do is something like:
With myDict
.add "key1", "item1"
.add "key2", "item2"
End With
But this barks at me when it's not inside of a routine, which I'd like to avoid.
I believe the (Of type, type) From { } syntax is beyond VBA 6.5
Does anyone know of some clean ways to initialize a scripting.dictionary in VBA 6.5?

By 6.5 presumably you mean VBA? If so there is no way to manipulate/initialise an object instance outside of a routine.
The only way is to encapsulate it within a class (cDict) and use the constuctor:
Private mDict As Scripting.Dictionary
Private Sub Class_Initialize()
Set mDict = New Scripting.Dictionary
With mDict
.Add "key1", "item1"
.Add "key2", "item2"
End With
End Sub
Public Property Get Dict() As Scripting.Dictionary
Set Dict = mDict
End Property
Then in the client module;
Private Dict As New cDict
Sub foo()
MsgBox Dict.Dict.Item("key1")
Dict.Dict.Add "key3", "item3"
MsgBox Dict.Dict.Item("key3")
End Sub
(Of type relates to VB.Net Generics)

VBA is really a PROCEDURAL language more then an Object Oriented Language so the answer is to use a procedure.
How to do it...
Option Explicit
Option Compare Text
Sub test()
Dim dic As New Scripting.Dictionary
Add dic, Array("Darren", 123, "Alex", 321)
MsgBox dic.Count
End Sub
Public Sub Add(dic As Dictionary, values As Variant)
If Not IsArray(values) Then
Err.Raise -1, "Add", "Values must be varient Array"
ElseIf UBound(values) Mod 2 = 0 Then
Err.Raise -1, "Add", "Even number of values required"
End If
Dim iQ As Long
For iQ = 0 To UBound(values) Step 2
dic.Add values(iQ), values(iQ + 1)
Next
End Sub

Related

Something like a Public constant populated dictionary

In VBA what's the best way to declare a Public constant collection/dictionary of elements with multiple properties like this?
Dim fruits as new dictionary
fruits.add "banana", array("yellow", "long", "curved")
fruits.add "watermelon", array("red", "big", "sferic")
fruits.add "blueberry", array("blue", "little", "sferic")
I could change the Dim fruits as new dictionaryintoPublic fruits as new dictionary moved on top (outside procedure) but how could I populate this dictionary once for multiple sub/functions that will use it?
I could put all the three "add" instructions in a dedicated sub called "fruits_populate()", and call this sub at the beginning in each sub/function where I use it but is there a better solution?
One solution would be to simulate a memoised getter:
Public Function FRUITS() As Dictionary
Static obj As Dictionary ' Static keeps the object between calls '
If obj Is Nothing Then
Set obj = New Dictionary
obj.add "banana", Array("yellow", "long", "curved")
obj.add "watermelon", Array("red", "big", "sferic")
obj.add "blueberry", Array("blue", "little", "sferic")
End If
Set FRUITS = obj
End Sub
Then to get an item:
Debug.Print FRUITS.Item("banana")(1)
Another way would be to implement a class module ClsFruits.cls :
Dim base As Dictionary
Private Sub Class_Initialize()
Set base = New Dictionary
base.add "banana", Array("yellow", "long", "curved")
base.add "watermelon", Array("red", "big", "sferic")
base.add "blueberry", Array("blue", "little", "sferic")
End Sub
Public Property Get Item(Key)
Item = base.Item(Key)
End Property
Then to get an item:
Dim fruits As New ClsFruits
Sub Test()
Debug.Print fruits.Item("banana")(1)
End Sub
Originally a comment, but it grew too long:
1) It is easy to declare a public dictionary (as you already know), but
2) It is not possible to initialize it with a literal -- you need to run some set-up code that runs before any other code. Workbook_Open() is a natural place for such code.
3) There is no way to lock it down as constant -- dictionaries are mutable, but
4) If you really want, you could define your own class of objects that refuse to update themselves.
To expand on point 2). It is probably still a good idea to write a dedicated sub such as fruits_populate(). Put that code in a public module, and then in the Workbook code module put:
Private Sub Workbook_Open()
fruits_populate
End Sub
The advantage of doing it this way is that if you get to the point of doing robust error-handling, you might want to be able to bounce back from loss of state (e.g. a runtime error causes the project to reset), in which case you might have reason to call fruits_populate() from some error-handling code.
You can create your own Fruits object (dictionary).
You can set the VB_PredeclaredId set to True to act as a global default instance (access it from anywhere in your program) and the Item as the default member so you don't have to call it each time.
To do this:
Create a Class Module
Export
Replace with below code
Import
Fruits Class:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Fruits"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Private m_fruits As Object
Public Property Get Item(ByVal Name As String) As Variant
Attribute Item.VB_UserMemId = 0
Item = m_fruits(Name)
End Property
Public Property Let Item(ByVal Name As String, ByVal Value As Variant)
m_fruits(Name) = Value
End Property
Public Sub Clear()
m_fruits.RemoveAll
End Sub
'For testing - can omit
Public Function Names() As Variant
Names = m_fruits.Keys
End Function
Private Sub Class_Initialize()
Set m_fruits = CreateObject("Scripting.Dictionary")
End Sub
A simple test:
Sub Test()
Fruits("Banana") = Array("yellow", "long", "curved")
Fruits("Watermelon") = Array("red", "big", "sferic")
Fruits("Blueberry") = Array("blue", "little", "sferic")
PrintFruits
Fruits.Clear
End Sub
Private Sub PrintFruits()
Dim d As Variant, idx As Integer
For Each d In Fruits.Names()
Debug.Print "Fruit: " & d
For idx = 0 To UBound(Fruits(d))
Debug.Print String(3, " ") & Fruits(d)(idx)
Next idx
Next d
End Sub
'Output:
'Fruit: Banana
'yellow
'long
'curved
'Fruit: Watermelon
'red
'big
'sferic
'Fruit: Blueberry
'blue
'little
'sferic

Accessing dictionary within array VBA

I want to access the dictionary value "Watermelon" by using the key "first", while the dictionary is sitting within the array. I believe this is the correct method for inserting a dictionary into an array. I've tried every combination I can think of for syntax, and can't find the answer anywhere for how to access the dictionary while it's sitting in the array.
Function dictTest() As Object
Dim m_info As Dictionary
Set m_info = New Dictionary
m_info.Add "first", "watermelon"
Set dictTest = m_info
End Function
Sub checker()
Dim x(1 to 1) As Object
Set x(1) = dictTest
End Sub
This should work for you:
Function dictTest() As Object
Dim m_info As Object
Set m_info = CreateObject("Scripting.Dictionary")
m_info.Add "first", "watermelon"
Set dictTest = m_info
End Function
Sub checker()
Dim x(1 To 1) As Object
Set x(1) = dictTest
MsgBox x(1)("first")
End Sub

Excel vba percentile worksheetfunction function with collection argument

Is there a way to call Application.Worksheetfunction.Percentile(resultColl) where resultColl is a Collection?
I tried it and it returns a Unable to get Percentile property of the WorksheetFunction class error.
EDIT:
I tried to first convert that collection to array:
Function convertToArray(resultColl As Collection)
Dim resultArray() As Variant
ReDim resultArray(1 To resultColl.Count)
Dim i As Long
For i = 1 To resultColl.Count
resultArray(i) = resultColl.Item(i)
Next
convertToArray = resultArray
End Function
and use that array inside Percentile function:
Application.WorksheetFunction.Percentile( _
convertToArray(clientsColl.Item(1).getSumLosses), 0.99)
But now it returns a wrong number of arguments or invalid property assignment error at convertToArray function, even though in this test example I created, the function works fine:
Sub testConvert() 'this works fine
Dim testColl As Collection
Set testColl = New Collection
testColl.Add "apple"
testColl.Add "orange"
testColl.Add "pineapple"
Dim tempArray() As Variant
tempArray = convertToArray(testColl)
MsgBox (tempArray(1))
End Sub
clientsColl.Item(1).getSumLosses is a Collection:
inside Client class:
Private sumLosses As Collection 'the collection of numbers, a percentile of which I need to calculate
Private Sub Class_Initialize()
Set sumLosses = New Collection
End Sub
Public Property Get getSumLosses()
Set getSumLosses = sumLosses
End Property
EDIT2:
Changed the Percentile function call to this:
Dim tempArray() As Variant
tempArray = convertToArray(clientsColl.Item(1).getSumLosses)
resultDict.Add "UL: " & _
Application.WorksheetFunction.Percentile(tempArray, 0.99)
the error occurs on the line with resultDict.
Figured it out. The adding to the dictionary was done in a wrong way:
resultDict.Add "UL: " & _
Application.WorksheetFunction.Percentile(tempArray, 0.99)
instead of
resultDict.Add "UL: ", _
Application.WorksheetFunction.Percentile(tempArray, 0.99)

Excel vba: Class sub: Wrong number of arguments or invalid property assignment on vba

I have a class State and some sub inside it that takes a Scripting.Dictionary as an argument. However when I try to pass a dictionary there, I get a wrong number of arguments or invalid property assignment error. I can't figure out what's wrong.
'Sub insite State class
Sub addSecondItems(itemsDict As Object)
MsgBox ("start addSecondItems")
End Sub
Sub test()
Dim stateCopy As State
Set stateCopy = New State
...
Dim dict1 As Object
Set dict1 = CreateObject("Scripting.Dictionary")
stateCopy.addSecondItems (dict1) 'error here
...
End Sub
At the same time
Sub testPetDict()
Dim petDict As Object
Set petDict = CreateObject("Scripting.Dictionary")
Call readPetDict(petDict)
End Sub
Sub readPetDict(petDict As Object)
Dim year As Integer
For year = 2014 To 2017
MsgBox (year & ". " & petDict(year))
Next
End Sub
works fine.
What may be wrong here and why the second case works, while the first fails?
You should remove the brackets:
stateCopy.addSecondItems dict1
or use Call
Call stateCopy.addSecondItems(dict1)
Otherwise the brackets try to coerce the dictionary to a value by calling its default property, Item, which requires an argument, hence the error message.

3 Dimentional Dictionary

I'm trying make to a 3 Dimension Dictionary to store the data in the form of tools(material)(part)(attribute), and I have managed to create the Dictionary like this:
Dim Tools As New Dictionary(Of String, Dictionary(Of String, Dictionary(Of String, Decimal)))
And what I basically want to do is have some subs that manage that for me instead of dealing with that mess, and I want it to be like this like this:
Add_Attribute("Iron", "Pickaxe Head", "Durability", 204)
Get_Attribute("Stone", "Pickaxe Head", "Mining Speed")
Any answers would be greatly be appreciated.
My comment was not worded properly.
Create a class with add/get attributes function that accepts 3 parameters.
Concatenate the parameters and use it as dictionary key.
Option Explicit
Dim oDict As Dictionary
Public Function Add_Attribute(psParam1 As String, psParam2 As String, psParam3 As String, psValue As String)
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
oDict.Item(sKey) = psValue
Else
oDict.Add sKey, psValue
End If
End Function
Public Function Get_Attribute(psParam1 As String, psParam2 As String, psParam3 As String) As String
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
Get_Attribute = oDict.Item(sKey)
Else
Get_Attribute = ""
End If
End Function
Private Sub Class_Initialize()
Set oDict = New Dictionary
End Sub
Private Function BuildKey(psParam1 As String, psParam2 As String, psParam3 As String) As String
BuildKey = Join(Array(psParam1, psParam2, psParam3), "#")
End Function
Private Sub Class_Terminate()
Set oDict = Nothing
End Sub
Jules' answer of a custom class and concatenation of the three strings as a key will work very nicely for you and is a neat solution to your problem.
I'm posting another answer here for anyone who wants more of a dot notation style of solution. So one of the lines in your example could look something like:
mTools("Pickaxe Head").Attr("Durability").Material("Iron") = 204
I'm guessing you're deriving the values from a comboxbox or something similar, so working with strings might serve you fine. However, if you wished, you could go one stage further and create objects for the Attributes and Material parameters to achieve true dot notation (I didn't do the Parts parameter but you could do that one too):
mTools("Pickaxe Head").Durability.OnIron = 204
From a development point of view, the time consuming part would be to create all the parameter objects and keys, but if you are intending to manipulate the data anything more than trivially, it could make your life easier further down the track.
For your own project, are you certain that the data is genuinely 3 dimensional? Perhaps it's just the variable names that you've picked, but it seems as though you have one main object, ie the part (Pickaxe Head) which has some attributes (Durability and Mining Speed) which themselves have values based on the material they're operating on (Stone and Iron). Structurally, could it look like this?:
In terms of the code for this solution, create three classes. I've called them clsKeys, clsMaterials and clsPart.
For your clsKeys, the code is simply your field names:
Public Durability As String
Public MiningSpeed As String
Public Iron As String
Public Stone As String
For clsPart, the code contains the object names and a means of accessing them by string:
Public Name As String
Public Durability As New clsMaterials
Public MiningSpeed As New clsMaterials
Private mProperties As New Collection
Public Property Get Attr(field As String) As clsMaterials
Set Attr = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add Durability, .Durability
mProperties.Add MiningSpeed, .MiningSpeed
End With
End Sub
clsMaterials is similar:
Public OnStone As Integer
Public OnIron As Integer
Private mProperties As New Collection
Public Property Let Material(field As String, value As Variant)
mProperties.Remove field
mProperties.Add value, field
End Property
Public Property Get Material(field As String) As Variant
Material = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add OnStone, .Stone
mProperties.Add OnIron, .Iron
End With
End Sub
These classes can take as many objects as you like. You'll note I've instantiated the objects in the declaration which isn't best form but I've done it in the interest of space.
Finally, in a Module you need 3 routines: one to create the field keys, one to populate the data and one to retrieve it.
For the keys:
Option Explicit
Public Keys As clsKeys
Private mTools As Collection
Sub CreateKeys()
Set Keys = New clsKeys
With Keys
.Durability = "Durability"
.MiningSpeed = "Mining Speed"
.Iron = "Iron"
.Stone = "Stone"
End With
End Sub
For data population:
Sub PopulateData()
Dim oPart As clsPart
Set mTools = New Collection
Set oPart = New clsPart
With oPart
.Name = "Pickaxe Head"
'You could use dot notation
.Durability.OnIron = 204
.Durability.OnStone = 100
'Or plain strings
.Attr("Mining Speed").Material("Stone") = 50
.Attr("Mining Speed").Material("Iron") = 200
mTools.Add oPart, .Name
End With
End Sub
and for data retrieval:
Sub RetrieveValue()
Dim oPart As clsPart
Dim v As Variant
Set oPart = mTools("Pickaxe Head")
With oPart
'Using dot notation
v = oPart.Durability.OnIron
Debug.Print v
'Using plain strings
v = oPart.Attr("Durability").Material("Stone")
Debug.Print v
End With
'Or even without assigning the oPart variable
v = mTools("Pickaxe Head").Attr("Mining Speed").Material("Iron")
Debug.Print v
End Sub