vba variant array bug - arrays

I am fairly new to excel vba and I can't seem to fix this problem with vbArrays. I created the function cumsum in vba just to make my life easier. However, I want to make the code flexible such that I can pass in both variants from a function and also a range. In my code, when I added the line vec=vec.value if I am passing in a range, it works perfectly fine but it doesn't work if I want it to work if I call the function and pass in a non range type. What I noticed was if I didn't have the line vec=vec.value in my code and I pass in a range, it has dimension 0 and I checked by writing my own function. Can someone please explain to me how I can fix this problem? Thanks.
Public Function cumsum(vec As Variant) As Variant
Dim temp() As Variant
MsgBox (getDimension(vec))
'works if i use vec=vec.value if vec is a range but has 0 if i do not vec = vec.values
ReDim temp(LBound(vec, 1) To UBound(vec, 1), 1 To 1) As Variant
Dim intCounter As Integer
For intCounter = LBound(vec) To UBound(vec)
If intCounter = LBound(vec) Then
temp(intCounter, 1) = vec(intCounter, 1)
Else
temp(intCounter, 1) = temp(intCounter - 1, 1) + vec(intCounter, 1)
End If
Next
cumsum = temp()
End Function
Function getDimension(var As Variant) As Integer
On Error GoTo Err:
Dim i As Integer
Dim tmp As Integer
i = 0
Do While True:
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function

Why don't you just check the data type of vec by using VarType and TypeName then perform the necessary manipulation on vec
Public Function cumsum2(vec As Variant) As Variant
MsgBox TypeName(vec)
MsgBox VarType(vec)
cumsum2 = 0
End Function

The answers from #Jake and #chris are hints in the right direction, but I don't think they go far enough.
If you are absolutely sure that you'll only ever call this routine as a UDF (i.e. from formulas in your worksheets), then all you really need to do is add this:
If IsObject(vec) Then
Debug.Assert TypeOf vec Is Range
vec = vec.Value2
End If
to the start of your function. Called as a UDF, the only object type it should ever get passed is Range. Also, called as a UDF, you can rely on the fact that any arrays it gets passed will be indexed starting from 1.
I could pick out other problems with your routine, but they would be beside the point of your original question. Briefly: this will only work on column vectors, it will fail for single-cell ranges, etc.
Note that the reason your getDimension function is returning zero for Ranges because UBound is choking on the range. Your error handler happily catches an error (type mismatch) you didn't really expect to get and returning zero. (That method of finding "dimension" is assuming the error will be a subscript out range error.)
I wrote an answer a while back describing why, when working with Excel, I don't think the general getDimension approach is a good one:
https://stackoverflow.com/a/6904433/58845
Finally, the issue with VarType is that, when passed an object that has a default property, it will actually return the type of the property. So VarType(<range>) is going to tell you the type of the stuff in the range, not the code for object, because Range has a default property, Range.Value.

Modify your getDimension to include
If TypeName(var) = "Range" Then
var = var.Value
End If

Related

Can an array be declared as a constant?

