What is the difference between `Array` and `Array()`? - vba

I could declare an array thus:
Dim arrTest() As Variant
or thus:
Dim arrTest2 As Variant
arrTest2 = Array()
however, the first can only be passed as an argument like this:
Sub(ByRef arrTest() As Variant)
and the second like this:
Sub(ByRef arrTest2 As Variant)
you can do this with the second:
ReDim arrTest2(UBound(arrTest2) + N)
but not with the first.
What is the difference between a variable array declared the first way, and a variable array declared the second?
They're both the same vartype() - 8204 - Array of variants
Why do macros treat them differently?

The first is an array of variants. It is always an array -- it can't be reassigned to be e.g. a Range. The second is a variant which can hold just about anything, including an array (which it does in this case). Consider the following code:
Sub test()
Dim arrTest() As Variant
Dim arrTest2 As Variant
arrTest2 = Array()
Debug.Print "arrTest is a " & TypeName(arrTest)
Debug.Print "arrTest2 is a " & TypeName(arrTest2)
End Sub
When you run it, you get this:
arrTest is a Variant()
arrTest2 is a Variant()
Which is what strikes you as strange. If they are the same type why does VBA sometimes treat them as different?
Answer -- they aren't the same type! typename (or just varType), when applied to a variant variable doesn't return the variables type at all. Instead it returns the subtype of the variable (a concept that only makes sense for variants). To get a clearer picture of what is happening -- put a break point before the first Debug.Print statement, run it, and open up the Locals Window:
Note how the type of arrTest is Variant() but arrTest2 is Variant/Variant(0 to -1). They really aren't the same type. arrTest is an array so it has to follow VBA syntax regarding array. arrTest2 isn't an array at all -- it is simple variable (of type variant) so it follows the VBA syntax of simple (non-array) variables. The fact that in this particular case it is pointing to an array doesn't make it an array variable.
If you are familiar with C, your question is similar to the question that many beginning C programmers ask about the difference between int and int*, only in some ways it is more mysterious here since the fact that functions like varType and TypeName implicitly dereference any variant variable is arguably a design flaw. In some way it would be nice if typename(arrTest2) would return the more accurate (albeit more verbose) Variant/Variant() in this case.

Related

Passing Lower and Upper Bound Array Dimensions as Arguments in VBA

I'm wondering if its possible to pass both lower and upper bound array sizes (using the "To" keyword) via arguments. Ultimately, I would like to do something like this:
sub foo
call bar(2 To 5)
end sub
sub bar(arrayDimensions)
dim myArray() as long
redim myArray(arrayDimensions)
end sub
But VBA throws a fit if I use the "To" keyword like this. Is there another easier alternative? Or am I doing something wrong? I know I could pass two arguments as a work around, but I would rather not do that if there's a better way.
Thanks in advance for any help.
Edit to clarify why I would like to do this instead of using two arguments.
I made an array class that I can use to store my arrays in and easily modify them (eg. myArray.fill(0)). But I want the user interface with this array class to be the same as just using a plan old array.
sub foo
dim regularArray() as long
redim regularArray(5)
regularArray(3) = 250
dim myArray as ArrayClass
set myArray = factory.newArrayClass(5)
myArray(3) = 250
end sub
This works great using default properties. My constructor is set up to either receive a one long argument to define the size of a 1D array. Or it can receive two long arguments to define the size of a 2D array. Or is can receive one range argument to build an array based on excel data. Or it can receive an array for its argument to instantiate with an array.
Right now my code works great as it is. But if I wanted to add a lower bound when I instantiate the class, lets say for a 2D array, then I have to add two more optional arguments, one for each dimensions.
That then leads to the question of: do two long arguments represent a 2D array, or does it represent the lower and upper bound of a 1D array. So it would get hairy in a hurry.
Passing two arguments is not a a work around, it is the way to do what you want.
Sub foo()
Call bar(2, 5)
End Sub
Sub bar(a As Long, b As Long)
Dim myArray() As Long
ReDim myArray(a To b)
End Sub
This works equally well, I guess:
Sub foo2()
Dim myArray(2 To 5) As Long
Call bar2(myArray)
End Sub
Sub bar2(myArray() As Long)
'Do stuff with myArray
End Sub
From a design point of view, it isn't clear why "foo" knows what dimensions are needed in "bar", and why "bar" can't set these dimensions (since "bar" is the function that is doing the work with "myArray" anyway). It is very unusual to see a construction like this. Also, for what its worth, I have never seen an array in practice that did not have a lower bound of either 0 or 1, so that is also a bit unusual.
You are trying to make things easier for your users which is to your credit. Unfortunately, this can make the programmers life a bit more complicated.
There are two alternative strategies.
1 use multiple constructors. Its not a sin to have multiple methods to construct your array so you might have methods such as
factory.newArrayClassBySize(5)
factory.newArrayClassOneDim(5,10)
factory.newArrayClassTwoDim(5,10,3,22)
factory.newArrayClassByArray(InputArray)
factory.newArrayClassByRange(inputXlRange)
This is, by far ,is my preferred method as it makes it very explicit what the code is doing.
An alternative strategy is to have 7 optional parameters where the name of the parameters makes it clear what the the constructor expects and will do. The limitation of this method is that users have to pay attention to the intellisense for the parameters
e.g.
Public Function newArrayClass _
( _
Optional Size as variant = Empty, _
Optional Lbound1 as variant = Empty, _
Optional Ubound1 as variant = Empty, _
Optional Lbound2 as variant = Empty, _
Optional Ubound2 as Variant = Empty, _
Optional XlRange as variant = Empty, _
Optional BasedOn as variant = Empty _
) as ArrayClass
Using the named parameters requires you todo more work to work out if a correct set of parameters has been provided.

