vb6 array with -1 for upper bound - arrays

Some functions such as Split() will return an array with -1 for the upper bound and zero for the lower bound if the array has no items, eg:
Dim s() As String
s = Split("", ",")
Debug.Print UBound(s)
Debug.Pring LBound(s)
In this case UBound(s) will equal -1 and LBound(s) will equal 0. I have a fair amount of code checking for -1 on the upper bound to see if the array has values or not. This works great.
The problem is that I now want to change the array data type from string to long. I cannot seem to create an array of longs with an upper bound of -1 and a lower bound of 0, and the Split() and Join() functions only operate on string arrays.
I would like to be able to return a long array with an upper bound of -1. Is this possible?

I don't think you can do it in VB6 it self. However, if you're willing to use the Windows API function SafeArrayCreateVector you can do it:
Private Declare Function LongSplitEmulator Lib "OLEAUT32.DLL" Alias "SafeArrayCreateVector" _
(Optional ByVal vt As VbVarType = vbLong, _
Optional ByVal low As Long = 0, _
Optional ByVal count As Long = 0) As Long()
Dim a() As Long
a = LongSplitEmulator()
MsgBox UBound(a)
If you need to do it for other datatypes you can change the vt parameter.
Please note, I think I originally found out about this from Vi2's answer to this discussion.

You could write your own split function to do this:
Private Sub SplitLongs(ByVal strData As String, ByRef lng() As Long)
Dim i As Integer
Dim s() As String
s = Split(strData, ",")
If UBound(s) = -1 Then
ReDim lng(-1 To -1)
Else
ReDim lng(LBound(s) To UBound(s))
For i = LBound(s) To UBound(s)
If IsNumeric(s(i)) Then lng(i) = s(i)
Next
End If
End Sub

One problem with VB6 is there is no way to reliably create or detect an empty (or uninitialized) array. Sometimes, it is possible to detect an uninitialized array by checking whether the upper-bound is greater than the lower-bound; however, this is neither elegant nor documented. The best way to accomplish such a thing properly is to enclose the array in a Variant, and set the Variant to Empty to deinitialize the array. You may then use a check such as If VarType(v) = vbEmpty ...

Another way is a strongly typed "factory" function:
Private Declare Function SafeArrayRedim Lib "oleaut32.dll" (ByVal ArrayPtr As Long, ByRef DataPtr As tagSAFEARRAYBOUND) As Long
Private Type tagSAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Public Type Feed
ID As String
Name As String
Active As Boolean
BasePath As String
End Type
Public Sub EmptyFeedArray(ByRef Arr() As Feed)
Dim Data As tagSAFEARRAYBOUND
Dim lngErr As Long
'Redim to one item
ReDim Arr(0 To 0)
'Reset the safe array to empty
lngErr = SafeArrayRedim(Not Not Arr, Data)
'Raise any errors
If lngErr <> 0 Then Err.Raise lngErr
End Sub
I think this also works with integral types.

Related

Create an empty 2d array

