Set a type in VBA to nothing? - vba

I have defined a variable with an own type, say
Dim point As DataPoint
Public Type DataPoint
list as Collection
name as String
number as Integer
End Type
and I want to delete all values of the variable point at once. If it was a class, I would just use Set point = New DataPoint, or set Set point = Nothing, but how can I proceed if it's a type?

You can benefit from the fact that functions in VB have an implicit variable that holds the result, and that contains the default type value by default.
public function GetBlankPoint() as DataPoint
end function
Usage:
point = GetBlankPoint()

The standard way is to reset each member to its default value individually. This is one limitation of user-defined types compared to objects.
At the risk of stating the obvious:
With point
Set .list = Nothing
.name = ""
.number = 0
End With
Alternatively, you can create a "blank" variable and assign it to your variable each time you want to "clear" it.
Dim point As DataPoint
Dim blank As DataPoint
With point
Set .list = New Collection
.list.Add "carrots"
.name = "joe"
.number = 12
End With
point = blank
' point members are now reset to default values

EDIT: Damn! Beaten by JFC :D
Here is an alternative to achieve that in 1 line ;)
Dim point As DataPoint
Dim emptyPoint As DataPoint
Public Type DataPoint
list As Collection
name As String
number As Integer
End Type
Sub Sample()
'~~> Fill the point
Debug.Print ">"; point.name
Debug.Print ">"; point.number
point.name = "a"
point.number = 25
Debug.Print ">>"; point.name
Debug.Print ">>"; point.number
'~~> Empty the point
point = emptyPoint
Debug.Print ">>>"; point.name
Debug.Print ">>>"; point.number
End Sub
SNAPSHOT

One-liner:
Function resetDataPoint() As DataPoint: End Function
Usage:
point = resetDataPoint()

Another option is to use the reserved word "Empty" such as:
.number= Empty
The only issue is that you will need to change the number from integer to variant.

Using classes in VBA is usually a good practice in case it is not a single purpose solution or the class do not contain too many private attributes because if you want to adhere on OOP rules and keep your class safe, you should declare all the Let and Get properties for all private attributes of class. This is too much coding in case you have more than 50 private attributes. Another negative side of using classes in excel is fact, that VBA do not fully support the OOP. There is no polymorfism, overloading, etc.) Even you want to use an inheritance, you have to declare all the attributes and methods from the original class in the inherited class.
So in this case I would prefer the solution suggested by Jean-François Corbett or GSeng, i.e. to assign an empty variable of the same UDT as the variable you want to clear or to use a function which to me seems little bit more elegant solution because it will not reserve permanent memory for the emtpy variable of your UDT type.

For that is better to use classes, you can declare a class module with the name of your type, then declare all of your members as public, then automatically you can set to nothing and new for create and delete instances.
syntax will be somthing like this after you create the class module and named like your type:
'
Public List as Collection
Public Name as String
Public Number as Long
Private Sub Class_Initialize()
'Here you can assign default values for the public members that you created if you want
End Sub

Related

Assigning Values to properties in Structures

