For some protocol testing I need to randomize the case of each character in a lot of strings.
The strings are commands, created by my application, which will be sent via a winsock control to a client.
As it involves a lot of strings I want each part to be as fast as possible.
Right now I have:
Private Function RandomCaps(strText As String) As String
Dim lngChar As Long
Dim strLower As String, strUpper As String
Dim strRandom As String
strRandom = ""
strLower = LCase$(strText)
strUpper = UCase$(strText)
For lngChar = 1 To Len(strText)
If Int(2 * Rnd) = 0 Then
strRandom = strRandom & Mid$(strLower, lngChar, 1)
Else
strRandom = strRandom & Mid$(strUpper, lngChar, 1)
End If
Next lngChar
RandomCaps = strRandom
End Function
This is pretty straightforward, but probably not the fastest way.
What could I do to improve its speed?
Instead of concatenating strings together, use Mid to change the string in-place:
Private Function RandomCaps(s As String) As String
Dim uc As String
Dim i As Long
RandomCaps = LCase$(s)
uc = UCase$(s)
For i = 1 To Len(s)
If Rnd < 0.5 Then
Mid(RandomCaps, i, 1) = Mid(uc, i, 1)
End If
Next i
End Function
You can try using MidB, but it hardly makes any difference – and since it works with individual bytes, you're in for some nasty surprises if you don't know how VB6 stores strings.
Use MidB() instead of Mid.
MidB is a bit faster.
The other solution could be to copy the stringpointer into an Array of integer.
for example:
Public Type TUDTPtr
pSA As Long
Reserved As Long ' z.B. für vbVarType oder IRecordInfo
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
cElements As Long
lLBound As Long
End Type
Public Type TCharPointer
pudt As TUDTPtr
Chars() As Integer
End Type
Public Enum SAFeature
FADF_AUTO = &H1
FADF_STATIC = &H2
FADF_EMBEDDED = &H4
FADF_FIXEDSIZE = &H10
FADF_RECORD = &H20
FADF_HAVEIID = &H40
FADF_HAVEVARTYPE = &H80
FADF_BSTR = &H100
FADF_UNKNOWN = &H200
FADF_DISPATCH = &H400
FADF_VARIANT = &H800
FADF_RESERVED = &HF008
End Enum
Public Declare Sub RtlMoveMemory Lib "kernel32" ( _
ByRef pDst As Any, _
ByRef pSrc As Any, _
ByVal bLength As Long)
Public Declare Sub RtlZeroMemory Lib "kernel32" ( _
ByRef pDst As Any, _
ByVal bLength As Long)
Public Declare Function ArrPtr Lib "msvbvm60" _
Alias "VarPtr" ( _
ByRef pArr() As Any) As Long
Public Sub New_UDTPtr(ByRef this As TUDTPtr, _
ByVal Feature As SAFeature, _
ByVal bytesPerElement As Long, _
Optional ByVal CountElements As Long = 1, _
Optional ByVal lLBound As Long = 0)
With this
.pSA = VarPtr(.cDims)
.cDims = 1
.cbElements = bytesPerElement
.fFeatures = CInt(Feature)
.cElements = CountElements
.lLBound = lLBound
End With
End Sub
Public Sub New_CharPointer(ByRef this As TCharPointer, ByRef StrVal As String)
With this
Call New_UDTPtr(.pudt, FADF_AUTO Or FADF_FIXEDSIZE, 2, Len(StrVal), 1)
With .pudt
.pvData = StrPtr(StrVal)
End With
Call RtlMoveMemory(ByVal ArrPtr(.Chars), ByVal VarPtr(.pudt), 4)
End With
End Sub
Public Sub DeleteCharPointer(ByRef this As TCharPointer)
With this
Call RtlZeroMemory(ByVal ArrPtr(.Chars), 4)
End With
End Sub
your function then could look like:
Private Sub RandomCapsX(strText As String) 'As String
Dim i As Long
Dim p As TCharPointer: Call MCharPointer.New_CharPointer(p, strText)
For i = 1 To p.pudt.cElements
Select Case p.Chars(i)
Case 65 To 90
'Uppercase
p.Chars(i) = p.Chars(i) + Int(2 * Rnd) * 32
Case 97 To 122
'lowercase
p.Chars(i) = p.Chars(i) - Int(2 * Rnd) * 32
End Select
Next
Call MCharPointer.DeleteCharPointer(p)
End Sub
To optimize code by RDHS, you don't really need to keep an uppercase version of the string. I think this is as optimized as you can get.
CODE 1:
Private Function RandomCaps(s As String) As String
Dim i As Long
RandomCaps = LCase$(s)
For i = 1 To Len(s)
If Rnd < 0.5 Then
Mid(RandomCaps, i, 1) = UCase(Mid(RandomCaps, i, 1))
End If
Next i
End Function
The code above is good, however, in the case of really really large strings, you might wanna try this (not tested for performance vs RDHS's code):
CODE 2:
Private Function RandomCaps(s As String) As String
Dim b() As Byte
b = StrConv(Text1.Text, vbFromUnicode)
Dim i As Long
For i = 0 To UBound(b) - 1
If Rnd < 0.5 Then
If UCase(Chr(b(i))) = Chr(b(i)) Then
'original char is uppercase, make it lowercase
b(i) = Asc(LCase(Chr(b(i))))
Else
'original char is lowercase, make it uppercase
b(i) = Asc(UCase(Chr(b(i))))
End If
End If
Next i
RandomCaps = StrConv(b, vbUnicode)
End Function
EDIT:
I did some performance testing and the difference between the two codes above is negligible: the 2nd code block is only about 1% faster then the first one.
EDIT 2:
Disregard my previous edit. Code 2 is approximately 50% less efficient as Code 1. however, as RDHS suggested, I adjusted code 2 to compare the values instead of going back and forth from CHR to ASC and it is more efficient starting with input strings that are approximately 40 characters long. The longer the input string, the better code 3 performance. With an input string that is 944640 characters long, Code 3 is 57% faster then Code 1.
Statistics:
First column is the length of the input string (in chars)
Second column is Code 3 efficiency compared to Code 2.
As you can see, with string length of 5 chars, Code 2 is 46% more efficient. Starting with string length around 40, Code 3 becomes more and more efficient.
5 -46.80%
50 6.22%
100 21.50%
500 38.54%
1000 41.11%
10000 44.87%
100000 43.25%
1260000 43.02%
CODE 3:
Private Function RandomCaps(s As String) As String
Dim b() As Byte
b = StrConv(Text1.Text, vbFromUnicode)
Dim i As Long
For i = 0 To UBound(b) - 1
If Rnd < 0.5 Then
If b(i) >= 64 And b(i) <= 90 Then
'A to Z
b(i) = b(i) + 32
ElseIf b(i) >= 97 And b(i) <= 122 Then
'a to z
b(i) = b(i) - 32
Else
'everything else
End If
End If
Next i
RandomCaps = StrConv(b, vbUnicode)
End Function
Related
I am trying to create my first function or procedure in VBA. Basic types which I use in the code:
Private Type T_DATA_COLUMN_INFO
count As Integer
positiveColumnsColors(2) As Long ' decimal values from Hex
negativeColumnsColors(1) As Long
excludeColumnsColors(1) As Long
zeroTop As Integer ' position of zero, the Top property of zero rectangle
dataWidth As Integer
negativeDataHeight As Integer
positiveDataFound As Boolean
negativeDataFound As Boolean
End Type
' All is on horizontal axis except negativeValueY
Private Type T_COLUMN_RANGES
Xmin As Integer ' (Left) actually
Xmid As Integer ' middle position
Xmax As Integer ' Left + Column width
Xgap As Integer ' Gap between column rectangles
Xpitch As Integer ' Gap between colRanges()(1).mid and colRanges()(2).mid
negativeValueY As Integer ' Top+Height
Q1Y As Integer
Q2Y As Integer ' position of median
Q3Y As Integer
initiated As ENUM_INITIATED
End Type
What I have currently is not a function but procedure:
Sub SetColumnRanges(Sh As Shape, i As Integer)
colRanges(0).Width = 0
End Sub
But best would be if it would return variable 'passed boolean'
My code starts like this (shorted version):
Sub LookForAxis()
Dim colRanges() As T_COLUMN_RANGES
Dim i As Integer
Dim Sh As Shape
Dim passed As Boolean
ReDim colRanges(1 To 1) As T_COLUMN_RANGES
colRanges(1).initiated = 0
...
With ActiveWindow.Selection
If (.Type = ppSelectionShapes) And (.ShapeRange.Type = msoGroup) Then
For Each Sh In .ShapeRange.GroupItems
If (Sh.Name Like "Rec*") Then
For i = 1 To dataInfo.count
If Not passed Then ' If the array ...
' code skipped
' HERE I TRY TO CALL THE PROCEDURE OR FUNCTION... best if passed is returned for function:
SetColumnRanges Sh, i
Now the code to be placed in the function:
If Sh.Fill.ForeColor.RGB = dataInfo.positiveColumnsColors(1) Then
colRanges(i).initiated = colRanges(i).initiated Or columns_initiated_positive
If colRanges(i).Q1Y = 0 Then
colRanges(i).Q3Y = 0
colRanges(i).Q2Y = Sh.Top
colRanges(i).Q1Y = (Sh.Top + Sh.Height) * -1
ElseIf colRanges(i).Q1Y < 0 Then
If colRanges(i).Q1Y * -1 < Sh.Top + Sh.Height Then
tempInt = colRanges(i).Q2Y * -1
colRanges(i).Q3Y = colRanges(i).Q2Y ' Make the old value positive
colRanges(i).Q2Y = tempInt ' Make the old value positive
colRanges(i).Q1Y = Sh.Top + Sh.Height
Else
' Sh.Name = "Q3"
colRanges(i).Q3Y = Sh.Top + Sh.Height
colRanges(i).Q1Y = colRanges(i).Q1Y * -1 ' Make the old value positive
End If
End If
ElseIf Sh.Fill.ForeColor.RGB = dataInfo.negativeColumnsColors(1) Then
' Sh.Name = "Negative"
colRanges(i).initiated = colRanges(i).initiated Or columns_initiated_negative
End If
So colRanges and SH should be used in the function.
Error I get is:
Byref argument type mismatch
What am I doing wrong and how to fix it correctly?
you question format is a bit messed up which makes it complicated to read so if you can update it I could be more precise but calling a function works like this:
Sub test()
MsgBox test2("hi")
Dim var As String
var = test2("hi")
MsgBox var
End Sub
Function test2(var As String) As Boolean
test2 = True
End Function
you must make sure the type of the vars you are passing to your function are of the same type as the ones declared in your function (e.g. passing "hi" to "string" is ok but this would not work if var would be of type long in the function.
at the end of your function you send the result back by using the function name => "Test2 = output of your function you want to send back".
I was researching different components of VBA and APIs and came across a website that proposes a personalized API that runs the game Pong within a spreadsheet that contains certain subs and functions. The instance used was with Windows/Excel version 97 and is stated to not be compatible with the 2000 version (and, I am assuming, the current version). I was wondering if any VBA-savvy individuals who thought this was worth doing could let me know whether this is a function capable of implementing under the current iteration of Excel, and if so, what the workaround would be?
When I use the following code below, I get a run-time error declaring that
vba332.dll is missing
the debugger highlights the 9th line of the Public Function AddrOf which states:Call GetCurrentVbaProject (hProject) is the error line and (hProject) when hovered on is 0, which I am assuming is also a problem as it is supposed to be getting a value other than 0 to move forward...
According to some reading that I have found, the newer version of the referenced .dll would be something like:
vbe7.dll
But when I have substituted that line within this string of codes, it still does not return any data for the Declare Function.
This seemed like a fun API to play around with, but I could not devise a way to upgrade it to the current Excel version. The code:
Option Explicit
Private Declare Function GetCurrentVbaProject _
Lib "vba332.dll" Alias "EbGetExecutingProj" _
(hProject As Long) As Long
Private Declare Function GetFuncID Lib "vba332.dll" Alias "TipGetFunctionId" _
(ByVal hProject As Long, ByVal strFunctionName As String, _
ByRef strFunctionId As String) As Long
Private Declare Function GetAddr _
Lib "vba332.dll" Alias "TipGetLpfnOfFunctionId" _
(ByVal hProject As Long, ByVal strFunctionId As String, _
ByRef lpfn As Long) As Long
Private Declare Function SetTimer Lib "user32" _
(ByVal hwnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" _
(ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private lngTimerId As Long
Dim Paddle As Shape
Dim Ball As Shape
Dim nVertical As Integer
Dim nHorizontal As Integer
Dim nSpeed As Integer
Sub Auto_Open()
Application.OnKey "{F12}", "StartPong"
End Sub
Sub Auto_Close()
Timer_Terminate
On Error Resume Next
Paddle.Delete
Ball.Delete
End Sub
Sub StartPong()
Dim nLeft As Integer
Dim nTop As Integer
Dim nWidth As Integer
Dim nHeight As Integer
'Draw the paddle
nLeft = ActiveWindow.UsableWidth - 100
nTop = ActiveWindow.UsableHeight - 30
nWidth = 50
nHeight = 10
Set Paddle = ActiveSheet.Shapes.AddShape(1, nLeft, nTop, nWidth, nHeight)
Paddle.Fill.ForeColor.SchemeColor = 8
'Draw the ball
nLeft = CInt(ActiveWindow.UsableWidth / 2) - 20
nTop = 0
nWidth = 15
nHeight = 15
Set Ball = ActiveSheet.Shapes.AddShape(9, nLeft, nTop, nWidth, nHeight)
Ball.Fill.ForeColor.SchemeColor = 8
'Define keys
Application.OnKey "{ESC}", "EndPong"
Application.OnKey "{RIGHT}", "MoveRight"
Application.OnKey "{LEFT}", "MoveLeft"
Application.OnKey "{F12}"
'Set speed
nVertical = 10 'Ball Vertical
nHorizontal = 10 'Ball Horizontal
nSpeed = 18 'Paddle Horizontal
'Start the ball movement timer
Timer_Initialize (15) 'Ball will be moved every 15 milliseconds
'Now we wait for events to move things
End Sub
Sub MoveBall()
Dim nLeft As Integer
Dim nTop As Integer
With Ball
'Move Horizontal
.Left = .Left + nHorizontal
'Move vertical
.Top = .Top + nVertical
'Bounce horizontal
nLeft = .Left
If nLeft > (ActiveWindow.UsableWidth - 50) Then
nHorizontal = -1 * Abs((nHorizontal))
End If
If nLeft < 20 Then
nHorizontal = Abs(nHorizontal)
End If
'Bounce vertical
nTop = .Top
If nTop > (ActiveWindow.UsableHeight - 50) Then
nVertical = -1 * (Abs(nVertical))
'Did Paddle hit it?
If (.Left + (.Width / 2)) > Paddle.Left And _
(.Left + (.Width / 2)) < (Paddle.Left + Paddle.Width) Then
'Paddle hit the ball
If (.Left + (.Width / 2)) < (Paddle.Left + (Paddle.Width / 3)) Then
'Ball hit paddle on left third; apply english
nHorizontal = nHorizontal - 5
If nHorizontal < -15 Then nHorizontal = -15
End If
If (.Left + (.Width / 2)) > (Paddle.Left + (2 * Paddle.Width / 3)) Then
'Ball hit paddle on right third
nHorizontal = nHorizontal + 5
If nHorizontal > 15 Then nHorizontal = 15
End If
Else
Beep 'missed
'Move the paddle in case window was resized
Paddle.Top = ActiveWindow.UsableHeight - 30
End If
End If
If nTop < 20 Then
nVertical = Abs(nVertical)
End If
End With
End Sub
Sub EndPong()
Timer_Terminate
Application.OnKey "{ESC}"
Application.OnKey "{RIGHT}"
Application.OnKey "{LEFT}"
Application.OnKey "{F12}", "StartPong"
Paddle.Delete
Ball.Delete
End Sub
Sub MoveRight()
Paddle.Left = Paddle.Left + nSpeed
If Paddle.Left > (Application.UsableWidth - 30 - Paddle.Width) Then
Paddle.Left = Application.UsableWidth - 30 - Paddle.Width
End If
End Sub
Sub MoveLeft()
Paddle.Left = Paddle.Left - nSpeed
If Paddle.Left < 0 Then
Paddle.Left = 0
End If
End Sub
Public Function AddrOf(strFuncName As String) As Long
'Returns a function pointer of a VBA public function given its name.
'AddrOf code from Microsoft Office Developer magazine
'http://www.informant.com/mod/index.htm
Dim hProject As Long
Dim lngResult As Long
Dim strID As String
Dim lpfn As Long
Dim strFuncNameUnicode As String
Const NO_ERROR = 0
' The function name must be in Unicode, so convert it.
strFuncNameUnicode = StrConv(strFuncName, vbUnicode)
' Get the current VBA project
Call GetCurrentVbaProject(hProject)
' Make sure we got a project handle
If hProject <> 0 Then
' Get the VBA function ID
lngResult = GetFuncID(hProject, strFuncNameUnicode, strID)
If lngResult = NO_ERROR Then
' Get the function pointer.
lngResult = GetAddr(hProject, strID, lpfn)
If lngResult = NO_ERROR Then
AddrOf = lpfn
End If
End If
End If
End Function
Private Sub TimerProc(ByVal hwnd&, ByVal lngMsg&, ByVal lngTimerId&, ByVal
lngTime&)
Call MoveBall
End Sub
Sub Timer_Initialize(Optional vInterval As Variant)
Dim lngInterval As Long
lngInterval = CLng(vInterval)
If lngInterval = 0 Then lngInterval = 60 '60 milliseconds just a bit longer
than a "tick"
lngTimerId = SetTimer(0, 0, lngInterval, AddrOf("TimerProc"))
If lngTimerId = 0 Then
MsgBox "Unable to initialize a new timer!"
End If
End Sub
Sub Timer_Terminate()
If lngTimerId <> 0 Then
Call KillTimer(0, lngTimerId)
End If
End Sub
Thanks!
In later versions of Office, "TipGetLpfnOfFunctionId" is exposed as AddressOf.
And since you can use AddressOf directly to get the address of a function, you don't need "TipGetFunctionId" nor all the "addrof" code.
Sub Timer_Initialize(Optional vInterval As Variant)
Dim lngInterval As Long
lngInterval = CLng(vInterval)
If lngInterval = 0 Then lngInterval = 60
lngTimerId = SetTimer(0, 0, lngInterval, AddressOf TimerProc)
If lngTimerId = 0 Then
MsgBox "Unable to initialize a new timer!"
End If
End Sub
Note the unique syntax of the "AddressOf" operator: it's not a function.
This question already has answers here:
How to find the number of dimensions that an array has?
(3 answers)
Closed 2 years ago.
Does anyone know how to return the number of dimensions of a (Variant) variable passed to it in VBA?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
That's the only way I could come up with. Not pretty….
Looking at MSDN, they basically did the same.
To return the number of dimensions without swallowing errors:
#If VBA7 Then
Private Type Pointer: Value As LongPtr: End Type
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (ByRef dest As Any, ByRef src As Any, ByVal Size As LongPtr)
#Else
Private Type Pointer: Value As Long: End Type
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef dest As Any, ByRef src As Any, ByVal Size As Long)
#End If
Private Type TtagVARIANT
vt As Integer
r1 As Integer
r2 As Integer
r3 As Integer
sa As Pointer
End Type
Public Function GetDims(source As Variant) As Integer
Dim va As TtagVARIANT
RtlMoveMemory va, source, LenB(va) ' read tagVARIANT '
If va.vt And &H2000 Then Else Exit Function ' exit if not an array '
If va.vt And &H4000 Then RtlMoveMemory va.sa, ByVal va.sa.Value, LenB(va.sa) ' read by reference '
If va.sa.Value Then RtlMoveMemory GetDims, ByVal va.sa.Value, 2 ' read cDims from tagSAFEARRAY '
End Function
Usage:
Sub Examples()
Dim list1
Debug.Print GetDims(list1) ' >> 0 '
list1 = Array(1, 2, 3, 4)
Debug.Print GetDims(list1) ' >> 1 '
Dim list2()
Debug.Print GetDims(list2) ' >> 0 '
ReDim list2(2)
Debug.Print GetDims(list2) ' >> 1 '
ReDim list2(2, 2)
Debug.Print GetDims(list2) ' >> 2 '
Dim list3(0 To 0, 0 To 0, 0 To 0)
Debug.Print GetDims(list3) ' >> 3 '
End Sub
#cularis and #Issun have perfectly adequate answers for the exact question asked. I'm going to question your question, though. Do you really have a bunch of arrays of unknown dimension count floating around? If you're working in Excel, the only situation where this should occur is a UDF where you might get passed either a 1-D array or a 2-D array (or a non-array), but nothing else.
You should almost never have a routine that expects something arbitrary though. And thus you probably shouldn't have a general "find # of array dimensions" routine either.
So, with that in mind, here is the routines I use:
Global Const ERR_VBA_NONE& = 0
Global Const ERR_VBA_SUBSCRIPT_OUT_OF_RANGE& = 9
'Tests an array to see if it extends to a given dimension
Public Function arrHasDim(arr, dimNum As Long) As Boolean
Debug.Assert IsArray(arr)
Debug.Assert dimNum > 0
'Note that it is possible for a VBA array to have no dimensions (i.e.
''LBound' raises an error even on the first dimension). This happens
'with "unallocated" (borrowing Chip Pearson's terminology; see
'http://www.cpearson.com/excel/VBAArrays.htm) dynamic arrays -
'essentially arrays that have been declared with 'Dim arr()' but never
'sized with 'ReDim', or arrays that have been deallocated with 'Erase'.
On Error Resume Next
Dim lb As Long
lb = LBound(arr, dimNum)
'No error (0) - array has given dimension
'Subscript out of range (9) - array doesn't have given dimension
arrHasDim = (Err.Number = ERR_VBA_NONE)
Debug.Assert (Err.Number = ERR_VBA_NONE Or Err.Number = ERR_VBA_SUBSCRIPT_OUT_OF_RANGE)
On Error GoTo 0
End Function
'"vect" = array of one and only one dimension
Public Function isVect(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 1) Then
isVect = Not arrHasDim(arg, 2)
End If
End Function
'"mat" = array of two and only two dimensions
Public Function isMat(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 2) Then
isMat = Not arrHasDim(arg, 3)
End If
End Function
Note the link to Chip Pearson's excellent web site: http://www.cpearson.com/excel/VBAArrays.htm
Also see: How do I determine if an array is initialized in VB6?. I personally don't like the undocumented behavior it relies on, and performance is rarely that important in the Excel VBA code I'm writing, but it's interesting nonetheless.
For arrays, MS has a nice method that involves looping through until an error occurs.
"This routine tests the array named Xarray by testing the LBound of each dimension. Using a For...Next loop, the routine cycles through the number of possible array dimensions, up to 60000, until an error is generated. Then the error handler takes the counter step that the loop failed on, subtracts one (because the previous one was the last one without an error), and displays the result in a message box...."
http://support.microsoft.com/kb/152288
Cleaned-up version of code (decided to write as a function, not sub):
Function NumberOfDimensions(ByVal vArray As Variant) As Long
Dim dimnum As Long
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
NumberOfDimensions = dimnum - 1
End Function
Microsoft has documented the structure of VARIANT and SAFEARRAY, and using those you can parse the binary data to get the dimensions.
Create a normal code module. I call mine "mdlDims". You would use it by calling the simple function 'GetDims' and passing it an array.
Option Compare Database
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx
Private Type SAFEARRAY
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
'Variants are all 16 bytes, but they are split up differently based on the contained type
'VBA doesn't have the ability to Union, so a Type is limited to representing one layout
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
Private Type ARRAY_VARIANT
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
lpSAFEARRAY As Long
data(4) As Byte
End Type
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170(v=vs.85).aspx
Private Enum VARENUM
VT_EMPTY = &H0
VT_NULL
VT_I2
VT_I4
VT_R4
VT_R8
VT_CY
VT_DATE
VT_BSTR
VT_DISPATCH
VT_ERROR
VT_BOOL
VT_VARIANT
VT_UNKNOWN
VT_DECIMAL
VT_I1 = &H10
VT_UI1
VT_UI2
VT_I8
VT_UI8
VT_INT
VT_VOID
VT_HRESULT
VT_PTR
VT_SAFEARRAY
VT_CARRAY
VT_USERDEFINED
VT_LPSTR
VT_LPWSTR
VT_RECORD = &H24
VT_INT_PTR
VT_UINT_PTR
VT_ARRAY = &H2000
VT_BYREF = &H4000
End Enum
Public Function GetDims(VarSafeArray As Variant) As Integer
Dim varArray As ARRAY_VARIANT
Dim lpSAFEARRAY As Long
Dim sArr As SAFEARRAY
'Inspect the Variant
CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16&
'If the Variant is pointing to an array...
If varArray.vt And (VARENUM.VT_ARRAY Or VARENUM.VT_BYREF) Then
'Get the pointer to the SAFEARRAY from the Variant
CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4&
'If the pointer is not Null
If Not lpSAFEARRAY = 0 Then
'Read the array dimensions from the SAFEARRAY
CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr)
'and return them
GetDims = sArr.cDims
Else
'The array is uninitialized
GetDims = 0
End If
Else
'Not an array, you could choose to raise an error here
GetDims = 0
End If
End Function
I presume you mean without using On Error Resume Next which most programmers dislike and which also means that during debugging you can't use 'Break On All Errors' to get the code to stop dead (Tools->Options->General->Error Trapping->Break on All Errors).
For me one solution is to bury any On Error Resume Next into a compiled DLL, in the old days this would have been VB6. Today you could use VB.NET but I choose to use C#.
If Visual Studio is available to you then here is some source. It will return a dictionary, the Dicitionary.Count will return the number of dimensions. The items will also contain the LBound and UBound as a concatenated string. I'm always querying an array not just for its dimensions but also for LBound and UBound of those dimensions so I put these together and return a whole bundle of info in a Scripting Dictionary
Here is C# source, start a Class Library calling it BuryVBAErrorsCS, set ComVisible(true) add a reference to COM library 'Microsoft Scripting Runtime', Register for Interop.
using Microsoft.VisualBasic;
using System;
using System.Runtime.InteropServices;
namespace BuryVBAErrorsCS
{
// Requires adding a reference to COM library Microsoft Scripting Runtime
// In AssemblyInfo.cs set ComVisible(true);
// In Build tab check 'Register for Interop'
public interface IDimensionsAndBounds
{
Scripting.Dictionary DimsAndBounds(Object v);
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDimensionsAndBounds))]
public class CDimensionsAndBounds : IDimensionsAndBounds
{
public Scripting.Dictionary DimsAndBounds(Object v)
{
Scripting.Dictionary dicDimsAndBounds;
dicDimsAndBounds = new Scripting.Dictionary();
try
{
for (Int32 lDimensionLoop = 1; lDimensionLoop < 30; lDimensionLoop++)
{
long vLBound = Information.LBound((Array)v, lDimensionLoop);
long vUBound = Information.UBound((Array)v, lDimensionLoop);
string concat = (string)vLBound.ToString() + " " + (string)vUBound.ToString();
dicDimsAndBounds.Add(lDimensionLoop, concat);
}
}
catch (Exception)
{
}
return dicDimsAndBounds;
}
}
}
For Excel client VBA code here is some source
Sub TestCDimensionsAndBounds()
'* requires Tools->References->BuryVBAErrorsCS.tlb
Dim rng As Excel.Range
Set rng = ThisWorkbook.Worksheets.Item(1).Range("B4:c7")
Dim v As Variant
v = rng.Value2
Dim o As BuryVBAErrorsCS.CDimensionsAndBounds
Set o = New BuryVBAErrorsCS.CDimensionsAndBounds
Dim dic As Scripting.Dictionary
Set dic = o.DimsAndBounds(v)
Debug.Assert dic.Items()(0) = "1 4"
Debug.Assert dic.Items()(1) = "1 2"
Dim s(1 To 2, 2 To 3, 3 To 4, 4 To 5, 5 To 6)
Set dic = o.DimsAndBounds(s)
Debug.Assert dic.Items()(0) = "1 2"
Debug.Assert dic.Items()(1) = "2 3"
Debug.Assert dic.Items()(2) = "3 4"
Debug.Assert dic.Items()(3) = "4 5"
Debug.Assert dic.Items()(4) = "5 6"
Stop
End Sub
NOTE WELL: This answer handles grid variants pulled off a worksheet with Range.Value as well as arrays created in code using Dim s(1) etc.! Some of the other answers do not do this.
I like to use the fact that with an error, the new variable-value is not charged.
To get the dimension (A_Dim) of an Array (vArray) you can use following code:
On Error Resume Next
A_Dim = -1
Do Until A = "X"
A_Dim = A_Dim + 1
A = "X"
A = UBound(vArray, A_Dim + 1)
Loop
On Error GoTo 0
Function ArrayDimension(ByRef ArrayX As Variant) As Byte
Dim i As Integer, a As String, arDim As Byte
On Error Resume Next
i = 0
Do
a = CStr(ArrayX(0, i))
If Err.Number > 0 Then
arDim = i
On Error GoTo 0
Exit Do
Else
i = i + 1
End If
Loop
If arDim = 0 Then arDim = 1
ArrayDimension = arDim
End Function
I found a pretty simple way to check, probably laden with a bunch of coding faux pas, incorrect lingo, and ill advised techniques but never the less:
Dim i as Long
Dim VarCount as Long
Dim Var as Variant
'generate your variant here
i = 0
VarCount = 0
recheck1:
If IsEmpty(Var(i)) = True Then GoTo VarCalc
i = i + 1
GoTo recheck1
VarCalc:
VarCount= i - 1
Note: VarCount will obviously return a negative number if Var(0) doesn't exist. VarCount is the max reference number for use with Var(i), i is the number of variants you have.
What about just using ubound(var) + 1? That should give you the last element of most of variables (unless it's a custom range, but in that case you should know that info already). The range of a conventional variable (for instance, when using the split function) starts with 0; ubound gives you the last item of the variable. So if you have a variable with 8 elements, for instance, it will go from 0 (lbound) to 7 (ubound), and you can know the quantity of elements just adding ubound(var) + 1. For example:
Public Sub PrintQntElements()
Dim str As String
Dim var As Variant
Dim i As Integer
str = "Element1!Element2!Element3!Element4!Element5!Element6!Element7!Element8"
var = Split(str, "!")
i = UBound(var) + 1
Debug.Print "First element: " & LBound(var)
Debug.Print "Last element: " & UBound(var)
Debug.Print "Quantity of elements: " & i
End Sub
It will print this output to the Inmediate window:
First element: 0
Last element: 7
Quantity of elements: 8
Also, if you are not sure that the first element (lbound) is 0, you can just use:
i = UBound(var) - LBound(var) + 1
Looking for a simple text encryption/decryption VB6 code. Ideally, the solution should accept (text, password) arguments and produce readable output (without any special characters), so it can be used anywhere without encoding issues.
There are lots of code available for .NET, but not really much I can find for legacy VB6. Only this I've found so far: http://www.devx.com/vb2themax/Tip/19211
I'm using RC4 implementation like this
Option Explicit
Private Sub Command1_Click()
Dim sSecret As String
sSecret = ToHexDump(CryptRC4("a message here", "password"))
Debug.Print sSecret
Debug.Print CryptRC4(FromHexDump(sSecret), "password")
End Sub
Public Function CryptRC4(sText As String, sKey As String) As String
Dim baS(0 To 255) As Byte
Dim baK(0 To 255) As Byte
Dim bytSwap As Byte
Dim lI As Long
Dim lJ As Long
Dim lIdx As Long
For lIdx = 0 To 255
baS(lIdx) = lIdx
baK(lIdx) = Asc(Mid$(sKey, 1 + (lIdx Mod Len(sKey)), 1))
Next
For lI = 0 To 255
lJ = (lJ + baS(lI) + baK(lI)) Mod 256
bytSwap = baS(lI)
baS(lI) = baS(lJ)
baS(lJ) = bytSwap
Next
lI = 0
lJ = 0
For lIdx = 1 To Len(sText)
lI = (lI + 1) Mod 256
lJ = (lJ + baS(lI)) Mod 256
bytSwap = baS(lI)
baS(lI) = baS(lJ)
baS(lJ) = bytSwap
CryptRC4 = CryptRC4 & Chr$((pvCryptXor(baS((CLng(baS(lI)) + baS(lJ)) Mod 256), Asc(Mid$(sText, lIdx, 1)))))
Next
End Function
Private Function pvCryptXor(ByVal lI As Long, ByVal lJ As Long) As Long
If lI = lJ Then
pvCryptXor = lJ
Else
pvCryptXor = lI Xor lJ
End If
End Function
Public Function ToHexDump(sText As String) As String
Dim lIdx As Long
For lIdx = 1 To Len(sText)
ToHexDump = ToHexDump & Right$("0" & Hex(Asc(Mid(sText, lIdx, 1))), 2)
Next
End Function
Public Function FromHexDump(sText As String) As String
Dim lIdx As Long
For lIdx = 1 To Len(sText) Step 2
FromHexDump = FromHexDump & Chr$(CLng("&H" & Mid(sText, lIdx, 2)))
Next
End Function
Command1 outputs this:
9ED5556B3F4DD5C90471C319402E
a message here
You might need better error handling on FromHexDump though.
Update (2018-05-04)
For much stronger AES 256-bit encryption (in ECB mode) and proper handling of unicode texts/passwords you can check out Simple AES 256-bit password protected encryption as implemented in mdAesEcb.bas module (~380 LOC).
MD5sum the the text and password together as a one way hash (and then to check, you encrypt again and compare with the stored hash. (This won't work if you MUST decrypt it again though)
Here's my encryption class. I use several constants to define the encryption key because in my mind it's a little more secure from someone trying to decompile the code to find it. Cryptography isn't my thing so maybe I'm kidding myself. Anyway, I used this class in an ActiveX dll called from other programs to do encryption and the reverse in a separate dll for decryption. I did it this way so people who shouldn't be seeing encrypted data don't even have the dll to do the decrypting. Change the key constants to what you want (5 long). I use a mix including unprintable characters and it has worked well for me so far. The CAPICOM is part of Windows® so you don't have to distribute.
Option Explicit
Private m_oENData As CAPICOM.EncryptedData
'combine these constants to build the encryption key
Private Const KEY1 = "12345"
Private Const KEY2 = "67890"
Private Const KEY3 = "abcde"
Private Const KEY4 = "fghij"
Private Const KEY5 = "klmno"
Private Sub Class_Initialize()
On Error Resume Next
Set m_oENData = New CAPICOM.EncryptedData
If Err.Number <> 0 Then
If Err.Number = 429 Then
Err.Raise Err.Number, App.EXEName, "Failed to create the capi com object. " & _
"Check that the capicom.dll file is installed and properly registered."
Else
Err.Raise Err.Number, Err.Source, Err.Description
End If
End If
End Sub
Private Sub Class_Terminate()
Set m_oENData = Nothing
End Sub
Public Function EncryptAsBase64(ByVal RawString As String) As String
EncryptAsBase64 = Encrypt(RawString, CAPICOM_ENCODE_BASE64)
End Function
Public Function EncryptAsBinary(ByVal RawString As String) As String
EncryptAsBinary = Encrypt(RawString, CAPICOM_ENCODE_BINARY)
End Function
Private Function Encrypt(ByVal s As String, ByVal EncryptionType As CAPICOM.CAPICOM_ENCODING_TYPE) As String
Dim oEN As New CAPICOM.EncryptedData
Dim intENCType As CAPICOM.CAPICOM_ENCRYPTION_ALGORITHM
Dim strSecret As String
Dim intTries As Integer
On Error GoTo errEncrypt
intENCType = CAPICOM_ENCRYPTION_ALGORITHM_AES ' try this first and fall back if not supported
With oEN
startEncryption:
.Algorithm = intENCType
strSecret = KEY2 & KEY5 & KEY4 & KEY1 & KEY3
.SetSecret strSecret
strSecret = ""
.Content = s
' the first encryption type needs to be base64 as the .content property
' can loose information if I try to manipulate a binary string
.Content = StrReverse(.Encrypt(CAPICOM_ENCODE_BASE64))
strSecret = KEY1 & KEY4 & KEY3 & KEY2 & KEY5
.SetSecret strSecret
strSecret = ""
Encrypt = .Encrypt(EncryptionType)
End With
Set oEN = Nothing
Exit Function
errEncrypt:
If Err.Number = -2138568448 Then
' if this is the first time the step the encryption back and try again
If intTries < 1 Then
intTries = intTries + 1
intENCType = CAPICOM_ENCRYPTION_ALGORITHM_3DES
Resume startEncryption
End If
End If
Err.Raise Err.Number, Err.Source & ":Encrypt", Err.Description
strSecret = ""
Set oEN = Nothing
End Function
This question already has answers here:
How to find the number of dimensions that an array has?
(3 answers)
Closed 2 years ago.
Does anyone know how to return the number of dimensions of a (Variant) variable passed to it in VBA?
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
That's the only way I could come up with. Not pretty….
Looking at MSDN, they basically did the same.
To return the number of dimensions without swallowing errors:
#If VBA7 Then
Private Type Pointer: Value As LongPtr: End Type
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (ByRef dest As Any, ByRef src As Any, ByVal Size As LongPtr)
#Else
Private Type Pointer: Value As Long: End Type
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByRef dest As Any, ByRef src As Any, ByVal Size As Long)
#End If
Private Type TtagVARIANT
vt As Integer
r1 As Integer
r2 As Integer
r3 As Integer
sa As Pointer
End Type
Public Function GetDims(source As Variant) As Integer
Dim va As TtagVARIANT
RtlMoveMemory va, source, LenB(va) ' read tagVARIANT '
If va.vt And &H2000 Then Else Exit Function ' exit if not an array '
If va.vt And &H4000 Then RtlMoveMemory va.sa, ByVal va.sa.Value, LenB(va.sa) ' read by reference '
If va.sa.Value Then RtlMoveMemory GetDims, ByVal va.sa.Value, 2 ' read cDims from tagSAFEARRAY '
End Function
Usage:
Sub Examples()
Dim list1
Debug.Print GetDims(list1) ' >> 0 '
list1 = Array(1, 2, 3, 4)
Debug.Print GetDims(list1) ' >> 1 '
Dim list2()
Debug.Print GetDims(list2) ' >> 0 '
ReDim list2(2)
Debug.Print GetDims(list2) ' >> 1 '
ReDim list2(2, 2)
Debug.Print GetDims(list2) ' >> 2 '
Dim list3(0 To 0, 0 To 0, 0 To 0)
Debug.Print GetDims(list3) ' >> 3 '
End Sub
#cularis and #Issun have perfectly adequate answers for the exact question asked. I'm going to question your question, though. Do you really have a bunch of arrays of unknown dimension count floating around? If you're working in Excel, the only situation where this should occur is a UDF where you might get passed either a 1-D array or a 2-D array (or a non-array), but nothing else.
You should almost never have a routine that expects something arbitrary though. And thus you probably shouldn't have a general "find # of array dimensions" routine either.
So, with that in mind, here is the routines I use:
Global Const ERR_VBA_NONE& = 0
Global Const ERR_VBA_SUBSCRIPT_OUT_OF_RANGE& = 9
'Tests an array to see if it extends to a given dimension
Public Function arrHasDim(arr, dimNum As Long) As Boolean
Debug.Assert IsArray(arr)
Debug.Assert dimNum > 0
'Note that it is possible for a VBA array to have no dimensions (i.e.
''LBound' raises an error even on the first dimension). This happens
'with "unallocated" (borrowing Chip Pearson's terminology; see
'http://www.cpearson.com/excel/VBAArrays.htm) dynamic arrays -
'essentially arrays that have been declared with 'Dim arr()' but never
'sized with 'ReDim', or arrays that have been deallocated with 'Erase'.
On Error Resume Next
Dim lb As Long
lb = LBound(arr, dimNum)
'No error (0) - array has given dimension
'Subscript out of range (9) - array doesn't have given dimension
arrHasDim = (Err.Number = ERR_VBA_NONE)
Debug.Assert (Err.Number = ERR_VBA_NONE Or Err.Number = ERR_VBA_SUBSCRIPT_OUT_OF_RANGE)
On Error GoTo 0
End Function
'"vect" = array of one and only one dimension
Public Function isVect(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 1) Then
isVect = Not arrHasDim(arg, 2)
End If
End Function
'"mat" = array of two and only two dimensions
Public Function isMat(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 2) Then
isMat = Not arrHasDim(arg, 3)
End If
End Function
Note the link to Chip Pearson's excellent web site: http://www.cpearson.com/excel/VBAArrays.htm
Also see: How do I determine if an array is initialized in VB6?. I personally don't like the undocumented behavior it relies on, and performance is rarely that important in the Excel VBA code I'm writing, but it's interesting nonetheless.
For arrays, MS has a nice method that involves looping through until an error occurs.
"This routine tests the array named Xarray by testing the LBound of each dimension. Using a For...Next loop, the routine cycles through the number of possible array dimensions, up to 60000, until an error is generated. Then the error handler takes the counter step that the loop failed on, subtracts one (because the previous one was the last one without an error), and displays the result in a message box...."
http://support.microsoft.com/kb/152288
Cleaned-up version of code (decided to write as a function, not sub):
Function NumberOfDimensions(ByVal vArray As Variant) As Long
Dim dimnum As Long
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
NumberOfDimensions = dimnum - 1
End Function
Microsoft has documented the structure of VARIANT and SAFEARRAY, and using those you can parse the binary data to get the dimensions.
Create a normal code module. I call mine "mdlDims". You would use it by calling the simple function 'GetDims' and passing it an array.
Option Compare Database
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx
Private Type SAFEARRAY
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
'Variants are all 16 bytes, but they are split up differently based on the contained type
'VBA doesn't have the ability to Union, so a Type is limited to representing one layout
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
Private Type ARRAY_VARIANT
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
lpSAFEARRAY As Long
data(4) As Byte
End Type
'http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170(v=vs.85).aspx
Private Enum VARENUM
VT_EMPTY = &H0
VT_NULL
VT_I2
VT_I4
VT_R4
VT_R8
VT_CY
VT_DATE
VT_BSTR
VT_DISPATCH
VT_ERROR
VT_BOOL
VT_VARIANT
VT_UNKNOWN
VT_DECIMAL
VT_I1 = &H10
VT_UI1
VT_UI2
VT_I8
VT_UI8
VT_INT
VT_VOID
VT_HRESULT
VT_PTR
VT_SAFEARRAY
VT_CARRAY
VT_USERDEFINED
VT_LPSTR
VT_LPWSTR
VT_RECORD = &H24
VT_INT_PTR
VT_UINT_PTR
VT_ARRAY = &H2000
VT_BYREF = &H4000
End Enum
Public Function GetDims(VarSafeArray As Variant) As Integer
Dim varArray As ARRAY_VARIANT
Dim lpSAFEARRAY As Long
Dim sArr As SAFEARRAY
'Inspect the Variant
CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16&
'If the Variant is pointing to an array...
If varArray.vt And (VARENUM.VT_ARRAY Or VARENUM.VT_BYREF) Then
'Get the pointer to the SAFEARRAY from the Variant
CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4&
'If the pointer is not Null
If Not lpSAFEARRAY = 0 Then
'Read the array dimensions from the SAFEARRAY
CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr)
'and return them
GetDims = sArr.cDims
Else
'The array is uninitialized
GetDims = 0
End If
Else
'Not an array, you could choose to raise an error here
GetDims = 0
End If
End Function
I presume you mean without using On Error Resume Next which most programmers dislike and which also means that during debugging you can't use 'Break On All Errors' to get the code to stop dead (Tools->Options->General->Error Trapping->Break on All Errors).
For me one solution is to bury any On Error Resume Next into a compiled DLL, in the old days this would have been VB6. Today you could use VB.NET but I choose to use C#.
If Visual Studio is available to you then here is some source. It will return a dictionary, the Dicitionary.Count will return the number of dimensions. The items will also contain the LBound and UBound as a concatenated string. I'm always querying an array not just for its dimensions but also for LBound and UBound of those dimensions so I put these together and return a whole bundle of info in a Scripting Dictionary
Here is C# source, start a Class Library calling it BuryVBAErrorsCS, set ComVisible(true) add a reference to COM library 'Microsoft Scripting Runtime', Register for Interop.
using Microsoft.VisualBasic;
using System;
using System.Runtime.InteropServices;
namespace BuryVBAErrorsCS
{
// Requires adding a reference to COM library Microsoft Scripting Runtime
// In AssemblyInfo.cs set ComVisible(true);
// In Build tab check 'Register for Interop'
public interface IDimensionsAndBounds
{
Scripting.Dictionary DimsAndBounds(Object v);
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDimensionsAndBounds))]
public class CDimensionsAndBounds : IDimensionsAndBounds
{
public Scripting.Dictionary DimsAndBounds(Object v)
{
Scripting.Dictionary dicDimsAndBounds;
dicDimsAndBounds = new Scripting.Dictionary();
try
{
for (Int32 lDimensionLoop = 1; lDimensionLoop < 30; lDimensionLoop++)
{
long vLBound = Information.LBound((Array)v, lDimensionLoop);
long vUBound = Information.UBound((Array)v, lDimensionLoop);
string concat = (string)vLBound.ToString() + " " + (string)vUBound.ToString();
dicDimsAndBounds.Add(lDimensionLoop, concat);
}
}
catch (Exception)
{
}
return dicDimsAndBounds;
}
}
}
For Excel client VBA code here is some source
Sub TestCDimensionsAndBounds()
'* requires Tools->References->BuryVBAErrorsCS.tlb
Dim rng As Excel.Range
Set rng = ThisWorkbook.Worksheets.Item(1).Range("B4:c7")
Dim v As Variant
v = rng.Value2
Dim o As BuryVBAErrorsCS.CDimensionsAndBounds
Set o = New BuryVBAErrorsCS.CDimensionsAndBounds
Dim dic As Scripting.Dictionary
Set dic = o.DimsAndBounds(v)
Debug.Assert dic.Items()(0) = "1 4"
Debug.Assert dic.Items()(1) = "1 2"
Dim s(1 To 2, 2 To 3, 3 To 4, 4 To 5, 5 To 6)
Set dic = o.DimsAndBounds(s)
Debug.Assert dic.Items()(0) = "1 2"
Debug.Assert dic.Items()(1) = "2 3"
Debug.Assert dic.Items()(2) = "3 4"
Debug.Assert dic.Items()(3) = "4 5"
Debug.Assert dic.Items()(4) = "5 6"
Stop
End Sub
NOTE WELL: This answer handles grid variants pulled off a worksheet with Range.Value as well as arrays created in code using Dim s(1) etc.! Some of the other answers do not do this.
I like to use the fact that with an error, the new variable-value is not charged.
To get the dimension (A_Dim) of an Array (vArray) you can use following code:
On Error Resume Next
A_Dim = -1
Do Until A = "X"
A_Dim = A_Dim + 1
A = "X"
A = UBound(vArray, A_Dim + 1)
Loop
On Error GoTo 0
Function ArrayDimension(ByRef ArrayX As Variant) As Byte
Dim i As Integer, a As String, arDim As Byte
On Error Resume Next
i = 0
Do
a = CStr(ArrayX(0, i))
If Err.Number > 0 Then
arDim = i
On Error GoTo 0
Exit Do
Else
i = i + 1
End If
Loop
If arDim = 0 Then arDim = 1
ArrayDimension = arDim
End Function
I found a pretty simple way to check, probably laden with a bunch of coding faux pas, incorrect lingo, and ill advised techniques but never the less:
Dim i as Long
Dim VarCount as Long
Dim Var as Variant
'generate your variant here
i = 0
VarCount = 0
recheck1:
If IsEmpty(Var(i)) = True Then GoTo VarCalc
i = i + 1
GoTo recheck1
VarCalc:
VarCount= i - 1
Note: VarCount will obviously return a negative number if Var(0) doesn't exist. VarCount is the max reference number for use with Var(i), i is the number of variants you have.
What about just using ubound(var) + 1? That should give you the last element of most of variables (unless it's a custom range, but in that case you should know that info already). The range of a conventional variable (for instance, when using the split function) starts with 0; ubound gives you the last item of the variable. So if you have a variable with 8 elements, for instance, it will go from 0 (lbound) to 7 (ubound), and you can know the quantity of elements just adding ubound(var) + 1. For example:
Public Sub PrintQntElements()
Dim str As String
Dim var As Variant
Dim i As Integer
str = "Element1!Element2!Element3!Element4!Element5!Element6!Element7!Element8"
var = Split(str, "!")
i = UBound(var) + 1
Debug.Print "First element: " & LBound(var)
Debug.Print "Last element: " & UBound(var)
Debug.Print "Quantity of elements: " & i
End Sub
It will print this output to the Inmediate window:
First element: 0
Last element: 7
Quantity of elements: 8
Also, if you are not sure that the first element (lbound) is 0, you can just use:
i = UBound(var) - LBound(var) + 1