What VBA variable type to use when reading values from a cell in Excel?

According to this answer one should always use Variant when assigning values in a cell to a variable in the code. Is this correct? I seem to recall reading elsewhere that using Variant indiscriminately is not a good practice.
You can read a cell value into any type you want, VBA will (try to) implicitly convert it to that type for you.
There are dozens of questions on this site involving run-time errors raised from reading cell values into a specific data type - perhaps you've seen this error message before?
Type mismatch
That's the error you get when you try to read a cell containing an error value (e.g. #REF!) into anything other than a Variant.
So if you read a cell value into, say, a Double, everything will work fine as long as you're reading something that VBA can coerce into that data type. The problem is that, well, data is never 100% clean, worksheets do break down, users delete columns and break formulas, lookups fail and the person that wrote the formula didn't bother wrapping it with IFERROR, etc.
That's why you read cell values into a Variant.
That doesn't mean you work with a Variant.
Dim cellValue As Variant
cellValue = someRange.Value
If IsError(cellValue) Then Exit Sub 'bail out before we blow up
Dim workingValue As String
workingValue = CStr(cellValue)
By assigning to another data type, you effectively cast the Variant to that more specific type - here a String. And because you like explicit type conversions, you use VBA's conversion functions to make the conversion explicit - here CStr.
Now, in real code, you probably wouldn't even bother reading it into a Variant - you can use IsError to test the cell value:
If IsError(someRange.Value) Then Exit Sub 'bail out before we blow up
Dim cellValue As String
cellValue = someRange.Value ' or cellValue = CStr(someRange.Value)
The flipside here is that you're accessing the cell twice. Whether or not that's better that reading it into a Variant is for you to decide; performance-wise, it's usually best to avoid accessing ranges as much as possible though.
The value you get from a cell (which is a Range) is a Variant according to the documentation:
Range.Value Property (Excel)
Returns or sets a Variant value that represents the value of the specified range.
Since a Variant can represent different data types, you could loose information if you would assign a cell's value to -- for instance -- a variable of type String.
The mere fact that there is data type information in a Variant already means you lose that type of information. If for instance the original type was numeric and you store it in a String variable, there is no way to know from that string value what the original data type was. You could also lose precision (on Date milliseconds for instance).
Furthermore, a Variant type value cannot always be cast to the data type of your variable, and so you could get a Type mismatch error. In practice this often happens with the Error sub data type.
Only when you know beforehand what the data type is of a certain cell's value, it would be good to define your receiving variable in that data type.
Not strictly answering your question, but thought I'd add this for reference anyway.
With native Excel functions you can usually provide either a range object or a value directly to a function. For example, you can either write =AVERAGE(A1,A2,A3) or =AVERAGE(10,20,30). If you want to do something similar for any user defined functions, you will need to check the type of object passed to your function:
Function test(input As Variant)
Dim var As Variant
If TypeName(input) = "Range" Then
var = input.Value
Else
var = input
End If
You may also want to check for other objects if your function can accept them, but doing this will make your functions behave more like users expect them to.