In the below code "f" is an instance of the Class FORM which has a property "s" of type SIZE, a structure which has been defined in the code. My question is: When I try to assign values to the attributes of property "s" of the instance "t" directly it does not work: That is the statement f.s.height = 15 does not work. My confusion is arising from the fact that when I print the values of the property "s" of the instance "f", I am able to print the individual attributes of the structure SIZE but the same cannot be done while assignment of value. Assignment of values require me to call the constructor. Why is it so? What is preventing the assignment of the value to the attributes of "s": i.e. f.s.height & f.s.width?
Module Module1
Sub Main()
Dim f As New MyForm()
f.s = New Size(2, 5) 'Works Fine
f.colour = "Red" 'Assignment works just fine
'Below: Individual elements cannot be acceessed for assignment. WHY?
f.s.height = 15 'Doesn't Work
f.s.height = +2 'Doesn't work
'Individual elements can be accessed while printing
Console.WriteLine("Widht = {0}", f.s.width)
Console.WriteLine("Height = {0}", f.s.height)
Console.ReadLine()
End Sub
End Module
Class MyForm
Public Property s As Size
Public Property colour As String
End Class
Public Structure Size
Dim height As Integer
Dim width As Integer
Public Sub New(ByVal w As Integer, ByVal h As Integer)
width = w
height = h
End Sub
End Structure
Pls help.
The compiler should be indicating "Expression is a value and therefore cannot be the target of an assignment".
Change Size from a Structure to a Class (and Dim to Property) to fix the problem:
Public Class Size
Property height As Integer
Property width As Integer
Public Sub New(w As Integer, h As Integer)
width = w
height = h
End Sub
End Class
By the way, you'll also see this behaviour with the standard System.Drawing.Size which is defined as a Structure rather than a Class. (So is Point and probably others.)
This behavior is fundamental to value types (Structures). Conceptually, an instance of a value type is supposed to represent a single immutable value, and any instances with the same value are all supposed to be equivalent. As you have observed, you can get very surprising behavior if you try to change parts of an existing value type. It's really not intended for you to be able to alter them piecewise.
For this reason, I will always recommend that the members of a value type should be marked as ReadOnly so that you get blocked from trying to change them after construction.
If you want to be able to treat something like a mutable object instance instead of an immutable value, it needs to be a reference type (a Class). That's what they're designed to do.
After a lot of searching I came across the following article which probably explains the reason why we are not able to directly access the height/width attribute of the SIZE structure through an instance of the FORM class. Requesting people to go through this as the author has given a lot of details:
http://www.albahari.com/valuevsreftypes.aspx
PLs feel free to share any difference in opinion.

Class variable as the counter of a For loop in VBA

I have a class module called MyClass, with a public integer in it:
Public i as Integer
When I try to use this variable in a For loop like so:
Dim MyInstance as MyClass: Set MyInstance = New MyClass
For MyInstance.i = 1 To 10
Debug.Print "Hello"
Next
I get the error: Variable required. Can't assign to this expression
I have consulted the help page but cannot see how it applies to my case. The relevant fragment is: "You tried to use a nonvariable as a loop counter in a For...Next construction. Use a variable as the counter." But i is a variable after all, and not a Let Property function or any other expression.
What is wrong with the code?
EDIT: I should point out that the reason I want my iterator to be part of the class is that I have multiple instances of the class, serving different purposes in my project, and there are multiple nested For loops for each instance of the class. Therefore it is worth having the iterators belong to their respective objects, say:
For Client.i = 1 To Client.Count
For Order.i = 1 To Order.Count
For Item.i = 1 To Item.Count
etc.
I have settled for the following workaround but am still not entirely satisfied with it:
For ciii = 1 To Client.Count
Client.i = ciii ' Client.i is later used in the code
For oiii = 1 To Order.Count
Order.i = oiii
For iiii = 1 To Item.Count
Item.i = iiii
You cannot use MyInstance.i as the increment counter but you can use it as the terminator; e.g. For i = 1 To MyInstance.i.
MyClass class
Option Explicit
Public pi As Long
Public Property Get i() As Long
i = pi
End Property
Public Property Let i(Value As Long)
pi = Value
End Property
test sub procedure in Module1
Sub test()
Dim MyInstance As MyClass, i As Long
Set MyInstance = New MyClass
MyInstance.i = 10
For i = 1 To MyInstance.i
Debug.Print "Hello"
Next
End Sub
If you want a publicly accessible loop variable stick it at the top of a standard module i.e. declare the Public i at the top of a standard module.
Note that this would mean you need to re-write your standard module code as, as per point two, you are treating i as if it is a property/method of the class.
So, standard module code would be:
Public i As Long
Sub ........
For i = 1 To 10
Debug.Print "Hello"
Next i
End Sub ......
If you want it to somehow be a property/method then you need to define Getters and Setters (potentially) in the class. And then re-write your module code accordingly. Especially if you are planning on looping using i, you will need an incrementor method in the class.
And yes, I have changed i to Long as there are no advantages, in this case I believe, of having it declared as Integer. A Long is a safer bet for avoiding potential overflow.
If you need a workaround so that you iterate through a property of the instance, you could create a method to increment it, change your loop to a Do While ... Loop and call that method before the loop call.
'Class Module
Option Explicit
Public i As Integer
Public Sub increment_i()
i = i + 1
End Sub
Private Sub Class_Initialize()
i = 0
End Sub
'Module
Sub loop_myclass()
Dim instance As MyClass: Set instance = New MyClass
Do While instance.i <= 10
'Instance property dependent code here
Debug.Print instance.i
instance.increment_i
Loop
End Sub
OK, I found the answer. There is a Microsoft help page on For…Next loop regarding VB, but I think it pertains to VBA as well.
It says:
If the scope of counter isn't local to the procedure, a compile-time
warning occurs.
So there's not much to discuss here, it's just the way MS wants it to be. Though I'd think that if the scope is greater than the procedure it shouldn't cause any problems, but apparently it does.