Is it possible to either:
Declare an array as a constant
OR
Use a workaround to declare an array that is protected from adding, deleting or changing elements, and therefore functionally constant during the life of a macro?
Of course I could do this:
Const myConstant1 As Integer = 2
Const myConstant2 As Integer = 13
Const myConstant3 As Integer = 17
Const myConstant4 ...and so on
...but it loses the elegance of working with arrays. I could also load the constants into an array, and reload them each time I use them, but any failure to reload the array with those constant values before use could expose the code to a "constant" value that has changed.
Any workable answer is welcome but the ideal answer is one that can be setup once and not require any changes/maintenance when other code is modified.
You could use a function to return the array and use the function as an array.
Function ContantArray()
ContantArray = Array(2, 13, 17)
End Function
How about making it a function? Such as:
Public Function myConstant(ByVal idx As Integer) As Integer
myConstant = Array(2, 13, 17, 23)(idx - 1)
End Function
Sub Test()
Debug.Print myConstant(1)
Debug.Print myConstant(2)
Debug.Print myConstant(3)
Debug.Print myConstant(4)
End Sub
Nobody can change it, resize it, or edit its content... Moreover, you can define your constants on just one line!
I declared a String constant of "1,2,3,4,5" and then used Split to create a new array, like so:
Public Const myArray = "1,2,3,4,5"
Public Sub createArray()
Dim i As Integer
A = Split(myArray, ",")
For i = LBound(A) To UBound(A)
Debug.Print A(i)
Next i
End Sub
When I tried to use ReDim or ReDim Preserve on A it did not let me. The downfall of this method is that you can still edit the values of the array, even if you can't change the size.
If the specific VBA environment is Excel-VBA then a nice syntax is available from the Excel Application's Evaluate method which can be shortened to just square brackets.
Look at this
Sub XlSerialization1()
Dim v
v = [{1,2;"foo",4.5}]
Debug.Assert v(1, 1) = 1
Debug.Assert v(1, 2) = 2
Debug.Assert v(2, 1) = "foo"
Debug.Assert v(2, 2) = 4.5
'* write all cells in one line
Sheet1.Cells(1, 1).Resize(2, 2).Value2 = v
End Sub
If you don't need a new instance each time you can use a Static local variable to avoid multiple objects creation and initialization:
Private Function MyConstants()
Static constants As Variant
If IsEmpty(constants) Then
constants = Array(2, 13, 17)
End If
MyConstants = constants
End Function
Can an array be declared as a constant? No.
Workarounds - Simplest one I can think of is to define a constant with delim and then use Split function to create an array.
Const myConstant = "2,13,17"
Sub Test()
i = Split(myConstant, ",")
For j = LBound(i) To UBound(i)
Debug.Print i(j)
Next
End Sub
Is this too simplistic?
PUBLIC CONST MyArray = "1,2,3,4"
then later in a module:
Dim Arr as Variant
SET Arr = split(MyArray,",")
I know this is an old question, but these archives are often scanned for many years after being posted, so I don't see a problem with adding things long after the origin date.
How about creating a class, with a read-only property returning the 'array' value? You can specify a parameter using the same syntax as an array index, and defining only a GET property effectively makes it read-only. Define the constant values inside the class and it will work just like a constant array, even though the actual construction is different.
No - arrays can't be declared as constant but you can use a workaround.
You can create a function that returns the array you want
http://www.vbaexpress.com/forum/showthread.php?1233-Solved-Declare-a-Constant-Array
Using above information, I came to following working solution for comparing short text information of a month, independent from Excel using German language:
Const MONATE = ",Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez"
.. and later in the code:
If StringToCompare = Split(MONATE, ",")(Month(dt)) Then
NOTE: as the Split-Array starts with index 0 I added the comma in the beginning.
Don't know when this changed, but in Excel 365, this works (or, at least, does not generate a compiler error):
Const table1Defs As Variant = Array("value 1", 42, Range("A1:D20"))

Distinguishing between 'Null' and '0' when taking the sum of a range of columns

I am trying to distinguish between no input or "Null" and the input of something including the number 0.
I wrote a public function called "ZeroToAppear" that works well enough when used with Index Match Functions by returning the number 0 as a string, but it will not work along with a sum function which is common in financial budgets:
Public Function ZeroToAppear(x As Variant) As Variant
If IsNull(x) Then
ZeroToAppear = Null
ElseIf x = 0 Then
ZeroToAppear = CStr(x)
Else
ZeroToAppear = x
End If
End Function
I have rationalized that the problem is that excel automatically considers null as a 0 in order to avoid ArgumentNullExceptions.
So I am trying to write another Macro that will work when taking the sum of a range that can distinguish between no input and 0 or greater input since the sum of cells with no input automatically equals zero in excel and I would like it to report null or even better report false in order to not do the sum at all.
I have started writing a function that tests each cell in the range that I would be summing to see whether it is null, error, or something. If it is null or error, I want it to report null into a test array. If there is some other input I want it to report whatever that input is into the test array. Then I want to identify if the entire test array is reporting null to make my original function false & not run the sum in the range that I am testing but if there are other values then the function should return true and the sum can be run.
Public Function NullOrErrorFalse() As Variant
Dim arrOutput() As Variant
ReDim arrOutput(n) As Variant
n = 0
For Each cell In NullOrErrorFalse()
If IsNull(cell) Then
arrOutput(n) = Null
ElseIf IsError(cell) Then
arrOutput(n) = Null
Else
arrOutput(n) = cell.Value
End If
n = n + 1
Next cell
Sub test(arrOutput())
If arrOutput() = Null Then
NullOrErrorFalse = False
Else
NullOrErrorFalse = True
End If
End Sub
End Function
At this point my function won't compile correctly and being new to VBA and programming in general, I am not sure if my issue is misuse or syntax or order of operations.
Yep, there is programming flaw due to misunderstanding VBA syntax.
Public Function NullOrErrorFalse() As Variant
Dim arrOutput() As Variant
....
For Each cell In NullOrErrorFalse()
NullOrErrorFalse is the name of the function, not the parameter you are examining. When changing it, you are actually changing the result. On the other hand, you need to provide your function with the parameter (range) that it will check.
You can do you custom sum as a User-Defined Function (UDF) in this way:
Public Function SumOrNull(r As Range) As Variant
SumOrNull = 0
For Each cel In r
If IsError(cel) Or cel = "" Or Not IsNumeric(cel.Value) Then
SumOrNull = CVErr(xlErrValue)
Exit Function
End If
SumOrNull = SumOrNull + cel.Value
Next
End Function
The idea is that as soon as there is any pattern that you dont want, your UDF raises an error, so that Excel displays #Value, and the cell using your UDF is considered as erroneous.

Which function for finding if a string is in an array (VBA) is better?

