VBA Variant - IsNull and IsEmpty both false for empty array - vba

I have a VBA class that contains a number of variants. These variants are there to hold Y x Z size arrays, but they're optional inputs (and so often the variants will never get used or initialized).
A function within the same class calls on each of these variants to perform a calculation. BUT, I need it to skip the variants that are blank. When I try to use IsNull(xyz) and IsEmpty(xyz), both return false. Looking at the watch on xyz, it has:
Expression: xyz
Value:
Type: Variant/Variant()
Context: className
Totally blank under value.
Any ideas how I can test/return a boolean if these things are empty? Or even a boolean if they're full, that would work too.
Thanks
Edit: I should add, that IsMissing just completely crashes excel...

Very dirty workaround but something like this might do the trick:
Function IsEmptyArray(testArr As Variant) As Boolean
Dim test As Long
Dim ret As Boolean
ret = False
On Error Resume Next
test = UBound(testArr)
If Err.Number = 9 Then
ret = True
End If
Err.Clear
On Error GoTo 0
IsEmptyArray = ret
End Function

One method I've used in the past is to test whether or not the array is filled using a helper function. I join the array using an empty string delimiter and test that the length is greater than zero. It has worked in my circumstances, but I'm not sure if there are any flaws in the logic.
Below code returns true if the array is empty and false if it is not:
Function TestIfArrayIsEmpty(vArray As Variant) As Boolean
TestIfArrayIsEmpty = (Len(Join(vArray, "")) = 0)
End Function