I don't like uninitialized VBA arrays, since it's necessary to check if array is initialized, each time prior using UBound() or For Each to avoid an exception, and there is no native VBA function to check it. That is why I initialize arrays, at least doing them empty with a = Array(). This eliminates the need for extra check in most of cases, so there are no problems with 1d arrays.
For the same reason I tried to create an empty 2d array. It's not possible simply do ReDim a(0 To -1, 0 To 0), transpose 1d empty array or something similar. The only way I came across by chance, is to use MSForms.ComboBox, assign empty array to .List property and read it back. Here is the example, which works in Excel and Word, you need to insert UserForm to VBA Project, place ComboBox on it, and add the below code:
Private Sub ComboBox1_Change()
Dim a()
ComboBox1.List = Array()
a = ComboBox1.List
Debug.Print "1st dimension upper bound = " & UBound(a, 1)
Debug.Print "2nd dimension upper bound = " & UBound(a, 2)
End Sub
After combo change the output is:
1st dimension upper bound = -1
2nd dimension upper bound = 0
Actually it's really the empty 2d array in debug:
Is there more elegant way to create an empty 2d array, without using ComboBox, or UserForm controls in general?
This is only going to work for Windows (not for Mac):
Option Explicit
#If Mac Then
#Else
#If VBA7 Then
Private Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
Private Declare PtrSafe Function VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any) As Long
Private Declare PtrSafe Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As LongPtr) As Long
#Else
Private Declare Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As Long
Private Declare Function VariantCopy Lib "OleAut32.dll" (pvargDest As Variant, pvargSrc As Any) As Long
Private Declare Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As Long) As Long
#End If
#End If
Private Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Private Type tagVariant
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
#If VBA7 Then
ptr As LongPtr
#Else
ptr As Long
#End If
End Type
Public Function EmptyArray(ByVal numberOfDimensions As Long, ByVal vType As VbVarType) As Variant
'In Visual Basic, you can declare arrays with up to 60 dimensions
Const MAX_DIMENSION As Long = 60
If numberOfDimensions < 1 Or numberOfDimensions > MAX_DIMENSION Then
Err.Raise 5, "EmptyArray", "Invalid number of dimensions"
End If
#If Mac Then
Err.Raise 298, "EmptyArray", "OleAut32.dll required"
#Else
Dim bounds() As SAFEARRAYBOUND
#If VBA7 Then
Dim ptrArray As LongPtr
#Else
Dim ptrArray As Long
#End If
Dim tVariant As tagVariant
Dim i As Long
'
ReDim bounds(0 To numberOfDimensions - 1)
'
'Make lower dimensions [0 to 0] instead of [0 to -1]
For i = 1 To numberOfDimensions - 1
bounds(i).cElements = 1
Next i
'
'Create empty array and store pointer
ptrArray = SafeArrayCreate(vType, numberOfDimensions, bounds(0))
'
'Create a Variant pointing to the array
tVariant.vt = vbArray + vType
tVariant.ptr = ptrArray
'
'Copy result
VariantCopy EmptyArray, tVariant
'
'Clean-up
SafeArrayDestroy ptrArray
#End If
End Function
You can now create empty arrays with different number of dimensions and data types:
Sub Test()
Dim arr2D() As Variant
Dim arr4D() As Double
'
arr2D = EmptyArray(2, vbVariant)
arr4D = EmptyArray(4, vbDouble)
Stop
End Sub
Update 30/09/2022
I've created an EmptyArray method (same signature) in my MemoryTools library on GitHub. That version will work on both Windows and Mac.
Idk man - I think you stumbling onto this property was pretty wild.
I'd probably stop here and just do:
Function Empty2DArray() As Variant
With CreateObject("Forms.ComboBox.1")
.List = Array()
Empty2DArray = .List
End With
End Function
And use it like: a = Empty2DArray
You don't need to create the userform or combobox - you can just use CreateObject.
But as others have said, it probably makes more sense to do error handling when checking whether or not your arrays are initialized.

array push - Write a VBA function that accepts any type of array

