VBA - Dictionary search - type mismatch - excel

So I'm learning how to use Dictionaries but I have encountered a type problem. In the code below I get an error.
Type mismatch
Each time I try to call If Not target.SearchItem(sKey) Is Nothing Then. I want it to return a object then if it isn't Nothing I can convert it back to long.
For Each target In dList.getArray
For Each key In aList.getArray(1).getKeys
sKey = CStr(key)
'Error occurs here - as Type mismatch
If Not target.SearchItem(sKey) Is Nothing Then
cData.setData(sKey) = CLng(target.SearchItem(sKey))
target.removeData = sKey
Else
'if value doesn't exists it will just be 0
cData.setData(sKey) = 0
End If
Next key
Next target
data is my dictionary that is in a separate class:
Property Get SearchItem(name As String) As Variant
If data.exists(name) Then
'becomes a Variant/Integer - data(name) is a Variant/long
Set SearchItem = CVar(data(name))
Else
'Should return Nothing if item doesnt exist
Set SearchItem = Nothing
End If
End Property
Update: To explain the problem I bit more. Even though I return it as a variant it will still be partly Integer and therefor If Not target.SearchItem(sKey) Is Nothing Then will return mismatch as it needs an object and VBA doesn't read is as a variant or something. Is there anything like Nothing, null or equally that works for a long? That would solve the problem.
This code below returns a long as I want it to, but I can't use -99 as it would corrupt the data analyse. It needs to be something that isn't a value
Property Get SearchItem(name As String)
If data.exists(name) Then
SearchItem = data(name)
Else
'SearchItem = Nothing
SearchItem = -99
End If
End Property

If Not target.SearchItem(sKey) Is Nothing Then
This implies that the stored item is an Object
cData.setData(sKey) = CLng(target.SearchItem(sKey))
The only way that this line will work on an Object is that the Object has a default value that can be converted to long. If the Object's default value is returning a value that can be converted to long.
Does Clng(Object) work on the actual Object that is being stored in the Dictionary?
If you are storing mixed data type then SearchItem check if the return data is an Object.
Property Get SearchItem(name As String) As Variant
If isObject(data(name)) Then
Set SearchItem = data(name)
Else
SearchItem = data(name)
End If
End Property
I would not convert the datatype in this method. I would instead create one or more separate methods or convert the data type at the point of use.
Function getTypedItem(Key as Variant, DataType as VbVarType) as Variant
IsNumeric(Obj)
Since you are using actual Keys of the Dictionary we know that it will return something and there is not a need for .Exists in this context. What you will need to do is test if it is returning an Object before you test whether the Object Is Nothing.
If IsObject(Target.SearchItem(sKey)) Then
If IsNumeric(Target.SearchItem(sKey)) Then
cData.setData(sKey) = CLng(Target.SearchItem(sKey))
End If
ElseIf IsNumeric(Target.SearchItem(sKey)) Then
cData.setData(sKey) = CLng(Target.SearchItem(sKey))
End If

I don't think you can use Set to assign an integer, so the property will fail when the dictionary item is an Integer. Perhaps use:
Property Get SearchItem(name As String) As Variant
If data.exists(name) Then
'becomes a Variant/Integer - data(name) is a Variant/long
SearchItem = data(name)
...

Unfortunately, VBA does not have nullable primitive types such as a nullable Long. A usual workaround is to use a Variant, which can be assigned the value of Empty and which can also hold a Long natively.
I would rewrite your property as follows:
Property Get SearchItem(name As String) As Variant
If Data.exists(name) Then
SearchItem = Data(name)
Else
SearchItem = Empty
End If
End Property
And the test for empty would be:
If Not IsEmpty(target.SearchItem(sKey)) Then ...

Related

Unable to find a way to store data into an array inside a class module