VBA, Excel, use Range to fill 2D-Array [duplicate]

This question already has answers here:
Why am I having issues assigning a Range to an Array of Variants
(3 answers)
Closed 7 years ago.
I do not understand this behaviour:
Sub tuEs()
Dim A() As Variant
A = Range("A1:A10") ' works
Dim B() As Variant
B = ActiveSheet.Range("A1:A10") ' Type mismatch
End Sub
The first version works the 2nd version does not. Why? What is the difference?
The way to go with this is by adding the ".value" at the end of the range. This is usually a good idea to make things very explicit (the reason you can omit this is because value is the default property for the range object)
I added all the values to watches to see what was going on and apparently there is a problem of Excel not been able to effectively ( and implicitly ) cast the object on the fly. Note in the picture how the expression that is failing "ActiveSheet.Range("A1:A10") is of type: Variant/Object/Range; the transition from Variant to object is most likely causing the issue.
A way to force it to cast correctly would be to split the process in two parts the first one casts to range and the second one casts to a variant array. Look at my example
Also notice that if you declare the variable as variant alone and not an array of variants (dim E and not dim E()) it will get it because the it will adapt to what is needed.
Sub tuEs()
'Works
Dim A() As Variant
A = Range("A1:A10")
' Type missmatch
Dim B() As Variant
B = ActiveSheet.Range("A1:A10")
' Fix to make it cast properly
Dim C() As Variant
Dim r As Range
Set r = ActiveSheet.Range("A1:A10")
C = r
' Best of all options
Dim d As Variant
d = ActiveSheet.Range("A1:A10").Value
End Sub
Hope this makes is somewhat clear.
This is indeed a mystery! But this works (no need to declare an array of variant objects, just a variant). As to why it doesn't work in your code as stated, I am afraid I can't answer.
Dim B As Variant ' instead of Dim B() as Variant
B = ActiveSheet.Range("A1:A10")
At first I thought it to be a syntax issue - some hidden ambiguity that caused the interpreter to respond differently to the different statements. But to my dismay the following code works flawlessly:
Dim B() as Variant
B = Application.Range("A1:A10")
As it is syntatically identical to the crashing line in the question, the only possible conclusion, AFAIK is that the implementations of Range in the Worksheet and in the Application classes return different types of objects even if they contain, in essence, the same information. Most casts will render the same results, but for some implementation quirk, only the Application version can be cast to an array of variants. This conclusion is backed by the fact that inspecting the results of expressions Range("A1:A10") and ActiveSheet.Range("A1:A10") in debug mode result in different type informations - "Object/Range" in the first case and "Variant/Object/Range" in the second.
If this is true, the exact reason for the difference (if not an accident at all) is probably known only by the coders or other folks at MS. I am curious if this is consistent thru different versions of Office..

Why do Len and VBA.Len return different results?

When I run the following macro:
Sub try()
Dim num As Integer
num = 123
MsgBox Len(num)
MsgBox VBA.Len(num)
End Sub
The first Msgbox displays 2 and the second Msgbox displays 3.
If I remove the first line which says Dim num As Integer, both MsgBoxes display 3.
Can anyone explain why?
Len and LenB are not just ordinary functions, they are keywords, and as such are found on the list of VBA keywords (although LenB is only mentioned after you click through to Len).
Mid would be another example of such keyword disguised as function, whereas e.g. Left isn't on the list at all, because that is just an ordinary function.
It has to be a keyword because one of its jobs is to perform the compile-time task of determining the storage size of a variable. E.g. with private user-defined types, an ordinary function could not do that at all:
Private Type foo
a As Long
b As String
End Type
Sub TestLens()
Dim f As foo
MsgBox Len(f) ' OK
MsgBox VBA.Len(f) ' Compile time error
End Sub
The fact that the object browser brings you to VBA.Len when you press Shift+F2 on that Len(f) is both correct and misleading.
The Len(f) here does not actually call the VBA.Len function that determines the string size, it simply cannot do that because it would require coercing a private struct into a Variant. Instead it calculates the size of foo at compile time and substitutes the result as a literal constant into the executable.
In your example the Len(num) calculates the compile-time size of variable num (which is 2) and substitutes the constant 2 into the resulting object code, whereas VBA.Len(num) packs the value of num into a Variant and passes that variant to VBA.Len where it's further converted to string "123" and the length of that is returned.
It has to do with the way that VB stores integers and how the Len() function handles arguments that are not strings.
When a datatype that is not a string is passed to the Len() function, it returns the nominal number of bytes used to store the data (VB uses 2 bytes to store an integer). See the documentation for the Len function.
The Len() function will automatically cast the variant variable (which is created by assigning a value to a variable without declaring it first) as a string. The return isn't changing because the storage allocation changes (although it does; variants require 16 bytes of storage space, minimum). Since the implicitly declared variable is actually a variant type, VB will automatically change its type based on the situation. In this case, Len() expects a string so VB makes the variable a string.
You could use Msgbox Len(Cstr(num)) to cast the integer variable as a string before passing it to the Len function if your intent is to return the number of characters in your integer value.