I'm trying to write a simple push function that can add an element to my VBA arrays.
I can't figure out how to allow it to accept typed arrays. So far, I can get the function to accept arrays explicitly typed as "Variant". See below
Function push(arr() As Variant, el As Variant) As Variant()
' function to redim an array and increment it by one. If array is length 1 and element is empty, it will place the "el" param in the first index
If IsEmpty(arr(UBound(arr))) And UBound(arr) = LBound(arr) Then
arr(UBound(arr)) = el
Else
ReDim Preserve arr(LBound(arr) To UBound(arr) + 1)
arr(UBound(arr)) = el
End If
push = arr
End Function
Sub testPush()
Dim myArr() As Variant
Dim newArr() As Variant
myArr = Array("apple", "banana", "4")
myArr = push(myArr, "coconut")
Debug.Print Join(myArr, ", ")
newArr = Array(1, 2, 3, 4)
newArr = push(newArr, 7)
Debug.Print Join(newArr, ", ")
End Sub
When I dimension myArr as string, e.g., Dim myArr() as String I see a compile error in my push function: Type mismatch: array or user defined type expected. Is there any way I can use a single function that attempts to increment an element to the end of an array, regardless of the array type?
Contrary to what I write below, maybe someone who knows more than me will tell you this is possible and exactly how you can do this.
I see that your function returns an array of variants:
Function push(arr() As Variant, el As Variant) As Variant()
which, if I understand correctly, can only be assigned to a variable of type Variant or Variant().
If I change the function to:
Function push(arr As Variant, el As Variant) As Variant
I think the function can now accept an array of any type1 but it still returns a Variant -- which I believe can't be assigned to every typed array (meaning you'll still get a compiler error in some cases).
What might be an easier approach is to change the Function to a Sub and have the subroutine modify the array in-place. That way there is no assignment at the call site (since subroutines do not return values) and the code should compile. This also means any type error2 will now occur at run-time.
Also, it's not clear to me what the point of the below is:
If IsEmpty(arr(UBound(arr)))
It seems like you're checking if the array's first element has been assigned something other than its default initialised value. But that check seems like it will only work for Variant types (which are initialised as empty). I think strings are initialised as "", numbers are initialised as 0, objects are initialised as Nothing, and so on. In short, I think the IsEmpty check may return false negative for types other than Variant. The aforementioned behaviour might be what you want, or it might not be (given that you say you want this Push code to work with arrays of any type).
Overall, one approach could be something like:
Option Explicit
Sub Push(ByRef someArray As Variant, ByVal someElement As Variant)
' This routine expects array to be 1-dimensional.
' and will modify the array in-place.
' The array must by dynamic (cannot Redim an array that
' was statically declared).
Dim lastIndex As Long
lastIndex = UBound(someArray)
Dim arrayNeedsExtending As Boolean
' If you are using "IsEmpty" to work out if the first element
' has been assigned a value other than its default value at initialisation
' then you may need to see: https://stackoverflow.com/a/3331239/8811778
' as "IsEmpty" might only work for Variants and may return false
' negatives otherwise.
arrayNeedsExtending = (lastIndex <> LBound(someArray)) Or Not IsEmpty(someArray(lastIndex))
If arrayNeedsExtending Then
ReDim Preserve someArray(LBound(someArray) To (lastIndex + 1))
End If
' If you have an array of objects (hypothetically, instead of a collection), the line below
' will raise a syntax error since Set keyword is required for objects.
someArray(UBound(someArray)) = someElement
End Sub
Private Sub TestPush()
Dim someStrings() As String
someStrings = Split("a,a", ",", -1, vbBinaryCompare)
Push someStrings, "b"
Debug.Assert (JoinArray(someStrings) = "a,a,b")
Dim someBooleans() As Boolean ' Can only Push dynamic arrays
ReDim someBooleans(0 To 1)
Push someBooleans, True
Debug.Assert (JoinArray(someBooleans) = "False,False,True")
Dim someZeros() As Long
ReDim someZeros(0 To 1)
Push someZeros, 0
Debug.Assert (JoinArray(someZeros) = "0,0,0")
End Sub
Private Function JoinArray(ByRef someArray As Variant, Optional delimiter As String = ",") As String
' Expects array to be 1-dimensional.
' Attempts to handle non-string types albeit without error handling.
Dim toJoin() As String
ReDim toJoin(LBound(someArray) To UBound(someArray))
Dim arrayIndex As Long
For arrayIndex = LBound(someArray) To UBound(someArray)
toJoin(arrayIndex) = CStr(someArray(arrayIndex)) ' Will throw if type cannot be coerced into string.
Next arrayIndex
JoinArray = Join(toJoin, delimiter)
End Function
1 Maybe not user-defined types though.
2 Say you pass the string"a" to be pushed into a Long-typed array -- or any value that cannot be type-coerced into the array's type.

Value in array cannot be updated VBA Excel

I am trying to get the nth similar of a range compare to a text. The Similarity function is to return a percentage similarity between 2 texts.
Function SimilarText(ByVal CompareText As String, _
ByRef TargetCompare As Range, _
Optional ByVal RankSimilarity As Integer = 1) As Long
Dim compareResults() As Long
ReDim compareResults(RankSimilarity)
Dim simiResult As Single
Dim smallestIndex As Integer
Dim result As String
Debug.Print (CompareText)
For Each cell In TargetCompare
simiResult = Similarity(cell.Value, CompareText)
Debug.Print (simiResult)
If simiResult > Application.Min(compareResults) Then
smallestIndex = Application.Match(Application.Min(compareResults), compareResults, 0) - 1
Debug.Print ("Index:" & smallestIndex)
compareResults(smallestIndex) = CLng(simiResult)'//This doesnt seem to do anything. I tried without the conversion but still nothing.
Debug.Print ("Smallest after update:" & compareResults(smallestIndex)) '//This always 0
End If
Next cell
SimilarText = Application.Min(compareResults) '//So this is also alway 0
End Function
I would expect the array element will be updated after each cell like '(0.22,0.44) but the result seems to always be 0.
Your array compareResults() is defined as Long, and that's only for integer numbers.
Long data type (Visual Basic)
Change data type to something that admits decimal.
Also, in line compareResults(smallestIndex) = CLng(simiResult), you are forcing a number to become a long integer. Use somethin else, or it will save a long integer number.

Type mismatch when changing argument types passed to a function