I've been wrestling with this problem for a while. My problem is that I have a bunch of JSON data and I want to represent it as objects.
Arrays are problematic.
I create a class module such as FancyCat with a public Name as String for its name.
Then I can set this with
Dim MyFancyCat as FancyCat
Set MyFancyCat = new FancyCat
FancyCat.Name = JSONData("Name")
I've typed that from memory but I think it's correct. Anyhoo, it works fine.
The problem is that a fancy cat has several pairs of socks. The number of socks is variable.
In vba you cannot for some reason have a public array. So this code is illegal:
public Socks() as FancySock 'Illegal
Looking on SO I found two solutions, one, to make it private and use a property to access it, and the other, to declare it as Variant and then stick an array into it later.
My approach to populating this array, is to examine the JSON array to get the Count, and then to ReDim the array to match and then populate it.
The problem is my ReDim statement refuses to work.
It seems I cannot redim a property, I get an error. And I also get an error trying to redim the public variant field. My ReDim works OK if I declare a local array and redim it, so potentially I could do that and then assign it to the property... but it just seems bizarre that I can't redim it directly.
Any idea why it's not working?
With the Variant approach above my code is:
ReDim MyFancyCat.Socks(socksLength) As FancySocks
And in the FancyCat class module:
public Socks As Variant
I get Method or Data Member Not Found.
The error for the other approach was different but I rejigged all my code to try the second approach so I am not sure what it was.
Edit: I'm gonna explain what I am trying to do a bit more clearly. I have some JSON data coming in, and I want to store it as an object hierarchy.
In C# I would do this (pseudo code without linq shortcuts):
var myData = ReadJsonData(); // Produces a kind of dictionary
var myFancyCat = new FancyCat();
myFancyCat.Name = myData["Name"];
myFancyCat.Age = myData["Age"];
myFancyCat.Socks = new List<FancySock>();
foreach (var sock in myData["Socks"])
{
myFancyCat.Socks.Add(sock);
}
In excel I want to do the same thing.
So I make a class module for FancyCat and FancySock and give FancyCat public members for Name, Age etc but then I also want an array of socks that my cat owns. I wanted to do this with strongly typed references, e.g. my c# code above I can do:
myFancyCat.Socks[0].Colour // Intellisense works, shows colour as a property
However it seems in excel you can't have publicly declared arrays. So you can get around this according to the comments by declaring it as variant and then sticking an array in anyway, but you would lose the intellisense. Or you can use a get/let property which kinda works but is more fiddly as it seems you can't actually expose an array using a get/let you have to have it take an index and expose elements individually.
So at this point I am thinking forget the strongly typed it's not happening, perhaps use a collection?
The FancySock class may have further nested arrays within it. I've read that there's no ByRef for arrays (at least, not completely - I think you can get an array ByRef but not set one?). I am not sure if that would create problems with trying to set it.
But ultimately, I just want to end up with my JSON data represented easily in an OO way, so that in my excel ultimately I can just do
myFancyCat.Name or myFancyCat.Socks.Count or myFancyCat.Socks(1).Colour etc
It seems much harder than it looks to simply deserialise JSON into 'objects' in vba.
Please, try the next way:
Insert a class module, name it FancyCat and copy the next code:
Option Explicit
Private arrL As Object
Public myName As String, myAge As Long
Public Sub Class_Initialize()
Set arrL = CreateObject("System.Collections.ArrayList")
End Sub
Public Property Let Name(strName As String)
myName = strName
End Property
Public Property Let Age(lngAge As String)
myAge = lngAge
End Property
Public Property Let SocksAdd(sMember)
arrL.Add sMember
End Property
Public Property Get Socks() As Variant
Socks = arrL.toarray()
End Property
Use it in the next testing Sub:
Sub testClassDictListArray()
Dim myFancyCat As New FancyCat, myData As Object
Dim arrSocks, sock
Set myData = CreateObject("Scripting.Dictionary") 'this should be the dictionary returned by ParseJSON
myData.Add "Name", "John Doe": myData.Add "Age", 35
myData.Add "Socks", Array("Blue", "White", "Red", "Green", "Yellow")
myFancyCat.Name = myData("Name")
myFancyCat.Age = myData("Age")
For Each sock In myData("Socks")
myFancyCat.SocksAdd = sock
Next sock
arrSocks = myFancyCat.Socks
Debug.Print Join(arrSocks, "|")
End Sub
I am not sure I perfectly understand the scenario you try putting in discussion...
If you want to benefit of instellisense suggestions, I will tell you what references to be added. Even, I will send two pieces of code to automatically add the necessary references (I mean, Scripting.Dictionary and ArrayList`).
Please, test it and send some feedback.
In your class:
Private m_Name As String
Private m_Socks() As String
Public Property Let Name(Name As String)
m_Name = Name
End Property
Public Property Get Name() As String
Name = m_Name
End Property
Public Sub SetSize(Quantity As Long)
ReDim m_Socks(1 To Quantity)
End Sub
Public Property Let Socks(Index As Long, Sock As String)
m_Socks(Index) = Sock
End Property
Public Property Get Socks(Index As Long) As String
Socks = m_Socks(Index)
End Property
In a regular module:
Sub UseFancyCat()
Dim MyFancyCat As FancyCat
Set MyFancyCat = New FancyCat
MyFancyCat.Name = "Fancy Name"
MyFancyCat.SetSize 2
MyFancyCat.Socks(1) = "Sock1"
MyFancyCat.Socks(2) = "Sock2"
Debug.Print MyFancyCat.Name
Debug.Print MyFancyCat.Socks(1)
Debug.Print MyFancyCat.Socks(2)
End Sub

Class constructor confusion - wrong number of arguments or invalid property assignment

I'm having a class module with some data:
Private sharedFolders() As String
Public Property Let SetSharedFolders(val As String)
Dim i As Integer
sharedFolders = Array("folder one", "folder two")
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
End Property
Property Get GetSharedFolders()
GetSharedFolders = sharedFolders()
End Property
And I want to add something to this property from other module like this:
Sub PrepareData()
Dim e
Dim s
Dim a(2) As String
Set e = New Entry
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
But I receive an "wrong number of arguments or invalid property assignment vba" exception... Can anyone assist?
Addendum
Thanks to #AJD and #Freeflow to pointing out a mistake and idea to make it easier. Decided to make as like below.
Class Module:
Private sharedFolders As New Collection
Public Property Let SetSharedFolders(val As String)
If sharedFolders.Count = 0 Then ' if empty fill with some preset data and add new item
sharedFolders.Add "folder 1"
sharedFolders.Add "folder 2"
sharedFolders.Add CStr(val)
Else
sharedFolders.Add CStr(val)
End If
End Property
Property Get GetSharedFolders() As Collection
Set GetSharedFolders = sharedFolders
End Property
and regular module:
Sub AddData()
Dim e As New Entry ' creating an instance of a class
Dim s As Variant ' variable to loop through collection
Dim a(1) As String 'some array with data to insert
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders = s
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
Initially I thought the problem lies in this code:
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
i is set twice to the same value, and then the sharedFolders is reDimmed to the same value it was before! Also, there is some trickery happening with the use of ix within a 0-based array.
But the problem is most likely how you have declared your variables.
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
s is a Variant, and a is a Variant. At this point VBA is trying to guess how to handle a For Each loop with two Variants. And then the improper call is made. The correct syntax is:
e.SetSharedFolders s '<-- no parenthesis
There are plenty of posts on StackOverflow explaining how to call routines and what the impact of the evaluating parenthesis are!
However, at this point we are only assuming it is passing in a single element of the array - it could be passing the full array itself (albeit unlikely).
And the third factor -
Public Property Let SetSharedFolders(val As String)
The parameter val is being passed ByRef and should be passed ByVal. This also has unintended side effects as I found out (Type mismatch trying to set data in an object in a collection).
Public Property Let SetSharedFolders(ByVal val As String)
All in all you have the perfect storm of ambiguity driving to an unknown result.
The answer here is to strongly type your variables. This removes about two layers of ambiguity and areas where errors can happen. In addition, this will slightly improve code execution.
Another aspect is to understand when you should pass something ByVal and when to use the default (preferably explicitly) ByRef.
And a final gratuitous hint: Use a Collection instead of an Array. Your code you have implies a Collection will be more efficient and easier to manage.
Addendum
(thanks to #FreeFlow):
If the OP changes the definition of sharedfolders to Variant rather than String() then the array statement will work as expected.
The line e.SetSharedFolders (s) will work fine if it is changed to e.SetSharedFolders = s because the method SetSharedFolders is a Let Property not a Sub. There are other errors but these two changes will make the code run.

Exist function in dictionary doesn't work for an object as Key created in a class module

In my code, in need to store two value in my key to be able to do the analysis i require. And since I didn't want to store everything in an array, i decided to create an object with 2 parameters. But when i run the Exist function of the dictionary with that object "TwoInputs" as the type of Key, I always get that they Key doesn't exist. Can anyone help please?
I added "Option Compare Text" just in case but the exist still return False.
When I run the code line by line and force it to go the If "true" condition, a new Key is still created, don't know why.
This is the class module i created:
Private acc As Double
Private act As Variant
'Account property
Public Property Get Account() As Double
Account = acc
End Property
Public Property Let Account(Value As Double)
acc = Value
End Property
'Activity property
Public Property Get Activity() As Variant
Activity = act
End Property
Public Property Let Activity(Value As Variant)
act = Value
End Property
In a normal module i wrote a function to create a TwoInputs object based on two entries:
Public Function cTwoInputs(Account As Double, Activity As Variant) As TwoInputs
Set cTwoInputs = New TwoInputs
cTwoInputs.Account = Account
cTwoInputs.Activity = Activity
End Function
Then I create a sub where I want to add the 2 informations in a Key if they exist:
While dataSheet.Range("dataAgent").Offset(j, 0).Value <> "Project ID:" And dataSheet.Range("dataAgent").Offset(j, 0).Row <= lRow
If dataSheet.Range("dataAgent").Offset( j, 0).Value = "Activity ID:" Then
actName = dataSheet.Range("dataAgent").Offset(j, 1).Value
End If
If (dataSheet.Range("dataAgent").Offset(j, 0).Value = "XXXXX" Or dataSheet.Range("dataAgent").Offset(j, 0).Value = "") Then
KeyExist.Account = dataSheet.Range("dataAccount").Offset(j , 0).Value
KeyExist.Activity = actName
If dicBudget.Exists(KeyExist) Then
dicBudget(KeyExist) = dicBudget(KeyExist) + dataSheet.Range("dataBudget").Offset(j , 0).Value
Else
dicBudget.Add cTwoInputs(dataSheet.Range("dataAccount").Offset(j, 0).Value, actName), dataSheet.Range("dataBudget").Offset( j, 0).Value
End If
End If
j = j + 1
Wend
.Exists() method compares objects by their instances not by values of their fields.
So it's better to use primitive types like String, Integer, Double etc. as keys, and not to use custom object as a key. If you really need to use object-key for some purpose than you must be sure that you call .Exists() method on the same object you put to dictionary (e.g. by storing that key in a global variable).

VBA Function - Argument Not Optional

Public Function RETURN_Equipment(Optional category As String) As Collection
Dim config As classConfiguration
Set config = New classConfiguration
Dim item As classItem
Set item = New classItem
Dim myCollection As Collection
Set myCollection = New Collection
For Each config In Configurations
For Each item In config.colItems
If IsMissing(category) Then
myCollection.add item
ElseIf InStr(category, "mainframe") <> 0 And item.category = "mainframe" Then
myCollection.add item
MsgBox "Fired!"
ElseIf category = "accessory" And item.category = "accessory" Then
Else
End If
Next
Next
RETURN_Equipment = myCollection
End Function
I keep getting
Compile error:
Argument not optional
I get the error on the last line
RETURN_Equipment = myCollection
I understand the error message, its telling me I did not fill out a parameter. But I only have one parameter, and I've declared it optional. It looks like the code thinks I'm trying to call the function from the function?
What gives?
Anytime you assign an object you need to use the set keyword.
set RETURN_Equipment = myCollection
I was getting this error because I was using the wrong function name when trying to return a result from a function. I was doing this:
Function MyFuncA(arg as String)
MyFuncB = arg 'The problem is I'm using MyFuncB instead of MyFuncA
End Function
This happened because I copied a function from somewhere else and changed the name, but not the return statement. This is not the OP's problem, but I was getting the same error message.
Because you've specified the Optional Parameter as a string it will default to an empty string if you've not specified a value.
This means it can't be missing
If you'd specified it as
Public Function RETURN_Equipment(Optional category) As Collection
It would be a variant and that could be missing, although you'd also be able to mess things up by passing non string variants as the category parameter
The best course of action is probably to replace
If IsMissing(category) Then
with
If category = "" Then
And as Brad has pointed out you'll need to use Set
Set RETURN_Equipment = myCollection
For full details check this
http://msdn.microsoft.com/en-us/library/office/gg251721%28v=office.15%29.aspx

Strange behavior of range when used as key in dictionary

I have the following code:
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
' A1 is empty - although the outcome is the same in any case
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1), Item:=0
Debug.Print dicMyHash.Exists(rngMyRange(1).Value) ' returns False
Debug.Print rngMyRange(1) = rngMyRange(1).Value ' returns True
This behavior is somewhat unexpected. Is there some type casting going on in the background? rngMyRange(1).Value property returns a variant, whereas rngMyRange(1) is rngMyRange.item(1), which is a range. However, casting rngMyRange(1) to Variant gives the same results..
Also, adding keys is by value (so a copy of rngMyRange(1) is passed as a key). But still I cannot get why .Exists does not find the key..
Thank you in advance!
So here, we have three different values being passed around:
The original range.
Range.Value, which is a variant.
The copy of (1) which is internal to the dictionary.
If you compare these with equal signs, they are all the same. But according to Dictionary.Exists they are all different.
Why? When you use an equal sign with an object, the equal sign forces the object to call its default property. The default property of Range is Range.Value, which is why r = r.Value and also r = r.Offset(0, 0).
But for a dictionary this isn't so smart. Think about it: Every call to Dictionary.Exists would cause every object used as a key to call its default property. This can get really expensive and it can potentially trigger a lot of side effects. So instead, Dictionary.Exists tests the following:
Are you comparing an object to a non-object? Automatic fail.
Are you comparing two non-ojects? Return a = b.
Are you comparing two objects? Return a Is b.
So r is not the same as r.Value, since one is an object and the other is a non-object. And if you make a copy of r, like with r.Offset(0, 0), those are not the same either since they still point to two different objects, even if the objects have identical contents.
This, on the other hand, will work, since you will make r into the same object as d.Keys(0):
Dim d As Scripting.Dictionary
Dim r As Range
Set r = [a1]
Set d = New Dictionary
d.Add r, 0
Set r = d.Keys(0)
Debug.Print d.Exists(r)
I think the reason of your situation is that rngMyRange is recognised as an two- dimensional array and both array dimensions are passed to your dictionary.
If you change the line which adding element into Dictionary into this one:
dicMyHash.Add Key:=rngMyRange(1).value, Item:=0
it starting to work as you expect- both check points return true.
You could additionally analyse this situation in Locals Window while debugging of your code.
I'm not sure how you are putting this to use, but this will return True:
Sub test()
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1).Value, Item:=0 ' assign it with Value
Debug.Print dicMyHash.Exists(rngMyRange(1).Value)
End Sub
So then you'll have an item with a key of whatever's in A1.
I believe the reason it doesn't work without Value is that you are assigning a Range to the Key. It would make more sense to me if you were assigning the range to the Dictionary's Item.

Resources