You can use vartype(variable_name. You can check
vartype(varriable_name) = vbArray or VbEmpty or VbNull
Then check LBound and UBound of eac dimension
for example
LBound(variable, 1) and UBound(variable_name, 1) to check the lower and higher indexes of the array.

Related

VBA for Access- Compare function reporting "false" or #error everytime

I am developing an Access DB for a little project at work. We have an ODBC Database that has a list of certain servers entered into a web client. I tried writing a VBA function to take them, convert them to lowercase, and assign the string to "true" if they match, and "false" if not. I use the function in a query so you can search for ranges of server and so-on and so-forth.
Public Function InWhatsUp(field1 As Field, field2 As Field) As String
If (LCase(field1.Value) = LCase(field2.Value)) Then
InWhatsUp = "True"
Else
InWhatsUp = "False"
End If
Return
End Function
I haven't been able to find much relevant research besides using an IIF to compare, but the results all turn up as "false" there as well. Thank you in advance
A query can never pass objects like Field to a VBA function.
Use parameter data type String, or if the values can be NULL, Variant.
Since normal string comparison is always usually (see below) case-insensitive, you can then simply do
InWhatsUp = (str1 = str2)
or directly in the query:
Identical: IIf(server1 = server2, "True", "False")
If you need case-sensitive string comparison, you must use
StrComp(string1, string2, vbBinaryCompare)
Edit
normal string comparison is always case-insensitive
That's only true if you keep the Option Compare Database that is by default inserted in Access VBA modules.
If you change it to Option Compare Binary or remove it altogether, string comparisons are case-sensitive.
Thanks #ThunderFrame for the correction.
I'd make that function return a Boolean. Its job is to determine a Boolean result - whoever calls it can then decide to turn that Boolean into a String, but most callers would normally run Boolean logic with it, and if that's the case then returning a String requires VBA to perform implicit type conversions all over the place.
And since it's a string utility function, it doesn't even need to know about a Field - give it two Variant values so it can take a Null, and pass them by value:
Public Function AreTheSame(ByVal string1 As Variant, ByVal string2 As Variant) As Boolean
If IsNull(string1) Or IsNull(string2) Then Exit Function
AreTheSame = (StrComp(string1, string2, vbTextCompare) = 0)
End Function
Now you can test this function anywhere, with any input:
?AreTheSame(Null, "ABC") -> False
?AreTheSame(Null, Null) -> False
?AreTheSame("abc", "ABC") -> True
If you want to treat to Null values as equal, then you need to coalesce them into empty strings - and since null-coalescing is a concern of its own, I'd make another, separate function for that:
Public Function Coalesce(ByVal value As Variant) As String
If IsNull(value) Then
Coalesce = vbNullString
Else
Coalesce = CStr(value)
End If
End Function
Public Function AreTheSame(ByVal string1 As Variant, ByVal string2 As Variant) As Boolean
AreTheSame = (StrComp(Coalesce(string1), Coalesce(string2), vbTextCompare) = 0)
End Function
With that, two null values are treated as empty strings. I'd probably use an optional parameter to make that behavior configurable from the call site - ditto for case-sensitivity.
Public Function InWhatsUp(field1 As Variant, field2 As Variant) As Boolean
If IsNull(field1) Or IsNull(field2) Then Exit Function
If (Len(field1) = False) Or (Len(field2) = False) Then Exit Function
If IsError(field1) Or IsError(field2) Then Exit Function
InWhatsUp = StrComp(field1, field2, vbTextCompare)
End Function
Public Sub TestMe()
Debug.Print InWhatsUp("aaB", "aAb")
Debug.Print InWhatsUp("aaB", "abb")
Debug.Print InWhatsUp(Null, "")
End Sub
You do not have return in vba. Instead you refer to the function.
It is a good idea to make the function Boolean, if the only two values to return are True and False.
As far as you are working in Access, probably there could be one exception - if the two strings are "", probably you would like to see False, depending on your business logic. The IsError() is due to the same logic.
Checking whether the input is Null is something that is quite handy in Access. And following the logic of plenty of languages, a Null is never the same as another Null.

EXCEL VBA how to use functions and split to extract integer from string

I'm working on a piece of code to extract the nominal size of a pipeline from it's tagname. For example: L-P-50-00XX-0000-000. The 50 would be it's nominal size (2") which I would like to extract. I know I could do it like this:
TagnameArray() = Split("L-P-50-00XX-0000-000", "-")
DNSize = TagnameArray(2)
But I would like it to be a function because it's a small part of my whole macro and I don't need it for all the plants I'm working on just this one. My current code is:
Sub WBDA_XXX()
Dim a As Range, b As Range
Dim TagnameArray() As String
Dim DNMaat As String
Dim DN As String
Set a = Selection
For Each b In a.Rows
IntRow = b.Row
TagnameArray() = Split(Cells(IntRow, 2).Value, "-")
DN = DNMaat(IntRow, TagnameArray())
Cells(IntRow, 3).Value = DN
Next b
End Sub
Function DNMaat(IntRow As Integer, TagnameArray() As String) As Integer
For i = LBound(TagnameArray()) To UBound(TagnameArray())
If IsNumeric(TagnameArray(i)) = True Then
DNMaat = TagnameArray(i)
Exit For
End If
Next i
End Function
However this code gives me a matrix expected error which I don't know how to resolve. I would also like to use the nominal size in further calculations so it will have to be converted to an integer after extracting it from the tagname. Does anyone see where I made a mistake in my code?
This is easy enough to do with a split, and a little help from the 'Like' evaluation.
A bit of background on 'Like' - It will return TRUE or FALSE based on whether an input variable matches a given pattern. In the pattern [A-Z] means it can be any uppercase letter between A and Z, and # means any number.
The code:
' Function declared to return variant strictly for returning a Null string or a Long
Public Function PipeSize(ByVal TagName As String) As Variant
' If TagName doesn't meet the tag formatting requirements, return a null string
If Not TagName Like "[A-Z]-[A-Z]-##-##[A-Z]-####-###" Then
PipeSize = vbNullString
Exit Function
End If
' This will hold our split pipecodes
Dim PipeCodes As Variant
PipeCodes = Split(TagName, "-")
' Return the code in position 2 (Split returns a 0 based array by default)
PipeSize = PipeCodes(2)
End Function
You will want to consider changing the return type of the function depending on your needs. It will return a null string if the input tag doesnt match the pattern, otherwise it returns a long (number). You can change it to return a string if needed, or you can write a second function to interpret the number to it's length.
Here's a refactored version of your code that finds just the first numeric tag. I cleaned up your code a bit, and I think I found the bug as well. You were declaring DNMAAT as a String but also calling it as a Function. This was likely causing your Array expected error.
Here's the code:
' Don't use underscores '_' in names. These hold special value in VBA.
Sub WBDAXXX()
Dim a As Range, b As Range
Dim IntRow As Long
Set a = Selection
For Each b In a.Rows
IntRow = b.Row
' No need to a middleman here. I directly pass the split values
' since the middleman was only used for the function. Same goes for cutting DN.
' Also, be sure to qualify these 'Cells' ranges. Relying on implicit
' Activesheet is dangerous and unpredictable.
Cells(IntRow, 3).value = DNMaat(Split(Cells(IntRow, 2).value, "-"))
Next b
End Sub
' By telling the function to expect a normal variant, we can input any
' value we like. This can be dangerous if you dont anticipate the errors
' caused by Variants. Thus, I check for Arrayness on the first line and
' exit the function if an input value will cause an issue.
Function DNMaat(TagnameArray As Variant) As Long
If Not IsArray(TagnameArray) Then Exit Function
Dim i As Long
For i = LBound(TagnameArray) To UBound(TagnameArray)
If IsNumeric(TagnameArray(i)) = True Then
DNMaat = TagnameArray(i)
Exit Function
End If
Next i
End Function
The error matrix expected is thrown by the compiler because you have defined DNMaat twice: Once as string variable and once as a function. Remove the definition as variable.
Another thing: Your function will return an integer, but you assigning it to a string (and this string is used just to write the result into a cell). Get rid of the variable DN and assign it directly:
Cells(IntRow, 3).Value = DNMaat(IntRow, TagnameArray())
Plus the global advice to use option explicit to enforce definition of all used variables and to define a variable holding a row/column number always as long and not as integer

Define function that can take null parameter

I'm was recently trying to redefine Access's Nz(Value, [ValueIfNull]) function in Excel, because I find it pretty useful yet it's absent in Excel (as I found to my disappointment when moving a few useful functions over). Access's Nz function checks Value - if this value is null, it returns ValueIfNull (otherwise it returns Value). In Access it's useful for checking the value of input boxes (amongst several other things):
If Nz(myTextBox.Value, "") = "" Then
MsgBox "You need to enter something!"
End If
Rolling my own Nz function didn't seem difficult:
Public Function Nz(value As Variant, Optional valueIfNull As Variant = "") As Variant
If IsNull(value) Then
Nz = valueIfNull
Else
Nz = value
End If
End Function
But as soon as I try to call it with anything that's actually null, Excel complains about it on the calling line (Run-time error '91': Object variable or With block not set, which I understand to be roughly equivilant to a NullReferenceException in other languages), before even getting to the Nz function body. For example Nz(someObj.Value, "") will only work if someObj.Value isn't null (rendering the function entirely moot).
Am I missing some detail of VBA here? Coming from languages like VB.NET, it seems very confusing - I understand object references to be simply addresses to an actual object residing in memory, and so passing around the reference (not the object) shouldn't cause issue (until you try to actually do something with the non-existant object, of course). For eg:
Dim myObj As SomeObject
SomeMethod(myObj) 'the call itself is fine
Public Sub SomeMethod(SomeObject obj)
myObj.DoSomething() 'but *here* it would crash
End Sub
How can you create subs and functions in VBA that will accept a null parameter?
see this and that if anything is still unclear and try
Sub Main()
Dim obj As Range
Debug.Print Nz(obj)
Dim v As Variant
v = Null
Debug.Print Nz(v)
End Sub
Public Function Nz(value As Variant, Optional valueIfNull As Variant = "") As Variant
' deal with an object data type, vbObject = 9
If VarType(value) = vbObject Then
If value Is Nothing Then
Nz = valueIfNull
Else
Nz = value
End If
' deal with variant set to null, vbNull is a Variant set to null
ElseIf VarType(value) = vbNull Then
If IsNull(value) Then
Nz = valueIfNull
Else
Nz = value
End If
End If
End Function

How to check whether a variant array is unallocated?

Dim Result() As Variant
In my watch window, this appears as
Expression | Value | Type
Result | | Variant/Variant()
How do I check the following:
if Result is nothing then
or
if Result is Not Set then
This is basically what I am trying to accomplish, but the first one does not work and the second does not exist.
To avoid error handling, I used this, seen on a forum long time ago and used sucessfully since then:
If (Not Not Result) <> 0 Then 'Means it is allocated
or alternatively
If (Not Not Result) = 0 Then 'Means it is not allocated
I used this mainly to extend array size from unset array this way
'Declare array
Dim arrIndex() As Variant
'Extend array
If (Not Not Result) = 0 Then
ReDim Preserve Result(0 To 0)
Else
ReDim Preserve Result(0 To UBound(Result) + 1)
End If
Chip Pearson made a useful module called modArraySupport that contains a bunch of functions to test for things like this. In your case, you would want to use IsArrayAllocated.
Public Function IsArrayAllocated(Arr As Variant) As Boolean
This function returns TRUE or FALSE indicating whether the specified array is allocated (not empty). Returns TRUE of the
array is a static array or a dynamic that has been allocated with a Redim statement. Returns FALSE if the array is a dynamic array that
has not yet been sized with ReDim or that has been deallocated with the Erase statement. This function is basically the opposite of
ArrayIsEmpty. For example,
Dim V() As Variant
Dim R As Boolean
R = IsArrayAllocated(V) ' returns false
ReDim V(1 To 10)
R = IsArrayAllocated(V) ' returns true
The technique used is basically to test the array bounds (as suggested by #Tim Williams) BUT with an extra gotcha.
To test in your immediate window:
?IsArrayAllocated(Result)
Testing in Watch window: there are may ways to do this; for example, add a watch on R and under "Watch Type" select "Break When Value Changes".
You can use the following in the immediate window:
?Result Is Nothing
?IsNull( Result )
?IsEmpty( Result )
?IsMissing( Result )
The first is simply for completeness. Since Result is not an object, Result Is Nothing will throw an error. Empty is for variants that have not been initialized including arrays which have not been dimensioned..
(Update) In doing some additional checking, I have discovered that IsEmpty will never return true on a declared array (whether Redim'd or not) with only one exception. The only exception I found is when the array is declared at the module level and not as Public and then only when you check it in the immediate window.
Missing if for optional values passed to a function or sub. While you cannot declare Optional Foo() As Variant, you could have something like ParamArray Foo() As Variant in which case if nothing is passed, IsMissing would return true.
Thus, the only way to determine if the array is initialized is to write a procedure that would check:
Public Function IsDimensioned(vValue As Variant) As Boolean
On Error Resume Next
If Not IsArray(vValue) Then Exit Function
Dim i As Integer
i = UBound(Bar)
IsDimensioned = Err.Number = 0
End Function
Btw, it should be noted that this routine (or the library posted by Jean-François Corbett) will return false if the array is dimensioned and then erased.
I recommend a slightly different approach because I think using language artifacts like (Not Array) = -1 to check for initialization is difficult to read and causes maintenance headaches.
If you are needing to check for array allocation, most likely it's because you're trying to make your own "vector" type: an array that grows during runtime to accommodate data as it is being added. VBA makes it fairly easy to implement a vector type, if you take advantage of the type system.
Type Vector
VectorData() As Variant
VectorCount As Long
End Type
Dim MyData As Vector
Sub AddData(NewData As Variant)
With MyData
' If .VectorData hasn't been allocated yet, allocate it with an
' initial size of 16 elements.
If .VectorCount = 0 Then ReDim .VectorData(1 To 16)
.VectorCount = .VectorCount + 1
' If there is not enough storage for the new element, double the
' storage of the vector.
If .VectorCount > UBound(.VectorData) Then
ReDim Preserve .VectorData(1 To UBound(.VectorData) * 2)
End If
.VectorData(.VectorCount) = NewData
End With
End Sub
' Example of looping through the vector:
For I = 1 To MyData.VectorCount
' Process MyData.VectorData(I)
Next
Notice how there's no need to check for array allocation in this code, because we can just check the VectorCount variable. If it's 0, we know that nothing has been added to the vector yet and therefore the array is unallocated.
Not only is this code simple and straightforward, vectors also have all the performance advantages of an array, and the amortized cost for adding elements is actually O(1), which is very efficient. The only tradeoff is that, due to how the storage is doubled every time the vector runs out of space, in the worst case 50% of the vector's storage is wasted.
Check the LBound of the array. If you get an error then it's uninitialized.

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