Sort a group of Classes by default property

TL;DR:
Is there any way to pass a class collection/list to a library sorting algorithm, and get it to return a sorted list (preferably by a named/default class property)?
I've recently been learning some Python, and was impressed by the Sorted() function, which can sort any iterable. For numbers this is straightforward, for classes however, it is possible to assign a comparison method like this. The method tells comparison operators how to compare 2 instances of the class. Amongst other things it allows you to use builtin sorting algorithms to sort a collection of the class.
In VBA I've been half successful in mimicking this. By setting a class' default member Attribute, you can use comparison operators (<,=,>=, etc.) on classes directly. Take the example class:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "defaultProp"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Private randVal As Single
Public Property Get DefaultValue() As Single
Attribute Value.VB_UserMemId = 0
DefaultValue = randVal
End Property
Private Property Let DefaultValue(ByVal value As Single)
randVal = value
End Property
Private Sub Class_Initialize()
DefaultValue = Rnd()
End Sub
Two instances of this class can be compared:
Dim instance1 As New defaultProp
Dim instance2 As New defaultProp
Debug.Print instance1.DefaultValue > instance2.DefaultValue
Debug.Print instance1 > instance2 'exactly equivalent, as the DefaultValue has the correct Attribute
And if I was implementing a VBA sort algorithm that can sort values, there should be no problem sorting classes by default value*. However I would prefer to use a builtin/library sorting algorithm (for the same reasons anyone would; clarity, efficiency, proper error handling etc.)
*One of these algorithms would work for that, although must be modified to switch the entire class round, not the value of it (by adding Sets)
Since VBA comparison operators have no issue, I assumed the same would be true for whatever the library was using. However when I tried with an ArrayList:
Sub testArrayList()
Dim arr As Object
Set arr = CreateObject("System.Collections.ArrayList")
' Initialise the ArrayList, for instance by generating random values
Dim i As Long
Dim v As defaultProp
For i = 1 To 5
Set v = New defaultProp
arr.Add v 'no problem here
Next i
arr.Sort 'raises an error
End Sub
I get an error
Failed to compare two elements in the array
So what's going on? Is it a flaw in my approach - is the default attribute not making it to the ArrayList? Or maybe the comparison operator in whatever language the library is written in is not as floopy-gloopy as the ones VBA and Python use? Any suggestions on more builtin sorting algorithms to try would be useful too!
It's not about the VBA comparison operators, ArrayList is a .NET class, so you're in the .NET world when you use it.
arr.Add v 'no problem here
You're adding instances of the defaultProp class; it doesn't matter that you have a default property on the type, .NET doesn't care about default properties. If you want to sort DefaultValue values, then do arr.Add v.DefaultValue or arr.Add (v) - then your ArrayList will contain items of type Single, which it knows how to sort.
In order for ArrayList.Sort to work with instances of your custom class, its items need to implement the IComparable interface, which is the case for System.Int32 (i.e. Long in VBA), System.String and every other primitive .NET types, and I think the VBA primitive types would indeed marshal correctly through .NET interop - but not custom classes.
Try adding a reference to mscorlib.tlb, and then in the defaultProp class module, specify this (you can't implement an interface that's defined in a late-bound library):
Implements IComparable
Then implement the interface - should look something like this (use the codepane dropdowns to make sure to get the correct signature - don't just copy-paste this snippet:
Private Function IComparable_CompareTo(ByVal obj As Variant) As Long
Dim other As defaultProp
Set other = obj
' return Less than zero (-1) if this object
' is less than the object specified by the CompareTo method.
' return Zero (0) if this object is equal to the object
' specified by the CompareTo method.
' return Greater than zero (1) if this object is greater than
' the object specified by the CompareTo method.
End Function
Now that your custom class implements the interface ArrayList.Sort uses to determine how your defaultProp items relate to each other, I don't see a reason for it to fail.
IMO, you are abusing things by mixing things cross the boundaries. You're using VBA's default properties (something I generally perceive as a bad practice), then you're using .NET's ArrayList and trying to Sort it.
I think it would be much more logical to see if you can implement IComparable on the VBA class and then let the ArrayList use the IComparable interface to compare an object against other by however you want it to compare, without using any default properties hacked.
If you add the DefaultValue to the arr it would work:
Sub testArrayList()
'... code
For i = 1 To 5
Set v = New defaultProp
arr.Add v.DefaultValue
Next i
arr.Sort
End Sub
Obviously the implementation of .Sort of ArrayList is a bit strange and does not like comparing objects and their default values (could not find the implementation of the Sort() method). Although, this would work flawlessly:
For i = 1 To 5
Set v = New defaultProp
arr.Add v
Next i
Debug.Print arr(1) > arr(2)
This is a possible implementation of sorting, that would work for the arr object as expected. However, it is not part of the ArrayList library:
Public Function varBubbleSort(varTempArray As Object) As Object
Dim varTemp As Object
Dim lngCounter As Long
Dim blnNoExchanges As Boolean
Do
blnNoExchanges = True
For lngCounter = 0 To varTempArray.Count - 2
If varTempArray(lngCounter) > varTempArray(lngCounter + 1) Then
blnNoExchanges = False
Set varTemp = varTempArray(lngCounter)
varTempArray(lngCounter) = varTempArray(lngCounter + 1)
varTempArray(lngCounter + 1) = varTemp
End If
Next lngCounter
Loop While Not (blnNoExchanges)
Set varBubbleSort = varTempArray
On Error GoTo 0
Exit Function
End Function
But the sorting is ok:

Checking if a value is a member of a list

I have to check a piece of user input against a list of items; if the input is in the list of items, then direct the flow one way. If not, direct the flow to another.
This list is NOT visible on the worksheet itself; it has to be obfuscated under code.
I have thought of two strategies to do this:
Declare as an enum and check if input is part of this enum, although I'm not sure on the syntax for this - do I need to initialise the enum every time I want to use it?
Declare as an array and check if input is part of this array.
I was wondering for VBA which is better in terms of efficiency and readability?
You can run a simple array test as below where you add the words to a single list:
Sub Main1()
arrList = Array("cat", "dog", "dogfish", "mouse")
Debug.Print "dog", Test("dog") 'True
Debug.Print "horse", Test("horse") 'False
End Sub
Function Test(strIn As String) As Boolean
Test = Not (IsError(Application.Match(strIn, arrList, 0)))
End Function
Or if you wanted to do a more detailed search and return a list of sub-string matches for further work then use Filter. This code would return the following via vFilter if looking up dog
dog, dogfish
In this particular case the code then checks for an exact match for dog.
Sub Main2()
arrList = Array("cat", "dog", "dogfish", "mouse")
Debug.Print "dog", Test1("dog")
Debug.Print "horse", Test1("horse")
End Sub
Function Test1(strIn As String) As Boolean
Dim vFilter
Dim lngCnt As Long
vFilter = Filter(arrList, strIn, True)
For lngCnt = 0 To UBound(vFilter)
If vFilter(lngCnt) = strIn Then
Test1 = True
Exit For
End If
Next
End Function
Unlike in .NET languages VBA does not expose Enum as text. It strictly is a number and there is no .ToString() method that would expose the name of the Enum. It's possible to create your own ToString() method and return a String representation of an enum. It's also possible to enumerate an Enum type. Although all is achievable I wouldn't recommend doing it this way as things are overcomplicated for such a single task.
How about you create a Dictionary collection of the items and simply use Exist method and some sort of error handling (or simple if/else statements) to check whether whatever user inputs in the input box exists in your list.
For instance:
Sub Main()
Dim myList As Object
Set myList = CreateObject("Scripting.Dictionary")
myList.Add "item1", 1
myList.Add "item2", 2
myList.Add "item3", 3
Dim userInput As String
userInput = InputBox("Type something:")
If myList.Exists(userInput) Then
MsgBox userInput & " exists in the list"
Else
MsgBox userInput & " does not exist in the list"
End If
End Sub
Note: If you add references to Microsoft Scripting Runtime library you then will be able to use the intelli-sense with the myList object as it would have been early bound replacing
Dim myList As Object
Set myList = CreateObject("Scripting.Dictionary")
with
Dim myList as Dictionary
Set myList = new Dictionary
It's up to you which way you want to go about this and what is more convenient. Note that you don't need to add references if you go with the Late Binding while references are required if you want Early Binding with the intelli-sense.
Just for the sake of readers to be able to visualize the version using Enum let me demonstrate how this mechanism could possibly work
Enum EList
item1
item2
item3
[_Min] = item1
[_Max] = item3
End Enum
Function ToString(eItem As EList) As String
Select Case eItem
Case EList.item1
ToString = "item1"
Case EList.item2
ToString = "item2"
Case EList.item3
ToString = "item3"
End Select
End Function
Function Exists(userInput As String) As Boolean
Dim i As EList
For i = EList.[_Min] To EList.[_Max]
If userInput = ToString(i) Then
Exists = True
Exit Function
End If
Next
Exists = False
End Function
Sub Main()
Dim userInput As String
userInput = InputBox("type something:")
MsgBox Exists(userInput)
End Sub
First you declare your List as Enum. I have added only 3 items for the example to be as simple as possible. [_Min] and [_Max] indicate the minimum value and maximum value of enum (it's possible to tweak this but again, let's keep it simple for now). You declare them both to be able to iterate over your EList.
ToString() method returns a String representation of Enum. Any VBA developer realizes at some point that it's too bad VBA is missing this as a built in feature. Anyway, you've got your own implementation now.
Exists takes whatever userInput stores and while iterating over the Enum EList matches against a String representation of your Enum. It's an overkill because you need to call many methods and loop over the enum to be able to achieve what a simple Dictionary's Exists method does in one go. This is mainly why I wouldn't recommend using Enums for your specific problem.
Then in the end you have the Main sub which simply gathers the input from the user and calls the Exists method. It shows a Message Box with either true or false which indicates if the String exists as an Enum type.
Just use the Select Case with a list:
Select Case entry
Case item1,item2, ite3,item4 ' add up to limit for Case, add more Case if limit exceeded
do stuff for being in the list
Case Else
do stuff for not being in list
End Select

Excel VBA - Initializing Empty User Types and Detecting Nulls

I have created a user defined type to contain some data that I will use to populate my form. I am utilizing an array of that user defined type, and I resize that array as I pull data from an off-site server.
In order to make my program easier to digest, I have started to split it into subroutines. However, when my program is initialized, I cannot tell when a particular array has been initialized, and so I cannot be certain that I can call a size function to see if the array is empty.
Is there a way to initialize an empty user type or detect a null user type? Currently, I am hard-coding it in and I would prefer a more elegant solution.
In addition to isempty(array) solution -
If IsNull(array) then
msgbox "array is empty"
End If
AFAIK, you cannot check whether user-defined type was initialized before it was sent as an argument to a procedure/function.
I am quoting this example from VBA help
Type StateData
CityCode(1 To 100) As Integer ' Declare a static array.
County As String * 30
End Type
The County field is initialized to some value, which you can use a base value.
If the user sets this field explicitly, it means it holds some value & remains uninitialized, otherwise.
for e.g.
Sub main()
Dim example As StateData
MsgBox IsInitialized(example)
Dim example2 As StateData
example2.County = "LA"
MsgBox IsInitialized(example2)
End Sub
Function IsInitialized(arg As StateData) As Boolean
Dim initCounty As String * 30
IsInitialized = (arg.County <> initCounty)
End Function
Try:
dim v
if isempty(v) then
msgbox "is empty"
end if
If myObjectVariable is Nothing
should work to detect if an object has been initialized.
Edit: "is nothing" DOES work, if it is an object variable:
Dim blah As Object
If blah Is Nothing Then
MsgBox "blah is nothing!"
End If
Dim foo as variant
If IsEmpty(foo) Then
MsgBox "foo is empty!"
End If
If you need to check whether the whole dynamic array of custom types was initialized or not (not just particular element) in VBA, then this might not be possible directly (as none of the IsEmpty etc. functions works on custom types). However you might be able to easily restructure your program to return an array of custom types of size 0 to indicate that nothing was read/initialized.
Private Function doStuff() As customType()
Dim result() As customType
' immediately size it to 0 and assing it as result
ReDim result(0)
doStuff = vysledek
' do real stuff, ... premature "Exit Function" will return an array of size 0
' possibly return initialized values
End Function
' then you can all
If (UBound(tabulky) = 0) Then
MsgBox "Nope, it is not initialized."
End If