Situation:
I have a function, RemoveEmptyArrRowCol, which accepts two arguments, one of which is an array (tempArr).
When the second argument was a Long everything was fine. When I changed the second argument to a String (and the associated call variable) I got:
Type mismatch (Error 13)
So in the below code examples:
Test1 runs fine
Test2 fails
Questions:
1) Why is the behaviour different between the two?
2) How do I fix the second version so it behaves as per the first?
3) How will I future proof this for when I pass a dictionary value (array) as the 1st parameter, rather than reading directly from the worksheet?
What I have tried:
This appears to be a common question on SO and I have looked at a number of these questions; some of which I have put as references at the bottom of this question. I still, however, have not resolved why the first of these two sub works, but the second does not?
I played around with different combinations of:
Adding extra brackets
Explicitly declaring the type of tempArr: Dim tempArr() As Variant
Changing part of the function signature: ByRef tempArr() As Variant
After looking at #Fionnuala's answer to this question, MS Access/VBA type mismatch when passing arrays to function/subroutine, I decided to try using Call:
Call RemoveEmptyArrRowCol2(ws.Range("C4:I129").Value, tempStr)
This compiled but means I would need to change other parts of my code to ensure tempArr is correctly populated. If I were to do it this way, I might as well convert the function to a procedure.
As is, the flow is that I populate tempArr, in the test example, direct from the sheet and then hand off to another sub i.e.
tempArr = RemoveEmptyArrRowCol(ws.Range("C4:I129").Value, tempStr)
ArrayToSheet wb.Worksheets("Test").Range("A1"), tempArr
Note re: Question 3:
In the final version, I will be passing an array, pulled from a dictionary, as the first parameter i.e.
tempArr = RemoveEmptyArrRowCol( ArrayDict(tempStr), tempStr)
Working version:
Option Explicit
Public Sub Test1()
Dim tempArr() 'variant
Init
Dim tempStr As String: tempStr = "Response Times"
tempArr = RemoveEmptyArrRowCol(ws.Range("C4:I129").Value, categoryDict(tempStr & "Cols"))
End Sub
Private Function RemoveEmptyArrRowCol(ByRef tempArr As Variant, ByVal nCols As Long) As Variant
End Function
Failing version:
Public Sub Test2()
Dim tempArr()
Init
Dim tempStr As String: tempStr = "Response Times"
tempArr = RemoveEmptyArrRowCol2(ws.Range("C4:I129").Value, tempStr)
End Sub
Private Function RemoveEmptyArrRowCol2(ByRef tempArr As Variant, ByVal tempStr As String) As Variant
End Function
Example of the current full function:
Private Function RemoveEmptyArrRowCol(ByRef tempArr As Variant, ByVal tempStr As String) As Variant
Dim i As Long
Dim j As Long
Dim counter As Long
counter = 0
Dim tempArr2()
Dim totCol As Long
Dim adjColTotal As Long
totCol = categoryDict(tempStr & "Cols")
adjColTotal = categoryDict(tempStr & "ColsAdj")
Select Case tempStr
Case "ResponseTimes", "NoCCPR"
ReDim tempArr2(1 To 1000, 1 To adjColTotal)
For i = 1 To UBound(tempArr, 1)
If tempArr(i, 2) <> vbNullString Then 'process row
counter = counter + 1 'load row to temp array (counter becomes row count)
For j = 1 To totCol
Select Case j
Case Is < 4
tempArr2(counter, j) = tempArr(i, j)
Case Is > 4
tempArr2(counter, j - 1) = tempArr(i, j)
End Select
Next j
End If
Next i
RemoveEmptyArrRowCol = RedimArrDimOne(tempArr2, adjColTotal, counter)
Case "Incidents"
End Select
End Function
Additional references:
1) Passing arrays to functions in vba
2) Passing array to function returns compile error
3) Type mismatch error when passing arrays to a function in excel vba
4) Should I use Call keyword in VBA
It really depends on your input and output, e.g. what is in the RemoveEmptyArrRowCol2 function. This is an option, in which tempStr as String is not failing:
Public Sub Test2()
Dim tempArr()
Dim tempStr As String: tempStr = "Response Times"
tempArr = RemoveEmptyArrRowCol2(Range("C4:I129").Value, tempStr)
End Sub
Private Function RemoveEmptyArrRowCol2(ByRef tempArr As Variant, _
ByVal tempStr As String) As Variant
RemoveEmptyArrRowCol2 = Array(1, 2)
End Function
E.g., if you remove the returning value (Array(1,2), it fails) but it should fail, because it does not return anything.
Define "Dim tempArr As Variant" not as Variant-Array with "()"
Please show us your function "categoryDict" and "ArrayDict"
"Call" is not nessesary!
You access Values as follows:
Dim r As Long
Dim c As Long
For r = 1 To UBound(tempArr, 1)
For c = 1 To UBound(tempArr, 2)
Debug.Print tempArr(r, c)
Next
Next

How do I determine if an array is initialized in VB6?

