Related
I'm trying to get the dimension of an array via PeekArray and SafeArrayGetDim API,
But the "Type mismatch" when compiling.
And if Debug.Print SafeArrayGetDim(PeekArray(TestArray).Ptr) will work fine.
Please find below the VB code.
Any help will be greatful.
Option Explicit
Private Type PeekArrayType
Ptr As Long
Reserved As Currency
End Type
Private Declare Function PeekArray Lib "kernel32" Alias "RtlMoveMemory" ( _
Arr() As Any, Optional ByVal Length As Long = 4) As PeekArrayType
Private Declare Function SafeArrayGetDim Lib "oleaut32.dll" (ByVal Ptr As Long) As Long
Sub GetArrayDimension()
Dim TestArray() As Long
ReDim TestArray(3, 2)
Debug.Print fnSafeArrayGetDim(TestArray)
End Sub
Function fnSafeArrayGetDim(varRunArray As Variant) As Long
Dim varTmpArray() As Variant
varTmpArray = varRunArray
fnSafeArrayGetDim = SafeArrayGetDim(PeekArray(varTmpArray).Ptr)
End Function
Here is a working fnSafeArrayGetDim function
Option Explicit
#Const HasPtrSafe = (VBA7 <> 0) Or (TWINBASIC <> 0)
#If Win64 Then
Private Const PTR_SIZE As Long = 8
#Else
Private Const PTR_SIZE As Long = 4
#End If
#If HasPtrSafe Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
#Else
Private Enum LongPtr
[_]
End Enum
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
#End If
Public Function fnSafeArrayGetDim(varRunArray As Variant) As Long
Const VT_BYREF As Long = &H4000
Dim lVarType As Long
Dim lPtr As LongPtr
Call CopyMemory(lVarType, varRunArray, 2)
If (lVarType And vbArray) <> 0 Then
Call CopyMemory(lPtr, ByVal VarPtr(varRunArray) + 8, PTR_SIZE)
If (lVarType And VT_BYREF) <> 0 Then
Call CopyMemory(lPtr, ByVal lPtr, PTR_SIZE)
End If
If lPtr <> 0 Then
Call CopyMemory(fnSafeArrayGetDim, ByVal lPtr, 2)
End If
End If
End Function
Private Sub Form_Load()
Dim TestArray() As Long
ReDim TestArray(3, 2)
Debug.Print fnSafeArrayGetDim(TestArray)
End Sub
You don't need PeekArray as you are dealing with pure Variants not arrays like Variant() (array of Variants), Long() (array of Longs) or Byte() (array of Bytes) generally a type ending with () in VB6 is so called SAFEARRAY in COM parlance.
So your varRunArray is a pure Variant that points to a SAFEARRAY in its pparray member which is located at VarPtr(varRunArray) + 8. Once you get this pointer you must heed the VT_BYREF flag in Variant's vt which introduces a double indirection (you have to dereference lPtr = *lPtr once more). At this point if you get a non-NULL pointer to the SAFEARRAY structure then the cDim member is in the first 2 bytes.
Here 's my solution, the ArrayDims function, adapted from wqw's post, above. In addition to wqw's basic logic, this solution will compile under VBA7/64-bit Office environments; it includes improved self-documentation and explanatory commentary; it eliminates the embedded constants and, instead, uses standard VB/VBA Type structures and Enum values where useful, and provides all associated Type elements and Enum values for reference. You can, of course, pare this down to the minimum necessary declarations and Enum values.
#If VBA7 Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
#Else
Private Declare Sub CopyMemory Lib "kernel32" _
Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
#End If
Enum VariantTypes
VTx_Empty = vbEmpty '(0) Uninitialized
VTx_Null = vbNull '(1) No valid data
VTx_Integer = vbInteger '(2)
VTx_Long = vbLong '(3)
VTx_FloatSingle = vbSingle '(4) Single-precision floating-point
VTx_FloatDouble = vbDouble '(5) Double-precision floating-point
VTx_Currency = vbCurrency '(6)
VTx_DATE = vbDate '(7)
VTx_String = vbString '(8)
VTx_Object = vbObject '(9)
VTx_Error = vbError '(10) An Error condition code
VTx_Boolean = vbBoolean '(11)
VTx_Variant = vbVariant '(12) Used only for arrays of Variants
VTx_Byte = vbByte '(17)
VTx_UDT = vbUserDefinedType '(36) User-defined data types
VTx_Array = vbArray '(8192)
VTx_ByRef = &H4000 '(16384) Is an indirect pointer to the Variant's data
End Enum
Type VariantStruct 'NOTE - the added "X_..." prefixes force the VBE Locals window to display the elements in
'their correct adjacency order:
A_VariantType As Integer '(2 bytes) See the VariantTypes Enum, above.
B_Reserved(1 To 6) As Byte '(6 bytes)
C_Data As LongLong '(8 bytes) NOTE: for an array-Variant, its Data is a pointer to the array.
End Type
Type ArrayStruct 'NOTE - the added "X_..." prefixes force the VBE Locals window to display the elements in
'their correct adjacency order:
A_DimCount As Integer '(aka cDim) 2 bytes: The number of dimensions in the array.
B_FeatureFlags As Integer '(aka fFeature) 2 bytes: See the FeatureFlags Enum, below.
C_ElementSize As Long '(aka cbElements) 4 bytes: The size of each element in the array.
D_LockCount As Long '(aka cLocks) 4 bytes: The count of active locks on the array.
E_DataPtr As Long '(aka pvData) 4 bytes: A pointer to the first data element in the array.
F_BoundsInfoArr As LongLong '(aka rgsabound) 8 bytes, min.: An info-array of SA_BoundInfo elements (see below)
' that contains bounds data for each dimension of the safe-array. There is one
' SA_BoundInfo element for each dimension in the array. F_BoundsInfoArr(0) holds
' the information for the right-most dimension and F_BoundsInfoArr[A_DimCount - 1]
' holds the information for the left-most dimension. Each SA_BoundInfo element is
' 8 bytes, structured as follows:
End Type
Private Type SA_BoundInfo
ElementCount As Long '(aka cElements) 4 bytes: The number of elements in the dimension.
LBoundVal As Long '(aka lLbound) 4 bytes: The lower bound of the dimension.
End Type
Enum FeatureFlags
FADF_AUTO = &H1 'Array is allocated on the stack.
FADF_STATIC = &H2 'Array is statically allocated.
FADF_EMBEDDED = &H4 'Array is embedded in a structure.
FADF_FIXEDSIZE = &H10 'Array may not be resized or reallocated.
FADF_BSTR = &H100 'An array of BSTRs.
FADF_UNKNOWN = &H200 'An array of IUnknown pointers.
FADF_DISPATCH = &H400 'An array of IDispatch pointers.
FADF_VARIANT = &H800 'An array of VARIANT type elements.
FADF_RESERVED = &HF0E8 'Bits reserved for future use.
End Enum
Function ArrayDims(SomeArray As Variant) As Long 'Cast the array argument to an array-Variant (if it isn't already)
'for a uniform reference-interface to it.
'
'Returns the number of dimensions of the specified array.
'
'AUTHOR: Peter Straton
'
'CREDIT: Adapted from wqw's post, above.
'
'*************************************************************************************************************
Dim DataPtrOffset As Integer
Dim DimCount As Integer '= ArrayStruct.A_DimCount (2 bytes)
Dim VariantType As Integer '= VariantStruct.A_VariantType (2 bytes)
Dim VariantDataPtr As LongLong '= VariantStruct.C_Data (8 bytes). See note about array-Variants' data, above.
'Check the Variant's type
Call CopyMemory(VariantType, SomeArray, LenB(VariantType))
If (VariantType And VTx_Array) Then
'It is an array-type Variant, so get its array data-pointer
Dim VariantX As VariantStruct 'Unfortunately, in VB/VBA, you can't reference the size of a user-defined
'data-Type element without instantiating one.
DataPtrOffset = LenB(VariantX) - LenB(VariantX.C_Data) 'Takes advantage of C_Data being the last element
Call CopyMemory(VariantDataPtr, ByVal VarPtr(SomeArray) + DataPtrOffset, LenB(VariantDataPtr))
If VariantDataPtr <> 0 Then
If (VariantType And VTx_ByRef) Then
'The passed array argument was not an array-Variant, so this function-call's cast to Variant type
'creates an indirect reference to the original array, via the Variant parameter. So de-reference
'that pointer.
Call CopyMemory(VariantDataPtr, ByVal VariantDataPtr, LenB(VariantDataPtr))
End If
If VariantDataPtr <> 0 Then
'Now have a legit Array reference, so get and return its dimension-count value
Call CopyMemory(DimCount, ByVal VariantDataPtr, LenB(DimCount))
End If
End If
End If
ArrayDims = DimCount
End Function 'ArrayDims
Sub Demo_ArrayDims()
'
'Demonstrates the functionality of the ArrayDims function using a 1-D, 2-D and 3-D array of various types
'
'*************************************************************************************************************
Dim Test2DArray As Variant
Dim Test3DArray() As Long
Debug.Print 'Blank line
Debug.Print ArrayDims(Array(20, 30, 400)) 'Test 1D array
Test2DArray = [{0, 0, 0, 0; "Apple", "Fig", "Orange", "Pear"}]
Debug.Print ArrayDims(Test2DArray)
ReDim Test3DArray(1 To 3, 0 To 1, 1 To 4)
Debug.Print ArrayDims(Test3DArray)
End Sub
Change it to
Function fnSafeArrayGetDim(ByRef varRunArray() As Long) As Long
Dim varTmpArray() As Long
varTmpArray = varRunArray
fnSafeArrayGetDim = SafeArrayGetDim(PeekArray(varTmpArray).Ptr)
End Function
You cannot put a Dim TestArray() As Long in a Dim varTmpArray() As Variant what you try here varTmpArray = varRunArray.
If you want to be more generic then use
Function fnSafeArrayGetDim(ByRef varRunArray As Variant) As Long
Dim varTmpArray As Variant
varTmpArray = varRunArray
fnSafeArrayGetDim = SafeArrayGetDim(PeekArray(varTmpArray).Ptr)
End Function
For example:
You cannot put a Long array into a Variant array
Sub ThisDoesNotWork()
Dim TestArray() As Long
ReDim TestArray(3, 2)
Dim varTmpArray() As Variant 'with parenthesis
varTmpArray = TestArray
End Sub
but you can put a Long array into a Variant (that is not an array)
Sub ThisWorks()
Dim TestArray() As Long
ReDim TestArray(3, 2)
Dim varTmpArray As Variant 'note this is without parenthesis!
varTmpArray = TestArray
End Sub
and you can put a Long array into another Long array
Sub ThisWorksToo()
Dim TestArray() As Long
ReDim TestArray(3, 2)
Dim varTmpArray() As Long 'with parenthesis it has to be the same type as TestArray
varTmpArray = TestArray
End Sub
Is it really not possible to declare a 0-length array in VBA? If I try this:
Dim lStringArr(-1) As String
I get a compile error saying range has no values. If I try to trick the compiler and redim at runtime like this:
ReDim lStringArr(-1)
I get a subscript out of range error.
I've varied the above around a bit but with no luck e.g.
Dim lStringArr(0 To -1) As String
Use Case
I want to convert a variant array to a string array. The variant array may be empty as it comes from the Keys property of a dictionary. The keys property gives back an array of variants. I want an array of strings to use in my code, as I have some functions for processing string arrays I'd like to use. Here's the conversion function I'm using. This throws a subscript out of range error due to lMaxIndex being = -1:
Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
Dim lStringArr() As String
Dim lMaxIndex As Long, lMinIndex As Long
lMaxIndex = UBound(pVariants)
lMinIndex = LBound(pVariants)
ReDim lStringArr(lMaxIndex)
Dim lVal As Variant
Dim lIndex As Long
For lIndex = lMinIndex To lMaxIndex
lStringArr(lIndex) = pVariants(lIndex)
Next
mVariantArrayToStringArray = lStringArr
End Function
Hack
Return a singleton array containing an empty string. Note- this isn't what we want. We want an empty array- such that looping over it is like doing nothing. But a singleton array containing an empty string will often work e.g. if we later want to join all the strings together in the string array.
Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
Dim lStringArr() As String
Dim lMaxIndex As Long, lMinIndex As Long
lMaxIndex = UBound(pVariants)
lMinIndex = LBound(pVariants)
If lMaxIndex < 0 Then
ReDim lStringArr(1)
lStringArr(1) = ""
Else
ReDim lStringArr(lMaxIndex)
End If
Dim lVal As Variant
Dim lIndex As Long
For lIndex = lMinIndex To lMaxIndex
lStringArr(lIndex) = pVariants(lIndex)
Next
mVariantArrayToStringArray = lStringArr
End Function
Update since answer
Here is the function I'm using for converting a variant array to a string array. Comintern's solution seems more advanced and general, and I may switch to that one day if I'm still stuck coding in VBA:
Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
Dim lStringArr() As String
Dim lMaxIndex As Long, lMinIndex As Long
lMaxIndex = UBound(pVariants)
lMinIndex = LBound(pVariants)
If lMaxIndex < 0 Then
mVariantArrayToStringArray = Split(vbNullString)
Else
ReDim lStringArr(lMaxIndex)
End If
Dim lVal As Variant
Dim lIndex As Long
For lIndex = lMinIndex To lMaxIndex
lStringArr(lIndex) = pVariants(lIndex)
Next
mVariantArrayToStringArray = lStringArr
End Function
Notes
I use Option Explicit. This can't change as it safeguards the rest of the code in the module.
As noted in the comments, you can do this "natively" by calling Split on a vbNullString, as documented here:
expression - Required. String expression containing substrings and delimiters. If expression is a zero-length string(""), Split returns an empty array, that is, an array with no elements and no data.
If you need a more general solution (i.e., other data types, you can call the SafeArrayRedim function in oleaut32.dll directly and request that it re-dimensions the passed array to 0 elements. You do have to jump through a couple of hoops to get the base address of the array (this is due to a quirk of the VarPtr function).
In the module declarations section:
'Headers
Private Type SafeBound
cElements As Long
lLbound As Long
End Type
Private Const VT_BY_REF = &H4000&
Private Const PVDATA_OFFSET = 8
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, _
ByVal length As Long)
Private Declare Sub SafeArrayRedim Lib "oleaut32" (ByVal psa As LongPtr, _
ByRef rgsabound As SafeBound)
The procedure - pass it an initialized array (any type) and it will remove all elements from it:
Private Sub EmptyArray(ByRef vbArray As Variant)
Dim vtype As Integer
CopyMemory vtype, vbArray, LenB(vtype)
Dim lp As LongPtr
CopyMemory lp, ByVal VarPtr(vbArray) + PVDATA_OFFSET, LenB(lp)
If Not (vtype And VT_BY_REF) Then
CopyMemory lp, ByVal lp, LenB(lp)
Dim bound As SafeBound
SafeArrayRedim lp, bound
End If
End Sub
Sample usage:
Private Sub Testing()
Dim test() As Long
ReDim test(0)
EmptyArray test
Debug.Print LBound(test) '0
Debug.Print UBound(test) '-1
End Sub
Per Comintern's comment.
Make a dedicated utility function that returns the result of the VBA.Strings.Split function, working off vbNullString, which is effectively a null string pointer, which makes the intent more explicit than using an empty string literal "", which would also work:
Public Function EmptyStringArray() As String()
EmptyStringArray = VBA.Strings.Split(vbNullString)
End Function
Now branch your function to check for the existence of keys, and return EmptyStringArray if there are none, otherwise proceed to resize your result array and convert each source element.
If we're going to use WinAPI anyway, we can also cleanly create the array from scratch using the WinAPI SafeArrayCreate function instead of redimensioning it.
Struct declarations:
Public Type SAFEARRAYBOUND
cElements As Long
lLbound As Long
End Type
Public Type tagVariant
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
pSomething As LongPtr
End Type
WinAPI declarations:
Public Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
Public Declare PtrSafe Sub VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any)
Public Declare PtrSafe Sub SafeArrayDestroy Lib "OleAut32.dll"(ByVal psa As LongPtr)
Use it:
Public Sub Test()
Dim bounds As SAFEARRAYBOUND 'Defaults to lower bound 0, 0 items
Dim NewArrayPointer As LongPtr 'Pointer to hold unmanaged string array
NewArrayPointer = SafeArrayCreate(vbString, 1, bounds)
Dim tagVar As tagVariant 'Unmanaged variant we can manually manipulate
tagVar.vt = vbArray + vbString 'Holds a string array
tagVar.pSomething = NewArrayPointer 'Make variant point to the new string array
Dim v As Variant 'Actual variant
VariantCopy v, ByVal tagVar 'Copy unmanaged variant to managed one
Dim s() As String 'Managed string array
s = v 'Copy the array from the variant
SafeArrayDestroy NewArrayPointer 'Destroy the unmanaged SafeArray, leaving the managed one
Debug.Print LBound(s); UBound(s) 'Prove the dimensions are 0 and -1
End Sub
SafeArrayCreateVector
One other option, mentioned in answers elsewhere,1 2 3 is with SafeArrayCreateVector. While SafeArrayCreate returns a pointer as shown by Erik A, this one returns an array directly. You'd need a declaration for each type, like this:
Private Declare PtrSafe Function VectorBoolean Lib "oleaut32" Alias "SafeArrayCreateVector" ( _
Optional ByVal vt As VbVarType = vbBoolean, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) _
As Boolean()
Private Declare PtrSafe Function VectorByte Lib "oleaut32" Alias "SafeArrayCreateVector" ( _
Optional ByVal vt As VbVarType = vbByte, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) _
As Byte()
The same works for Currency, Date, Double, Integer, Long, LongLong, Object, Single, String and Variant.
If you're willing to stuff those into a module, you can create a function that works just like Array() but with an initial argument that sets the type:
Function ArrayTyped(vt As VbVarType, ParamArray argList()) As Variant
Dim ub As Long: ub = UBound(argList) + 1
Dim ret As Variant 'a variant to hold the array to be returned
Select Case vt
Case vbBoolean: Dim bln() As Boolean: bln = VectorBoolean(, , ub): ret = bln
Case vbByte: Dim byt() As Byte: byt = VectorByte(, , ub): ret = byt
Case vbCurrency: Dim cur() As Currency: cur = VectorCurrency(, , ub): ret = cur
Case vbDate: Dim dat() As Date: dat = VectorDate(, , ub): ret = dat
Case vbDouble: Dim dbl() As Double: dbl = VectorDouble(, , ub): ret = dbl
Case vbInteger: Dim i() As Integer: i = VectorInteger(, , ub): ret = i
Case vbLong: Dim lng() As Long: lng = VectorLong(, , ub): ret = lng
Case vbLongLong: Dim ll() As LongLong: ll = VectorLongLong(, , ub): ret = ll
Case vbObject: Dim obj() As Object: obj = VectorObject(, , ub): ret = obj
Case vbSingle: Dim sng() As Single: sng = VectorSingle(, , ub): ret = sng
Case vbString: Dim str() As String: str = VectorString(, , ub): ret = str
End Select
Dim argIndex As Long
For argIndex = 0 To ub - 1
ret(argIndex) = argList(argIndex)
Next
ArrayTyped = ret
End Function
This gives empty or filled arrays, like Array(). For example:
Dim myLongs() as Long
myLongs = ArrayTyped(vbLong, 1,2,3) '<-- populated Long(0,2)
Dim Pinnochio() as String
Pinnochio = ArrayTyped(vbString) '<-- empty String(0,-1)
Same ArrayTyped() Function With SafeArrayRedim
I like this function, but all those API calls for each type seem bloated. It seems the same function can be done with SafeArrayRedim, and just one API call. Declared as such:
Private Declare PtrSafe Function PtrRedim Lib "oleaut32" Alias "SafeArrayRedim" (ByVal arr As LongPtr, ByRef dims As Any) As Long
The same ArrayTyped function could then look like this:
Function ArrayTyped(vt As VbVarType, ParamArray argList()) As Variant
Dim ub As Long: ub = UBound(argList) + 1
Dim ret As Variant 'a variant to hold the array to be returned
Select Case vt
Case vbBoolean: Dim bln() As Boolean: ReDim bln(0): PtrRedim Not Not bln, ub: ret = bln
Case vbByte: Dim byt() As Byte: ReDim byt(0): PtrRedim Not Not byt, ub: ret = byt
Case vbCurrency: Dim cur() As Currency: ReDim cur(0): PtrRedim Not Not cur, ub: ret = cur
Case vbDate: Dim dat() As Date: ReDim dat(0): PtrRedim Not Not dat, ub: ret = dat
Case vbDouble: Dim dbl() As Double: ReDim dbl(0): PtrRedim Not Not dbl, ub: ret = dbl
Case vbInteger: Dim i() As Integer: ReDim i(0): PtrRedim Not Not i, ub: ret = i
Case vbLong: Dim lng() As Long: ReDim lng(0): PtrRedim Not Not lng, ub: ret = lng
Case vbLongLong: Dim ll() As LongLong: ReDim ll(0): PtrRedim Not Not ll, ub: ret = ll
Case vbObject: Dim obj() As Object: ReDim obj(0): PtrRedim Not Not obj, ub: ret = obj
Case vbSingle: Dim sng() As Single: ReDim sng(0): PtrRedim Not Not sng, ub: ret = sng
Case vbString: Dim str() As String: ReDim str(0): PtrRedim Not Not str, ub: ret = str
Case vbVariant: Dim var() As Variant: ReDim var(0): PtrRedim Not Not var, ub: ret = var
End Select
Dim argIndex As Long
For argIndex = 0 To ub - 1
ret(argIndex) = argList(argIndex)
Next
ArrayTyped = ret
End Function
A couple of other resources:
Following logic here you can also do this with user defined types. Just add another API call like the others. More discussion here.
If anyone wants empties with multiple dimensions, there is another interesting approach using SafeArrayCreate here.
What you can do is declare a variable length array of whatever type you need by declaring it with no specified length in the Dim. Then call IsArray with the array variable. It will return True but we're not interested in that, this just initialises the array.
Dim lStringArr() As String
' Call IsArray to initialise the array
IsArray lStringArr
' Print the amount of elements in the array to the Immediate window
Debug.Print UBound(lStringArr) - LBound(lStringArr) + 1
' It should print 0 without errors
Goal: populate 1-D array from 2 columns (in 2 different files) without looping.
The code where I'm trying to read the first list to an array fails on the line
MergeAccountOpportArr = NamesRng.Value
Attempted code:
Option Explicit
Public AccountsWB As Workbook
Public AccountsSht As Worksheet
' --- Columns Variables ---
Public Const NamesCol As String = "F"
' --- Public Arrays ---
Public MergeAccountOpportArr() As String
'===================================================================
Sub MergeRangestoArray()
Dim OpportWBName As String, AccountsWBName As String, WebinarWBName As String
Dim NamesRng As Rang
Dim LastRow As Long, i As Long
ReDim MergeAccountOpportArr(100000) 'init size array to very large size >> will optimize later
' open Accounts file
AccountsWBName = GetFileName(ThisWorkbook.Path, "Accounts")
' set the Accounts file workbook object
Set AccountsWB = Workbooks.Open(Filename:=AccountsWBName, ReadOnly:=True)
' set the worksheet object
Set AccountsSht = AccountsWB.Worksheets(1)
With AccountsSht
LastRow = FindLastRow(AccountsSht) ' get last row
Set NamesRng = .Range(.Cells(1, NamesCol), .Cells(LastRow, NamesCol))
MergeAccountOpportArr = NamesRng.Value ' <---- Here comes the error
End With
' rest of my code
End Sub
In theory, you should be able to do this by hacking around with the SAFEARRAY structures in memory. The indexing of the data area for a SAFEARRAY is determined by the product of the indexes of the individual dimensions, so if you have a two dimensional array where one dimension only has a single element, the memory addresses should be the same for a one dimensional array (row * 1 = row).
As proof of concept...
YOU CAN TRY THIS AT HOME KIDS, BUT THIS IS NOT PRODUCTION GRADE CODE.
'In declarations section:
#If VBA7 Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, _
ByVal length As Long)
#Else
Private Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, _
ByVal length As Long)
#End If
Private Const VT_BY_REF = &H4000&
Private Type SafeBound
cElements As Long
lLbound As Long
End Type
Private Type SafeArray
cDim As Integer
fFeature As Integer
cbElements As Long
cLocks As Long
#If VBA7 Then
pvData As LongPtr
#Else
pvData As Long
#End If
rgsabound As SafeBound
rgsabound2 As SafeBound
End Type
Public Function RangeToOneDimensionalArray(Target As Range) As Variant()
If Target.Columns.Count > 1 Or Target.Rows.Count = 1 Then
Err.Raise 5 'Invalid procedure call or argument
End If
Dim values() As Variant
values = Target.Value
If HackDimensions(values) Then
RangeToOneDimensionalArray = values
End If
End Function
Private Function HackDimensions(SafeArray As Variant) As Boolean
Dim vtype As Integer
'First 2 bytes are the VARENUM.
CopyMemory vtype, SafeArray, 2
Dim lp As Long
'Get the data pointer.
CopyMemory lp, ByVal VarPtr(SafeArray) + 8, 4
'Make sure the VARENUM is a pointer.
If (vtype And VT_BY_REF) <> 0 Then
'Dereference it for the actual data address.
CopyMemory lp, ByVal lp, 4
Dim victim As SafeArray
CopyMemory ByVal VarPtr(victim), ByVal lp, LenB(victim)
'Set the dimensions to 1
victim.cDim = 1
'Set the bound on the first dimension.
victim.rgsabound.cElements = victim.rgsabound2.cElements
CopyMemory ByVal lp, ByVal VarPtr(victim), LenB(victim)
HackDimensions = True
End If
End Function
Note that this has to swap the 2 dimensions (and the declarations are limited to 2D arrays). It also leaves the second dimension rgsabound "hanging", so you'll likely leak the memory for that structure (8 bytes) every time you run this.
The safer way would be to copy the contents of the memory area onto a new one dimensional array and use that instead, OR wrap this whole mess in a Class module and clean up after yourself when you get done.
Oh yeah, it works ;-)
Public Sub Testing()
Dim sample() As Variant
sample = RangeToOneDimensionalArray(Sheet1.Range("A1:A30"))
Dim idx As Long
For idx = 1 To 30
Debug.Print sample(idx)
Next
End Sub
This converts the ranges into a strings delimited by a specified character. It then joins the two lists into an array with split()
Note:
Delimiter will have to be a character not in your dataset
Transpose is due to your data being in columns. If your data is in rows you'll have to check it, maybe with something like a column count.
.
Sub Test()
Dim oResultArray() As String
oResultArray = MergeRngToArray(Sheet1.Range("B3:B12"), Sheet2.Range("B2:B6"))
End Sub
Private Function MergeRngToArray(ByVal Range1 As Range, ByVal Range2 As Range, Optional Delimiter As String = ",") As String()
Dim sRange1 As String
Dim sRange2 As String
sRange1 = Join(Application.WorksheetFunction.Transpose(Range1.Value), Delimiter) & Delimiter
sRange2 = Join(Application.WorksheetFunction.Transpose(Range2.Value), Delimiter)
MergeRngToArray = Split(sRange1 & sRange2, Delimiter)
End Function
Start with the easier problem of copying cells into a 1D array
You can go from a 1D array to a range easily with the following trick:
Public Sub TESTING()
Dim keyarr() As Variant
keyarr = Array("1", "2", "3", "4", "5")
Range("D3").Resize(5, 1).Value = WorksheetFunction.Transpose(keyarr)
End Sub
But the opposite is much harder because the .Value property of a range always returns a 2D array.
Except when used with the transpose function:
Public Sub TESTING()
Dim i As Long, n As Long
Dim keyarr() As Variant
n = Range(Range("B3"), Range("B3").End(xlDown)).Rows.Count
keyarr = WorksheetFunction.Transpose(Range("B3").Resize(n, 1).Value)
' keyarr is a nĂ—1 1D array
' Proof:
For i = 1 To n
Debug.Print keyarr(i)
Next i
End Sub
The trick is a) use the .Transpose() function to make a column into a single row and b) to use an array of Variant and not String. Internally the array will store strings, but the type has to be Variant.
Now the last problem is to combine two arrays
The only solution I can think of is to combine the data into a different worksheet.
Public Sub TESTING()
Dim i As Long, n1 As Long, n2 As Long
Dim vals1() As Variant, vals2() As Variant
' Pull two sets of data from two columns. You could use different sheets if you wanted.
n1 = Range(Range("B3"), Range("B3").End(xlDown)).Rows.Count
vals1 = WorksheetFunction.Transpose(Range("B3").Resize(n1, 1).Value)
n2 = Range(Range("D3"), Range("D3").End(xlDown)).Rows.Count
vals2 = WorksheetFunction.Transpose(Range("D3").Resize(n2, 1).Value)
Sheet2.Range("A1").Resize(n1, 1).Value = WorksheetFunction.Transpose(vals1)
Sheet2.Range("A1").Offset(n1, 0).Resize(n2, 1).Value = WorksheetFunction.Transpose(vals2)
Dim keyarr() As Variant
keyarr = WorksheetFunction.Transpose(Sheet2.Range("A1").Resize(n1 + n2, 1).Value)
End Sub
Array approach
Sub JoinColumnArrays(a, b)
'Purpose: join 2 vertical 1-based 2-dim datafield arrays based on two range columns
'Note: returns 2-dim array with only 1 column
'Hint: overcomes ReDim Preserve restriction to change only the last dimension!
a = Application.Index(a, Evaluate("row(1:" & UBound(a) + UBound(b) & ")"), 0)
Dim i As Long, Start As Long: Start = UBound(a) - UBound(b)
For i = 1 To UBound(b)
a(Start + i, 1) = b(i, 1) ' fills empty a elements with b elements
Next i
End Sub
The above array approach returns a 1-based 2-dim array (of only 1 "column" as 2nd dimension) with changed UBound(a) value, i.e. the sum of the original "row" count of array a plus elements count of array b.
Note that using the Application.Index() function overcomes the restriction of ReDim Preserve which only would change an array's last dimension.
Example Call
'...
Dim a as Variant, b as Variant
dim ws1 as Worksheet, ws2 as Worksheet
' Set ws1 = ... ' << change worksheet definitions to your needs
' Set ws2 = ...
a = ws1.Range("A2:B4") ' assign column data from different sheets
b = ws2.Range("C2:C3")
JoinColumnArrays a, b ' << call procedure JoinColumnArrays
'Debug.Print "column ~>" & Join(Application.Transpose(Application.Index(a, 0, 1)), ", ")
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.
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