I have 2 functions that check to see if a string exists in an array.
I don't know which is better and if there are any reasons to use one over the other. Any help would be appreciated. Thank you :)
function 1
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
function 2
Function IsInArray(myArray As Variant, val As String) As Boolean
Dim i As Integer, found As Boolean
found = False
If Not Len(Join(myArray)) > 0 Then
found = False
Else
For i = 0 To UBound(myArray)
If myArray(i) = val Then
found = True
End If
Next i
End If
IsInArray = found
End Function
This is what I would do.
For each thing in Arr
If instr(Thing, StringToBeFound) > 0 then msgbox thing
Next
Your second function uses a lot of memory with a big array.
This is what happens when you join strings. I don't know if Join function uses stringbuilding or not. I doubt it does as nothing else in basic does. So ordinary concatination shuffles a lot of bytes around memory.
String Concatination
And don't join strings one character at a time. See this from a VBScript programmer. It requires 50,000 bytes and many allocation and deallocation to make a 100 character string.
http://blogs.msdn.com/b/ericlippert/archive/2003/10/20/53248.aspx
PS: We have the Mid statement in VBA (not to be confused with the Mid function) that allows string building as a generic solution to shuffling bytes. It only matters on large arrays/strings.
Both solutions should work. The first one is small and straightforward, and moreover it returns how many times the match was found.
The second solution has the advantage of being "under your control", since you know what exactly you are doing. For example, with a slight modification you can return the position of the first match. You can make it faster though by adding the statement "Exit For" after "found = true", since there is no need to check further...
Thanks to several people who input in this thread I was able to create this solution
Function IsInArrayyyy(stringToBeFound As String, arr As Variant) As Boolean
For Each thing In arr
If InStr(thing, stringToBeFound) > 0 Then
If Len(thing) = Len(stringToBeFound) Then 'remove this if exact match not needed
IsInArrayyyy = True: Exit Function
End If 'remove this if exact match not needed
End If
Next
End Function

passing arrays or ranges to VBA udf

I am trying to pass an array to a UDF to avoid massive duplication of code. As a simple example:
function USERFUNC1(inp as variant)
Dim array_size As Integer
dim i as integer
dim values as double
array_size = WorksheetFunction.CountA(inp)
for i = 1 to array_size
values = values + inp(i)
Next i
USERFUNC1 = values
End function
function USERFUNC2(input1 as variant, input2 as variant)
Dim array_size As Integer
dim i as integer
dim values as double
array_size = WorksheetFunction.CountA(input1)
redim nested_array(array_size)
for i = 1 to array_size
nested_array(i) = input1(i)+input2(i)
Next i
USERFUNC2= USERFUNC1(nested_array)
End function
In the example I have create I have the nested array that I am passing internally to the UDF. However when run this results in a by ref error. I am sure that this can be done but I appear to be missing something
EDIT
It seems what I wrote caused confusion, essentially I have a number of functions, the above is just to demonstrate the idea.
On some of the cells I am calculating a value using a function (call it fugacity) that picks up values into an array a range from the worksheet. In another function (phase equilibrium) I need to perform the same calculation (fugacity) within the second function (phase equilibrium) using values calculated within the second function. This requires me to pass the array from the second function into the first or write out the entire first function again within the second.
I could do that however it make the 2nd function much more difficult to debug as I can no longer be certain that the nested calculation is performing the right calculation, rather I need to check the whole thing. So far I have about 250 lines of code in the first and 300 line the second and within the second (phase equilibrium) I need to perform the first (fugacity) 4 times.
If you want USERFUNC1 to return an array of the values in the inp range, then merely:
function USERFUNC1(inp as Range) as Variant
USERFUNC1 = inp
End function
This will be a 1-based two dimensional array where the first dimension represents the rows, and the second dimension the columns in the original range inp.
I'm not sure what your end goal is, but you may not even need USERFUNC1

Assigning values to a dynamically resized array in Excel VBA

I am trying to populate a two dimensional array of ranges. I don't know how big the array will need to be, so I am using the ReDim and Preserve functions to dynamically re-size the array as required.
I am encountering runtime error 91: "Object variable or With block variable not set" when I run the code.
I am not an experienced coder, but I have managed to isolate the error, and am sure it is coming from the pseudo code below.
Can anyone see any mistakes I have made that would produce the runtime error?
Dim ArrayName() as Range
Dim counter as Integer
If condition = True Then
counter = counter + 1
ReDim Preserve ArrayName(0, counter - 1)
ArrayName(0, counter - 1) = Cells(counter, counter) 'I get a runtime error here
End If
Thank you.
If you want to store ranges within your array you need to add Set before problem line in this way:
Set ArrayName(0, counter - 1) = Cells(counter, counter)
But if you want to store values of the cells you need to change declaration line to this:
Dim ArrayName() as Double 'or String or Variant depending on value type you need to keep in array

Resources