Passing an undimensioned array to the VB6's Ubound function will cause an error, so I want to check if it has been dimensioned yet before attempting to check its upper bound. How do I do this?
Note: the code has been updated, the original version can be found in the revision history (not that it is useful to find it). The updated code does not depend on the undocumented GetMem4 function and correctly handles arrays of all types.
Note for VBA users: This code is for VB6 which never got an x64 update. If you intend to use this code for VBA, see https://stackoverflow.com/a/32539884/11683 for the VBA version. You will only need to take the CopyMemory declaration and the pArrPtr function, leaving the rest.
I use this:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(ByRef Destination As Any, ByRef Source As Any, ByVal length As Long)
Private Const VT_BYREF As Long = &H4000&
' When declared in this way, the passed array is wrapped in a Variant/ByRef. It is not copied.
' Returns *SAFEARRAY, not **SAFEARRAY
Public Function pArrPtr(ByRef arr As Variant) As Long
'VarType lies to you, hiding important differences. Manual VarType here.
Dim vt As Integer
CopyMemory ByVal VarPtr(vt), ByVal VarPtr(arr), Len(vt)
If (vt And vbArray) <> vbArray Then
Err.Raise 5, , "Variant must contain an array"
End If
'see https://msdn.microsoft.com/en-us/library/windows/desktop/ms221627%28v=vs.85%29.aspx
If (vt And VT_BYREF) = VT_BYREF Then
'By-ref variant array. Contains **pparray at offset 8
CopyMemory ByVal VarPtr(pArrPtr), ByVal VarPtr(arr) + 8, Len(pArrPtr) 'pArrPtr = arr->pparray;
CopyMemory ByVal VarPtr(pArrPtr), ByVal pArrPtr, Len(pArrPtr) 'pArrPtr = *pArrPtr;
Else
'Non-by-ref variant array. Contains *parray at offset 8
CopyMemory ByVal VarPtr(pArrPtr), ByVal VarPtr(arr) + 8, Len(pArrPtr) 'pArrPtr = arr->parray;
End If
End Function
Public Function ArrayExists(ByRef arr As Variant) As Boolean
ArrayExists = pArrPtr(arr) <> 0
End Function
Usage:
? ArrayExists(someArray)
Your code seems to do the same (testing for SAFEARRAY** being NULL), but in a way which I would consider a compiler bug :)
I just thought of this one. Simple enough, no API calls needed. Any problems with it?
Public Function IsArrayInitialized(arr) As Boolean
Dim rv As Long
On Error Resume Next
rv = UBound(arr)
IsArrayInitialized = (Err.Number = 0)
End Function
Edit: I did discover a flaw with this related to the behavior of the Split function (actually I'd call it a flaw in the Split function). Take this example:
Dim arr() As String
arr = Split(vbNullString, ",")
Debug.Print UBound(arr)
What is the value of Ubound(arr) at this point? It's -1! So, passing this array to this IsArrayInitialized function would return true, but attempting to access arr(0) would cause a subscript out of range error.
Here's what I went with. This is similar to GSerg's answer, but uses the better documented CopyMemory API function and is entirely self-contained (you can just pass the array rather than ArrPtr(array) to this function). It does use the VarPtr function, which Microsoft warns against, but this is an XP-only app, and it works, so I'm not concerned.
Yes, I know this function will accept anything you throw at it, but I'll leave the error checking as an exercise for the reader.
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(pDst As Any, pSrc As Any, ByVal ByteLen As Long)
Public Function ArrayIsInitialized(arr) As Boolean
Dim memVal As Long
CopyMemory memVal, ByVal VarPtr(arr) + 8, ByVal 4 'get pointer to array
CopyMemory memVal, ByVal memVal, ByVal 4 'see if it points to an address...
ArrayIsInitialized = (memVal <> 0) '...if it does, array is intialized
End Function
I found this:
Dim someArray() As Integer
If ((Not someArray) = -1) Then
Debug.Print "this array is NOT initialized"
End If
Edit: RS Conley pointed out in his answer that (Not someArray) will sometimes return 0, so you have to use ((Not someArray) = -1).
Both methods by GSerg and Raven are undocumented hacks but since Visual BASIC 6 is no longer being developed then it is not a issue. However Raven's example doesn't work on all machines. You have to test like this.
If (Not someArray) = -1 Then
On some machines it will return a zero on others some large negative number.
In VB6 there is a function called "IsArray", but it does not check if the array has been initialized. You will receive Error 9 - Subscript out of range if you attempt to use UBound on an uninitialized array. My method is very similar to S J's, except it works with all variable types and has error handling. If a non-array variable is checked, you will receive Error 13 - Type Mismatch.
Private Function IsArray(vTemp As Variant) As Boolean
On Error GoTo ProcError
Dim lTmp As Long
lTmp = UBound(vTemp) ' Error would occur here
IsArray = True: Exit Function
ProcError:
'If error is something other than "Subscript
'out of range", then display the error
If Not Err.Number = 9 Then Err.Raise (Err.Number)
End Function
Since wanted comment on here will post answer.
Correct answer seems is from #raven:
Dim someArray() As Integer
If ((Not someArray) = -1) Then
Debug.Print "this array is NOT initialized"
End If
When documentation or Google does not immediately return an explanation people tend to call it a hack.
Although what seems to be the explanation is that Not is not only a Logical, it is also a Bitwise operator, so it handles the bit representation of structures, rather than Booleans only.
For example of another bitwise operation is here:
Dim x As Integer
x = 3 And 5 'x=1
So the above And is also being treated as a bitwise operator.
Furthermore, and worth to check, even if not the directly related with this,
The Not operator can be overloaded, which means that a class or
structure can redefine its behavior when its operand has the type of
that class or structure.
Overloading
Accordingly, Not is interpreting the array as its bitwise representation and it distinguishes output when array is empty or not like differently in the form of signed number. So it can be considered this is not a hack, is just an undocumentation of the array bitwise representation, which Not here is exposing and taking advantage of.
Not takes a single operand and inverts all the bits, including the
sign bit, and assigns that value to the result. This means that for
signed positive numbers, Not always returns a negative value, and for
negative numbers, Not always returns a positive or zero value.
Logical Bitwise
Having decided to post since this offered a new approach which is welcome to be expanded, completed or adjusted by anyone who has access to how arrays are being represented in their structure. So if anyone offers proof it is actually not intended for arrays to be treated by Not bitwise we should accept it as not a hack and actually as best clean answer, if they do or do not offer any support for this theory, if it is constructive comment on this is welcome of course.
This is modification of raven's answer. Without using API's.
Public Function IsArrayInitalized(ByRef arr() As String) As Boolean
'Return True if array is initalized
On Error GoTo errHandler 'Raise error if directory doesnot exist
Dim temp As Long
temp = UBound(arr)
'Reach this point only if arr is initalized i.e. no error occured
If temp > -1 Then IsArrayInitalized = True 'UBound is greater then -1
Exit Function
errHandler:
'if an error occurs, this function returns False. i.e. array not initialized
End Function
This one should also be working in case of split function.
Limitation is you would need to define type of array (string in this example).
When you initialite the array put an integer or boolean with a flag = 1. and query this flag when you need.
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function ArrPtr Lib "msvbvm60" Alias "VarPtr" (arr() As Any) As Long
Private Type SafeArray
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
Private Function ArrayInitialized(ByVal arrayPointer As Long) As Boolean
Dim pSafeArray As Long
CopyMemory pSafeArray, ByVal arrayPointer, 4
Dim tArrayDescriptor As SafeArray
If pSafeArray Then
CopyMemory tArrayDescriptor, ByVal pSafeArray, LenB(tArrayDescriptor)
If tArrayDescriptor.cDims > 0 Then ArrayInitialized = True
End If
End Function
Usage:
Private Type tUDT
t As Long
End Type
Private Sub Form_Load()
Dim longArrayNotDimmed() As Long
Dim longArrayDimmed(1) As Long
Dim stringArrayNotDimmed() As String
Dim stringArrayDimmed(1) As String
Dim udtArrayNotDimmed() As tUDT
Dim udtArrayDimmed(1) As tUDT
Dim objArrayNotDimmed() As Collection
Dim objArrayDimmed(1) As Collection
Debug.Print "longArrayNotDimmed " & ArrayInitialized(ArrPtr(longArrayNotDimmed))
Debug.Print "longArrayDimmed " & ArrayInitialized(ArrPtr(longArrayDimmed))
Debug.Print "stringArrayNotDimmed " & ArrayInitialized(ArrPtr(stringArrayNotDimmed))
Debug.Print "stringArrayDimmed " & ArrayInitialized(ArrPtr(stringArrayDimmed))
Debug.Print "udtArrayNotDimmed " & ArrayInitialized(ArrPtr(udtArrayNotDimmed))
Debug.Print "udtArrayDimmed " & ArrayInitialized(ArrPtr(udtArrayDimmed))
Debug.Print "objArrayNotDimmed " & ArrayInitialized(ArrPtr(objArrayNotDimmed))
Debug.Print "objArrayDimmed " & ArrayInitialized(ArrPtr(objArrayDimmed))
Unload Me
End Sub
Based on all the information I read in this existing post this works the best for me when dealing with a typed array that starts as uninitialized.
It keeps the testing code consistent with the usage of UBOUND and It does not require the usage of error handling for testing.
It IS dependent on Zero Based Arrays (which is the case in most development).
Must not use "Erase" to clear the array. use alternative listed below.
Dim data() as string ' creates the untestable holder.
data = Split(vbNullString, ",") ' causes array to return ubound(data) = -1
If Ubound(data)=-1 then ' has no contents
' do something
End If
redim preserve data(Ubound(data)+1) ' works to increase array size regardless of it being empty or not.
data = Split(vbNullString, ",") ' MUST use this to clear the array again.
The easiest way to handle this is to insure that the array is initialized up front, before you need to check for the Ubound. I needed an array that was declared in the (General) area of the form code.
i.e.
Dim arySomeArray() As sometype
Then in the form load routine I redim the array:
Private Sub Form_Load()
ReDim arySomeArray(1) As sometype 'insure that the array is initialized
End Sub
This will allow the array to be re-defined at any point later in the program.
When you find out how big the array needs to be just redim it.
ReDim arySomeArray(i) As sometype 'i is the size needed to hold the new data
The title of the question asks how to determine if an array is initialized, but, after reading the question, it looks like the real problem is how to get the UBound of an array that is not initialized.
Here is my solution (to the the actual problem, not to the title):
Function UBound2(Arr) As Integer
On Error Resume Next
UBound2 = UBound(Arr)
If Err.Number = 9 Then UBound2 = -1
On Error GoTo 0
End Function
This function works in the following four scenarios, the first three that I have found when Arr is created by an external dll COM and the fourth when the Arr is not ReDim-ed (the subject of this question):
UBound(Arr) works, so calling UBound2(Arr) adds a little overhead, but doesn't hurt much
UBound(Arr) fails in in the function that defines Arr, but succeeds inside UBound2()
UBound(Arr) fails both in the function that defines Arr and in UBound2(), so the error handling does the job
After Dim Arr() As Whatever, before ReDim Arr(X)
For any variable declared as an array, you can easily check if the array is initialized by calling the SafeArrayGetDim API. If the array is initialized, then the return value will be non-zero, otherwise the function returns zero.
Note that you can't use this function with variants that contain arrays. Doing so will cause a Compile error (Type mismatch).
Public Declare Function SafeArrayGetDim Lib "oleaut32.dll" (psa() As Any) As Long
Public Sub Main()
Dim MyArray() As String
Debug.Print SafeArrayGetDim(MyArray) ' zero
ReDim MyArray(64)
Debug.Print SafeArrayGetDim(MyArray) ' non-zero
Erase MyArray
Debug.Print SafeArrayGetDim(MyArray) ' zero
ReDim MyArray(31, 15, 63)
Debug.Print SafeArrayGetDim(MyArray) ' non-zero
Erase MyArray
Debug.Print SafeArrayGetDim(MyArray) ' zero
ReDim MyArray(127)
Debug.Print SafeArrayGetDim(MyArray) ' non-zero
Dim vArray As Variant
vArray = MyArray
' If you uncomment the next line, the program won't compile or run.
'Debug.Print SafeArrayGetDim(vArray) ' <- Type mismatch
End Sub
If the array is a string array, you can use the Join() method as a test:
Private Sub Test()
Dim ArrayToTest() As String
MsgBox StringArrayCheck(ArrayToTest) ' returns "false"
ReDim ArrayToTest(1 To 10)
MsgBox StringArrayCheck(ArrayToTest) ' returns "true"
ReDim ArrayToTest(0 To 0)
MsgBox StringArrayCheck(ArrayToTest) ' returns "false"
End Sub
Function StringArrayCheck(o As Variant) As Boolean
Dim x As String
x = Join(o)
StringArrayCheck = (Len(x) <> 0)
End Function
My only problem with API calls is moving from 32-bit to 64-bit OS's.
This works with Objects, Strings, etc...
Public Function ArrayIsInitialized(ByRef arr As Variant) As Boolean
On Error Resume Next
ArrayIsInitialized = False
If UBound(arr) >= 0 Then If Err.Number = 0 Then ArrayIsInitialized = True
End Function
If ChkArray(MyArray)=True then
....
End If
Public Function ChkArray(ByRef b) As Boolean
On Error goto 1
If UBound(b) > 0 Then ChkArray = True
End Function
You can solve the issue with Ubound() function, check if the array is empty by retrieving total elements count using JScript's VBArray() object (works with arrays of variant type, single or multidimensional):
Sub Test()
Dim a() As Variant
Dim b As Variant
Dim c As Long
' Uninitialized array of variant
' MsgBox UBound(a) ' gives 'Subscript out of range' error
MsgBox GetElementsCount(a) ' 0
' Variant containing an empty array
b = Array()
MsgBox GetElementsCount(b) ' 0
' Any other types, eg Long or not Variant type arrays
MsgBox GetElementsCount(c) ' -1
End Sub
Function GetElementsCount(aSample) As Long
Static oHtmlfile As Object ' instantiate once
If oHtmlfile Is Nothing Then
Set oHtmlfile = CreateObject("htmlfile")
oHtmlfile.parentWindow.execScript ("function arrlength(arr) {try {return (new VBArray(arr)).toArray().length} catch(e) {return -1}}"), "jscript"
End If
GetElementsCount = oHtmlfile.parentWindow.arrlength(aSample)
End Function
For me it takes about 0.4 mksec for each element + 100 msec initialization, being compiled with VB 6.0.9782, so the array of 10M elements takes about 4.1 sec. The same functionality could be implemented via ScriptControl ActiveX.
There are two slightly different scenarios to test:
The array is initialised (effectively it is not a null pointer)
The array is initialised and has at least one element
Case 2 is required for cases like Split(vbNullString, ",") which returns a String array with LBound=0 and UBound=-1.
Here are the simplest example code snippets I can produce for each test:
Public Function IsInitialised(arr() As String) As Boolean
On Error Resume Next
IsInitialised = UBound(arr) <> 0.5
End Function
Public Function IsInitialisedAndHasElements(arr() As String) As Boolean
On Error Resume Next
IsInitialisedAndHasElements = UBound(arr) >= LBound(arr)
End Function
Either of these two ways is valid to detect an uninitialized array, but they must include the parentheses:
(Not myArray) = -1
(Not Not myArray) = 0
' Function CountElements return counted elements of an array.
' Returns:
' [ -1]. If the argument is not an array.
' [ 0]. If the argument is a not initialized array.
' [Count of elements]. If the argument is an initialized array.
Private Function CountElements(ByRef vArray As Variant) As Integer
' Check whether the argument is an array.
If (VarType(vArray) And vbArray) <> vbArray Then
' Not an array. CountElements is set to -1.
Let CountElements = -1
Else
On Error Resume Next
' Calculate number of elements in array.
' Scenarios:
' - Array is initialized. CountElements is set to counted elements.
' - Array is NOT initialized. CountElements is never set and keeps its
' initial value of zero (since an error is
' raised).
Let CountElements = (UBound(vArray) - LBound(vArray)) + 1
End If
End Function
' Test of function CountElements.
Dim arrStr() As String
Dim arrV As Variant
Let iCount = CountElements(arrStr) ' arrStr is not initialized, returns 0.
ReDim arrStr(2)
Let iCount = CountElements(arrStr) ' arrStr is initialized, returns 3.
ReDim arrStr(5 To 8)
Let iCount = CountElements(arrStr) ' arrStr is initialized, returns 4.
Let arrV = arrStr
Let iCount = CountElements(arrV) ' arrV contains a boxed arrStr which is initialized, returns 4
Erase arrStr
Let iCount = CountElements(arrStr) ' arrStr size is erased, returns 0.
Let iCount = CountElements(Nothing) ' Nothing is not an array, returns -1.
Let iCount = CountElements(Null) ' Null is not an array, returns -1.
Let iCount = CountElements(5) ' Figure is not an array, returns -1.
Let iCount = CountElements("My imaginary array") ' Text is not an array, returns -1.
Let iCount = CountElements(Array(1, 2, 3, 4, 5)) ' Created array of Integer elements, returns 5.
Let iCount = CountElements(Array("A", "B", "C")) ' Created array of String elements, returns 3.
I see a lot of suggestions online about how to tell if an array has been initialized. Below is a function that will take any array, check what the ubound of that array is, redimension the array to ubound +1 (with or without PRESERVER) and then return what the current ubound of the array is, without errors.
Function ifuncRedimUbound(ByRef byrefArr, Optional bPreserve As Boolean)
On Error GoTo err:
1: Dim upp%: upp% = (UBound(byrefArr) + 1)
errContinue:
If bPreserve Then
ReDim Preserve byrefArr(upp%)
Else
ReDim byrefArr(upp%)
End If
ifuncRedimUbound = upp%
Exit Function
err:
If err.Number = 0 Then Resume Next
If err.Number = 9 Then ' subscript out of range (array has not been initialized yet)
If Erl = 1 Then
upp% = 0
GoTo errContinue:
End If
Else
ErrHandler.ReportError "modArray", ifuncRedimUbound, "1", err.Number, err.Description
End If
End Function
This worked for me, any bug in this?
If IsEmpty(a) Then
Exit Function
End If
MSDN
Dim someArray() as Integer
If someArray Is Nothing Then
Debug.print "this array is not initialised"
End If

Resources