Working with arrays in VBA - arrays

I'm having a bit of a problem working with arrays in VBA, where the same would be trivial in (almost) any other language:
Public Function getArray() As MyType()
'do a lot of work which returns an array of an unknown length'
'this is O(N^2) or something equally intensive so I only want to call this once'
End Function
Public Sub doSomething()
Dim myArray() As MyType
Set myArray = getArray() 'FAILS with "Cannot assign to array error"'
End Sub
I think it might be that I need to define the length of the array in advance, or ReDim a dynamic array. But I don't know the length of the returned array ahead of time, and I'd like to avoid calling the function twice:
Public Sub doSomething()
Dim myArray(0 To UBound(getArray()) As MyType 'not entirely sure if this would work, but it involves calling getArray twice which I'd like to avoid
Set myArray = getArray()
End Sub
In C# or Java the equivalent would be:
public MyType[] getArray(){
//do some work and return an array of an unknown length
}
public void doSomething(){
MyType[] myArray;
myArray = getArray(); //one line and I don't need to define the length of array beforehand
}

When assigning arrays of custom objects in vba you need to pass them around as variants I've included a full working sample.
Class Module named MyType:
Public Once As Integer
Public Twice As Integer
Public Thrice As Integer
Code in standard module:
Public Function getArray() As MyType()
Dim i As Integer, arr() As MyType
'do a lot of work which returns an array of an unknown length'
'this is O(N^2) or something equally intensive so I only want to call this once'
For i = 0 To Int(Rnd() * 6) + 1
ReDim Preserve arr(i)
Set arr(i) = New MyType
arr(i).Once = i
arr(i).Twice = i * 2
arr(i).Thrice = i * 3
Next i
getArray = arr
MsgBox "Long process complete"
End Function
Public Sub doSomething()
Static myArray() As MyType
Dim i As Integer
If UBound(myArray) = -1 Then
myArray = getArray()
End If
For i = LBound(myArray) To UBound(myArray)
Debug.Print myArray(i).Once & vbTab & _
myArray(i).Twice & vbTab & _
myArray(i).Thrice
Next i
End Sub
Public Sub Test()
Dim i As Integer
For i = 1 To 3
Debug.Print "Run Number " & i & vbCrLf & String(10, "-")
doSomething
Debug.Print
Next i
End Sub
The first time you run doSomething an array of random length will be generated and you will see a message box that says "Long process complete". Subsequent calls to doSomething will re-use the array created the first time.
If you copy this code and just run the Test sub it will call doSomething three times. You will see the message box once and the output of doSomething in the immediate window three times.

Well, you could pass the array as a reference to the function like this:
Public Sub MyFunc(ByRef arr() As MyType)
...
End Sub
Dim myArr() as MyType
MyFunc myArr
Inside the function you can ReDim your array as wanted.

It is indeed possible to return an array from a function in VBA. According to MSDN:
[Y]ou can also call a procedure that returns an array and assign that to another array. [ . . . ] Note that to return an array from a procedure, you simply assign the array to the name of the procedure.
So you just need to modify your existing code by removing Set from the assignment statement:
Public Function getArray() As MyType()
'do a lot of work which returns an array of an unknown length'
'this is O(N^2) or something equally intensive so I only want to call this once'
End Function
Public Sub doSomething()
Dim myArray() As MyType
myArray = getArray
End Sub

I think you just need to get rid of the set in Set myArray = getArray()
This behaves properly:
Option Explicit
Public Function getArray() As Integer()
Dim test(1 To 5) As Integer
test(1) = 2
test(2) = 4
getArray = test
End Function
Public Sub doSomething()
Dim myArray() As Integer
myArray = getArray()
Debug.Print (myArray(2))
End Sub

Related

UserForm variable scope: transfer 2D array values from userform2 to userform1

I have a problem transferring a 2D array between two userforms.
When I click on a CommandButton in userform1, it will open userform2. Then I click on CommandButton in userform2 for creating a 2D array. After this I terminate userform2 and want to transfer my 2D array into userform1.
My best try is calling a function in userform1 click event. I put this function into the userform2 module. But my userform2's function doesn't see 2D array from another subs in userform2. Private Sub userform_terminate() can see this 2D-array which was created in Private Sub CommandButton1_Click() but my function doesn't.
userform1:
Private Sub CommandButton1_Click()
dim results()
results = userform2.get2dArray()
End Sub
userform2:
Private myArray()
Private Sub CommandButton1_Click()
ReDim myArray(1 To 2, 1 To 2)
myArray(1, 1) = "arg1"
myArray(2, 1) = "arg2"
myArray(1, 2) = "arg3"
myArray(2, 2) = "arg4"
End Sub
Private Sub userform_terminate()
'here i can see every args in myArray
...
end sub
Function get2dArray()
'that function I called from userform1
userform2.show vbModal
get2dArray = myArray 'but myArray is empty
End Function
I want to transfer myArray from userform2 back to the main form userform1.
The main problem is userform2.get2dArray doesn't see the private variable myArray in userform2 module.
Making myArray global is also impossible.
Use a public function in a standard module (not in a userform) which takes an optional parameter (your 2D array).
The parameter is then stored in the function as a static variable. The next time the function is called, if the parameter is missing, then return the stored static variable. Here is the example:
Public Function store2DArray(Optional my2DArray As Variant) As Variant
Static storedArray As Variant
If IsMissing(my2DArray) Then
store2DArray = storedArray
Else
storedArray = my2DArray
End If
End Function
The usage would then be like this to store the array:
Sub Userform2Button1_Click()
store2DArray myArray
End Sub
This is how you would retrieve the array:
Sub Userform1Button2_Click()
myArray = store2DArray
End Sub

How to access populated array in another sub in VBA, w/o arguments?

I am so confused, and endless googling is so far defying me.
I have a sub that collected unique values in Column C into an array. Now that the array is created, I need to use it in a different sub so I can loop through the values.
I have tried passing it as an argument but then I can't figure out how to run the new sub that has arguments, i.e.:
Sub useArray(ByRef varArr() As String)
how in the world do I run useArray? And useArray should be the main sub, anyways, so I am just confused about how I could then run the main sub and use this array variable that has already been defined/populated.
I tried using my sub that gets the unique values as a function, but it doesn't pass the values in the array back to the main sub. At the end of the function AND in the main sub I have:
MsgBox varArr(1)
In the function, it returns the first value. Back in the sub it returns an error.
An assistance would save my sanity!
The preferred method is going to be turning your Sub into a Function:
Function useArray(ByRef varArr() As String) As string()
varArr(2) = "changed it"
useArray = varArr
End Function
Which you would call by:
Sub test()
Dim a(2) As String
a(0) = "a0"
a(1) = "a1"
a(2) = "a2"
MsgBox useArray(a)(2)
End Sub
Sub Main()
Call DoSomethingWithArray(PopulateArray)
End Sub
Function PopulateArray() As Variant
PopulateArray = Array("foo", "bar") ' return some array
End Function
Sub DoSomethingWithArray(ByVal someArray As Variant) ' process array
Dim i As Integer
For i = LBound(someArray) To UBound(someArray)
Debug.Print someArray(i)
Next i
End Sub

Type mismatch when changing argument types passed to a function

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

Function to write an array to an ArrayList

I am trying to create a VBA function which writes an array to a .NET System.Collections.ArrayList and returns it.
So far I have:
Function arrayToArrayList(inArray As Variant) As ArrayList
'function to take input array and output as arraylist
If IsArray(inArray) Then
Dim result As New ArrayList
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
result.Add inArray(i) 'throws the error
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = result
End Function
Called with (for example)
Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As ArrayList
Dim myArray As Variant
myArray = Split("this,will,be,an,array",",")
Set result = arrayToArrayList(myArray)
End Sub
But I get the error
Variable uses an Automation type not supported in Visual Basic
Presumably because my array is not the correct type (maybe Variant). However
Dim v As Variant, o As Variant
v = "test_string"
set o = New testClass
result.Add v 'add a string in variant form
result.Add o 'add an object in variant form
raises no errors, so the problem isn't directly to do with the Variant type
What's going on here, and is there any way of writing an array of unspecified type to the ArrayList, or will I have to define the type of inArray?
Change
result.Add inArray(i)
to
result.Add CVar(inArray(i))
Two ways to do this. First is late binding if you don't have a reference to mscorlib.dll. You'll see that I've changed your ArrayList to Object for declaring the function and the return value (retVal). The test sub also declares result as Object. The retVal and result are both late bound to System.Collections.ArrayList. You also need to declare inArray and myArray as dynamic arrays of string. In your example, Split expects returns an array of strings, so you need provide a declared dynamic array of strings. If you wanted to other object types, then you'd pass those declared object types to your function.
Private Function arrayToArrayList(inArray() As String) As Object
'function to take input array and output as arraylist
Dim retVal As Object
Set retVal = CreateObject("System.Collections.ArrayList")
If IsArray(inArray) Then
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
retVal.Add inArray(i)
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = retVal
End Function
Public Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As Object
Dim myArray() As String
Set result = CreateObject("System.Collections.ArrayList")
myArray = Split("this,will,be,an,array", ",")
Set result = arrayToArrayList(myArray)
End Sub
The second way is to add a reference to mscorlib.dll through the Tools->Reference menu item. When the dialog box appears you'll have to click browse. You'll need to browse to C:\Windows\Microsoft.NET\Framework and then select the folder with the current version of .NET on your machine. In that folder you'll find mscorlib.dll and mscorelib.tlb. Highlight the file ending in .TLB file, click the Open button, on the Tools Reference dialog, click OK.
Now you can use any of the classes in Systems.Collections directly in your code. This is called early binding and looks like this
Private Function arrayToArrayList(inArray() As String) As ArrayList
'function to take input array and output as arraylist
Dim retVal As ArrayList
If IsArray(inArray) Then
Dim i As Long
For i = LBound(inArray) To UBound(inArray)
retVal.Add inArray(i)
Next i
Else
Err.Raise 5
End If
Set arrayToArrayList = retVal
End Function
Public Sub testArrayListWriter()
'tests with variant/string array
'intend to pass array of custom class objects
Dim result As ArrayList
Dim myArray() As String
myArray = Split("this,will,be,an,array", ",")
Set result = arrayToArrayList(myArray)
End Sub
I think the problem is with the assignment of the return value from the Split function to a variable that has not been decalred anywhere.
Try adding:
Dim myArray() as string
inside the testArrayListWriter() procedure.

VBA - Public Array Error - Subscript out of range

I want to declare a public array, create it and then use it in another sub.
This is exapmle of what I wrote:
Public array1() As String
Sub Create_Array()
Dim array1(1 To 4) As String
array1(1) = "1"
array1(2) = "2"
array1(3) = "A"
array1(4) = "B"
End Sub
Sub Show_Some_Index()
Dim a As String
a = array1(1)
MsgBox (a)
End Sub
I get Error 9: "Subscript out of range".
Couldn't find an answer, what am I doing wrong?
Variable array1() in Sub Create_Array is scoped to that procedure - basically it's a local variable that's only ever accessible within that procedure, and it happens to have the same name as another public field declared elsewhere, so what's happening is that Show_Some_Index is working off an array that hasn't been initialized yet.
Dim is used for declaring variables. If you mean to re-dimension an array that's in-scope, use the ReDim keyword.
A better approach would be to use a function that returns the array, instead of relying on global variables.
I want to declare a public array, create it and then use it in another sub.
In that case remove the Dim Statement from your code. Further to what Mat explained, here is another way to make your code work
WAY 1
Public array1(1 To 4) As String
Sub Create_Array()
array1(1) = "1"
array1(2) = "2"
array1(3) = "A"
array1(4) = "B"
Show_Some_Index
End Sub
Sub Show_Some_Index()
Dim a As String
a = array1(1)
MsgBox (a)
End Sub
WAY 2
Public array1(1 To 4) As String
Sub Create_Array()
array1(1) = "1"
array1(2) = "2"
array1(3) = "A"
array1(4) = "B"
End Sub
Sub Show_Some_Index()
Create_Array
Dim a As String
a = array1(1)
MsgBox (a)
End Sub
Once you initialize it, you should be able to use it in other procedures.

Resources