Function calls another function but gets "stuck"

I'm an occasional VBA programmer, for fun (not my job).
I have a series of VBA modules in MS Excel 2010. Can't figure out what I did wrong. This routine worked, then I changed some things and it stopped working. One of the things I did was split the code from a single function to two functions in two modules. I think it worked for a while after I split it into two, but now I can't remember if that is true since I've tried so many things to make it work again. Luckily, I saved the older version with all of the code in a single function, and it still works. It returns a large array to the spreadsheet.
Fundamentally, I have a worksheet that calls a function. That function calls another function. Using the Debug - Toggle Breakpoints in combination with some MsgBox calls, I have figured out that the first function runs down to the point that it calls the second function. Then the second function runs down to the "End Function" command. At that point, the name at the top of the worksheet flickers a few times...and nothing. While debugging, the program does not seem to return to the first function. The array that is supposed to be populated in the first function is filled with #Value.
I read several places where VBA can be corrupted, so shutting everything down and rebooting can fix it. It didn't. Then I read that if you copy/paste everything into a new worksheet, with new modules (yes, a LOT of copy/pasting), that can clean it up. It didn't.
I also read of an issue where dimensioning arrays had problems when the dimensions were input variables to the function. So I initialized the variables used to set the array dimensions, even though they were input variables to the function. Maybe that's a problem?
The code is really long, so I've only included the call to the second function, the declarations of variables in the second function, and a few other things. Maybe I screwed up some syntax when I passed variables?
The first function:
Option Explicit 'forces all variables to be explicitly declared
Function InputOutputDVL(InData As Variant)
'---------------------------------------------------------------------------------------------
Dim g, p, ng, np, ns, ID, count As Integer
Dim ngmax, npmax, nsmax, namax, nxmax, nymax As Integer
Dim row As Integer
Dim panelmax As Integer
Dim TLstyle As Integer
Dim Group(), Part(), Section(), Airfoil() As Variant
Dim ABP(), TV() As Double
ngmax = 20
npmax = 100
nsmax = 1000
namax = 10
ReDim Group(1 To ngmax, 1 To 4)
ReDim Part(1 To npmax, 1 To 6)
ReDim Section(1 To nsmax, 1 To 17)
ReDim Airfoil(0 To 100, 0 To 2 * namax + 1)
'missing code here
MsgBox ng & " " & np 'This msgbox works correctly and give the right value for np
ABP = Section2VL(nxmax, nymax, ns, panelmax, Group, Part, Section, Airfoil)
MsgBox "Completed Section2VL" 'The code never gets to this msgbox
InputOutputDVL = ABP 'I've tried setting this to = 1234 assuming there was a problem with
'ABP, but the cells on the spreadsheet are still #Value
End Function
The second function:
Option Explicit 'forces all variables to be explicitly declared
Function Section2VL(nxmax, nymax, ns, panelmax, Group, Part, Section, Airfoil)
Dim i, j, k, l, c1, c2 As Integer
Dim g, p, s, ng, np, chord, span, panel, ID, count As Integer
Dim NX, NY As Integer
Dim station, panelID, panelIDref As Integer
Dim pi, Xstyle, Ystyle As Double
Dim angle, dist As Double
Dim sx(), sy() As Double
Dim Scoord(), ABP() As Double
ns = 6
nxmax = 12
nymax = 12
panelmax = 300
ReDim sx(0 To nxmax), sy(0 To nymax)
ReDim Scoord(1 To ns, 0 To nxmax, 1 To 3), ABP(1 To panelmax, 1 To 32)
MsgBox ABP(panelmax, 5) 'This call works, and provides the proper value in the msgbox
'return ABP vector
Section2VL = ABP
'I've also tried just returning an integer thinking there was something wrong with the
'ABP array, like 987, but that doesn't work either
End Function 'This is where it stops when debugging. Doesn't return to first function
Thanks in advance. I've blown two long evenings and can't figure it out.
You issue is incompatable types between InputOutputDVL.ABP, Section2VL.ABP and Section2VL itself
Try declaring all thses () As Double
ie.
In InputOutputDVL
Dim ABP() As Double
In Section2VL
Dim ABP() As Double
And
Function Section2VL(...) As Double()
Note on Type declarations
When you declare variables like this
Dim i, j, k, l, c1, c2 As Integer
all except the last one are infact declared as Variant's
You need to specify each variables type explicitly.
Also, don't use Integer unless you have a specific reason. Use Long instead.
Thanks Chris! You definitely led me in the right direction. I also used these two websites:
http://www.cpearson.com/excel/passingandreturningarrays.htm
http://www.cpearson.com/excel/vbaarrays.htm
Incomplete/incompatible variable declarations were the source of my problem.
First, I had forgotten that VBA requires "as Integer" or "as Double" after EACH variable, not just at the end of the line. So many of variables were VARIANTS instead of integers, doubles, etc. Thanks again Chris!
Second, and more specific, I only had to make one of Chris' changes noted above, and that was to properly declare ABP() as Double in the first calling function. ABP() was already properly called Double in the second function, and I did NOT declare the Function Section2VL as Double(). With the original code:
Dim ABP(), TV() as Double
This indicated that ABP was a VARIANT ARRAY. The later call to ABP = Section2VL() was then trying to stuff a VARIANT into a VARIANT ARRAY, and THAT was the problem. I'm still disappointed that the compiler didn't somehow say it had incompatible data types or some other error. This also explains where the problem came from. The code was previously working as two functions in two modules. As I was making some other changes, I noticed that I hadn't declared ABP as an Array, so I added the "()". I made other changes, and it stopped working. I focused on the other changes, not the seemingly minor "correction" of adding the () marks. What was really happening was that in the original code a combination of mistakes had resulted in a correctly working code! Imagine that!
So the code worked with ABP as a variant, or ABP() as a double array, but NOT with ABP() as a variant array.
Of course, the correct answer here is what Chris suggested, and that is correctly and explicitly declaring all of the variables. An interesting note is that a VARIANT (not VARIANT ARRAY) can essentially "accept" being assigned any incoming value, including a DOUBLE ARRAY.
So in my final code, I've left both Functions as VARIANTs, but I've now specifically declared them as VARIANT to make it clear. In the second function, ABP is properly declared as a dynamic DOUBLE ARRAY, and later given the proper dimensions to allocate memory space. In the first function, ABP could either be a VARIANT or a DOUBLE ARRAY, and either would work, but since I always want it to be a DOUBLE ARRAY, I've specified it as such.
Option Explicit 'forces all variables to be explicitly declared
Function InputOutputDVL(InData As Variant) as Variant
'---------------------------------------------------------------------------------------------
Dim ABP() as Double, TV() As Double 'This ABP was a variant array - which was incompatible
'code removed here
ABP = Section2VL(nxmax, nymax, ns, panelmax, Group, Part, Section, Airfoil)
InputOutputDVL = ABP
End Function
And the second, called function:
Option Explicit 'forces all variables to be explicitly declared
Function Section2VL(nxmax as integer, nymax as integer... ) as Variant
Dim Scoord(), ABP() As Double 'This was already correctly declaring ABP as Double Array, but
'now I realize Scoord was incorrectly a Variant Array, but it
'wasn't actually causing a problem with my code.
'I fixed this in my own code, but left here as example of what
'not to do!
ReDim ABP(1 To panelmax, 1 To 32)
'Code removed here
'return ABP vector
Section2VL = ABP
End Function