Excel VBA Passing in an array of 2D arrays - excel

I am working with an array of 2D arrays in Excel VBA. I have a function...
Public Function constructStack(vbr() As Variant, hr As Integer) As stack
Where stack is a class I made. I have another function, in which I am calling constructStack from. here is the call:
Set stacks(i) = stack(i).constructStack(vbr(i), i)
vbr happens to be an array of 2D arrays. Seeing as vbr(i) would refer to a single 2D array of type variant, I'm confused why I'm getting the "Type mismatch: array or user-defined type expected," compile error.
It's almost as if the compiler doesn't realize that vbr() will be filled with 24 2D arrays, which is why it's giving me the compile error. Here is how I Dim vbr:
Dim vbr(1 To 24) As Variant
After declaring vbr, I eventually run this for loop which assigns each element of vbr a 2D array...
vb = GetVBRSorted
For j = 1 To 24
For i = 2 To 2000
If (vb(i, 1)(j) <> "") Then
lastFilleds(j) = i
End If
Next
Next
For j = 1 To 24
ReDim vbrTemp(1 To lastFilleds(j) - 1, 1 To 5)
For i = 2 To lastFilleds(j)
For k = 1 To 5
vbrTemp(i - 1, k) = vb(i, k)(j)
Next
Next
vbr(j) = vbrTemp
Next
GetVBRSorted returns the exact same type as vbr - an array of 2D arrays. If anyone has any input on this issue, it would be much appreciated.

Because each individual vbr(i) is a Variant, this is what you must declare as your parameter type. There is no magical compile-time sniffing to realise the Variant contains an array.
Use
Public Function constructStack(vbr As Variant, hr As Integer) As stack
Also see How can I use an optional array argument in a VBA procedure?.

the call you have .constructStack(vbr(i), i) is passing a single element of the array. If you want to pass the whole array then you would use .constructStack(vbr, i)

Related

Using a FOR loop within an Excel VBA Function

I created a simple function in MATLAB, and am trying to convert the function into Excel VBA function. My goal is to create an Excel formula =RT('range of dB levels', 'delta-time') and output the estimated reverberation time. The math is simple, see MATLAB code below:
function rr=RT(lvl_broad, dt)
n=12; %number of samples within slope calc
slope=zeros(length(lvl_broad), 1);
for i=1:length(lvl_broad)
if i<((n/2)+1) | i>length(lvl_broad)-(n/2)-1
slope(i)=0;
else
slope(i)=(lvl_broad(i+(n/2))-lvl_broad(i-(n/2)))/n;
end
end
min_slope=min(slope);
rr=abs(dt./min_slope)*60;
end
In excel, I modified/simplified this until I no longer got errors, however, the cell that I enter my 'RT' function in returns #VALUE and I do not know why. Does anything stand out in the code below? (note I changed the input range from lvl_broad to InterruptedNZ)
Function RT(InterruptedNZ, dt)
Dim Slope As Double
Slope = Slope(InterruptedNZ.Height, 1)
For i = 1 To InterruptedNZ.Height
If i < ((6) + 1) Or i > (InterruptedNZ.Height - (6) - 1) Then
Slope(i) = 0
Else
Slope(i) = (InterruptedNZ(i + (6)) - InterruptedNZ(i - (6))) / 12
End If
Next
End
min_slope = Application.WorksheetFunction.Min(Slope)
RT = Abs((dt / min_slope) * 60)
End Function
Here are some tips to translate MATLAB code into VBA code:
length()
If you are trying to get the dimensions of a range, you'll need to use the .Rows.Count or .Columns.Count properties on the range you are working with.
PERFORMANCE NOTE:
When you have a large enough range, it's a good idea to store the values of the range inside an array since you will reduce the number of times you access data from the sheets which can comme with lot of overhead. If so, you'll have to use ubound() and lbound().
zeros()
In VBA, there is no exact equivalent to the zeros() function in MATLAB. The way we would initialize an array of zeros would simply be by initializing an array of doubles (or another numerical type). And since the default value of a double is zero, we don't need to do anything else :
Dim Slope() As Double
ReDim Slope(1 To InterruptedNZ.Rows.Count)
Note that you cannot pass the dimensions in the Dim statement since it only accepts constants as arguments, so we need to create Slope as a dynamic array of doubles and then redimension it to the desired size.
Putting these two principles together, it seems like your code would look something like this:
Function RT(ByRef InterruptedNZ As Range, ByVal dt As Double)
Dim Slope() As Double
ReDim Slope(1 To InterruptedNZ.Rows.Count)
Dim i As Long
For i = 1 To InterruptedNZ.Rows.Count
If i < ((6) + 1) Or i > (InterruptedNZ.Rows.Count - (6) - 1) Then
Slope(i) = 0
Else
Slope(i) = (InterruptedNZ(i + (6)) - InterruptedNZ(i - (6))) / 12
End If
Next
Dim min_slope As Double
min_slope = Application.WorksheetFunction.Min(Slope)
RT = Abs((dt / min_slope) * 60)
End Function
Addtionnal notes:
Refering to cells from a range like this InterruptedNZ(i) works but it is good practice to be more specific like this (assuming column range) :
InterruptedNZ.Cells(i,1)
During my tests, I had a division by zero error since min_slope was zero. You might want to account for that in your code.

Excel VBA: Transpose Vector and Matrix Multiply in a function

I have an 3x3 matrix "mat" and a vector "vec" (3x1) of real numbers I want to matrix multiply (in the linear algebra sense) in a VBA function like so: t(vec)matvec to produce a 1x1 real number I can use in an equation.
I do not want to interact with a worksheet in the function. The values in the matrix and vector are eiter hard-coded or calculated from within the function. There should be a simple way to transpose then do a couple matrix multiplications like in MATLAB or R. Here is where I am so far:
Public Function QuickMaths()
Dim vec As Variant
Dim mat As Variant
mat = Array(Array(1,1+1,3), _
Array(2^2,5,6), _
Array(7,8,9))
vec = Array(2*5,11,12)
QuickMaths = Application.WorksheetFunction.MMult(Application.WorksheetFunction.MMult(Application.WorksheetFunction.Transpose(vec), mat), vec)
End Function
I get #VALUE from this in a worksheet when I run it. I would expect the output to be a 1x1 matrix, but I don't know if Excel VBA would consider that a scalar that can be output into a sheet as a single value (e.g. Double).
Please send help.
It would have been good if you had provided expected output (the specific scalar you're expecting at the end).
Based on what I gather from your code and question, I'm going to assume you're trying to perform two steps. The first being:
The second being:
(It's been a while since I've done any matrix multiplication, so if you think I've misunderstood, let me know.)
Your first array (mat) is an array of arrays (not a two dimensional array), which I don't think MMULT handles (https://support.office.com/en-us/article/mmult-function-40593ed7-a3cd-4b6b-b9a3-e4ad3c7245eb). So you might need to replace:
mat = Array(Array(1, 1 + 1, 3), _
Array(2 ^ 2, 5, 6), _
Array(7, 8, 9))
with:
ReDim mat(0 To 2, 0 To 2)
mat(0, 0) = 1
mat(0, 1) = 2
mat(0, 2) = 3
mat(1, 0) = 4
mat(1, 1) = 5
mat(1, 2) = 6
mat(2, 0) = 7
mat(2, 1) = 8
mat(2, 2) = 9
That said, manually assigning each array element can be impractical, so maybe make a small function to do it for you (see FlattenAnArrayOfArrays function in code below).
From what I've read online in the last 30 minutes, matrix multiplication is not commutative and also requires that the number of columns in your first matrix match the number of rows in the second matrix. (You may already know all of this, but just mentioning it anyway.)
Based on the above, your code might look something like:
Option Explicit
Public Function QuickMaths() As Variant
' This function returns a value of type Variant.
' Could return a Long/Double/numeric type; scalar should be at QuickMaths(1,1)
' But MMULT can return non-numeric values, so you risk
' getting a type mismatch error if the matrix multiplication
' is not successful (for whatever reason).
' Maybe this shouldn't be this function's concern -- or maybe it should.
Dim mat As Variant
mat = Array(Array(1, 1 + 1, 3), _
Array(2 ^ 2, 5, 6), _
Array(7, 8, 9))
mat = FlattenAnArrayOfArrays(mat)
Dim vec As Variant
vec = Array(2 * 5, 11, 12)
Dim resultantMatrix As Variant
resultantMatrix = Application.MMult(vec, mat) ' Number of columns in "vec" must match number of rows in "mat"
resultantMatrix = Application.MMult(vec, Application.Transpose(resultantMatrix))
QuickMaths = resultantMatrix
End Function
Private Function FlattenAnArrayOfArrays(ByRef arrayOfArrays As Variant) As Variant()
' Given an array of arrays, returns a two-dimensional array.
' This function is very basic and has no error handling implemented.
Dim firstArray() As Variant
firstArray = arrayOfArrays(LBound(arrayOfArrays)) ' Columns inferred from first array in "arrayOfArrays"
Dim outputArray() As Variant
ReDim outputArray(LBound(arrayOfArrays) To UBound(arrayOfArrays), LBound(firstArray) To UBound(firstArray))
Dim rowIndex As Long
For rowIndex = LBound(outputArray, 1) To UBound(outputArray, 1)
Dim columnIndex As Long
For columnIndex = LBound(outputArray, 2) To UBound(outputArray, 2)
outputArray(rowIndex, columnIndex) = arrayOfArrays(rowIndex)(columnIndex)
Next columnIndex
Next rowIndex
FlattenAnArrayOfArrays = outputArray
End Function
Closing points:
The return value of the QuickMaths function is a 1x1 matrix, but you can assign it to a cell's value.
Similarly, if you call the QuickMaths function from a worksheet cell, the cell will display the return value (without any issues or need for an array formula).

How do I instantiate a dynamic, empty array and add its first element?

I've been trying to instantiate an empty array, where I'll be adding elements. For some reason, my script is throwing an error on simply calling Ubound on the empty array. I can't figure out how to instantiate an empty array... Here's what I've got:
Dim data_dates
data_dates = Array("6/24/2019", "7/1/2019", "7/8/2019", "7/15/2019", "7/22/2019", "7/29/2019", "8/5/2019", "8/12/2019", "8/19/2019", "8/26/2019", "9/2/2019")
Dim site_dates
For date_iter = 1 To UBound(data_dates)
If start_date <= data_dates(date_iter) And last_date <= data_dates(date_iter) Then
MsgBox UBound(site_dates) '- LBound(site_dates) + 1
site_dates(UBound(site_dates) + 1) = data_dates(date_iter)
End If
Next date_iter
So that MsgBox line is throwing an error. Is it normal for Ubound to throw an error on an empty array? If so, how do I add the first element to an empty array?
Dim site_dates
This variable is an implicit Variant. While a Variant can very well hold an array, it initializes to Variant/Empty, which isn't an array - that's why UBound(site_dates) is throwing an error: you're trying to get the upper bound of a Variant/Empty, and VBA doesn't know what to do with that.
This declares a dynamic array of Variant items:
Dim site_dates()
That said, in general you should avoid resizing arrays (a loop with ReDim Preserve theArray(UBound(theArray) + 1) is copying the entire array at every iteration just to add a single item - the penalty gets more apparent with more items): if you don't know how many elements you're going to need, it's usually a better idea to use a Collection and Add items as you go. If you do know how many elements you're going to need, then explicitly size the array accordingly, at the declaration site:
Dim site_dates(1 To 10)
Note that Dim statements aren't executable, so you can't use a variable. Use the ReDim statement to do this:
ReDim site_dates(1 To datesCount)
ReDim acts as a declarative statement, so you don't need a prior Dim, even with Option Explicit specified.
In this case you can use Application.WorksheetFunction.CountIf to get the number of dates matching the criteria and size the array before you start iterating the values.
One way to start the growth process:
Sub InTheBeginning()
Dim site_dates() As Date, msg As String
ReDim site_dates(1)
For i = 1 To 10
ReDim site_dates(1 To UBound(site_dates) + 1)
Next i
msg = LBound(site_dates) & vbCrLf & UBound(site_dates)
MsgBox msg
End Sub
First to check that you are using Option base 1 because start your loops with 1??
If you have a setup where you know the max possible site_dates from your data_dates, you can ReDim the site_dates at the beginning (before the loop)to have the same bounds as data-dates.
Keep a track of the number of qualifying items written in a counter in the 'If logic' loop.
Then at the end, you can Redim Preserve down to the amount generated:
ReDim Preserve site_dates(1 to qualifyingCounter)
Alternatively, with no worry about performance you can consider the easiest for me:
Reference mscorlib and use an ArrayList
Dim data_dates
data_dates = Array("6/24/2019", "7/1/2019", "7/8/2019", "7/15/2019", "7/22/2019", "7/29/2019", "8/5/2019", "8/12/2019", "8/19/2019", "8/26/2019", "9/2/2019")
Dim siteDate_ArrayList As New ArrayList
Dim date_iter As Long
For date_iter = 0 To UBound(data_dates)
If date_iter Mod 2 = 0 Then 'I changed this logic just for my test
siteDate_ArrayList.Add data_dates(date_iter)
End If
Next date_iter
Dim site_dates As Variant
'Please note that array resultant from ToArray on an empty ArrayList will have a Ubound of -1
site_dates = siteDate_ArrayList.ToArray
EDIT:
To refernce, go to Tools --> References and look down alphabetically for mscorlib.dll. Then check it.

Can a logic test for an `if` statement be a variable in excel vba?

I have the following function:
Function get_equal_array_subset(column_label As String, _
loop_array() As Variant, _
values_array() As Variant)
' this function outputs an array of value from the values_array, based on a loop through the loop_array
' column_label is the first item in the array of the ouput array; i.e. the column lable of a new range
' loop_array is array being looped through and testing each value
' valus_array is the array from which values are taken with the test is met in the first array
' *** arrays have to be of equal lenght ***
Dim subset_array() As Variant
subset_array = Array(column_label)
Dim rows_dim As Long
Dim cols_dim As Integer
Dim agent_subset_counter As Long
agent_subset_counter = 0 ' counter to set the key for the new array
For rows_dim = 2 To UBound(loop_array, 1)
For cols_dim = 1 To UBound(loop_array, 2)
If loop_array(rows_dim, cols_dim) > 2 Then
agent_subset_counter = agent_subset_counter + 1 ' increase the subset counter by 1
ReDim Preserve subset_array(agent_subset_counter) ' resize the array account for the next id
subset_array(agent_subset_counter) = values_array(rows_dim, cols_dim) ' add the new id to the agent subset
End If
Next cols_dim
Next rows_dim
get_equal_array_subset = subset_array
End Function
Is there a way for me to make the If loop_array(rows_dim, cols_dim) > 2 Then a variable? Let's say I wanted the test to be > 3 or = 5 or non blank...etc.
I would go for the magic Application.Evaluate() method of the Application class. An example might be to define a series of tests into an array, let's say:
Dim myTests(4)
myTests(1) = "> 3"
myTests(2) = "= 5"
myTests(3) = "+3 < 5"
myTests(4) = "- 4 + sum(1,2) < 5"
Hence, using the simple statement:
If Application.Evaluate(loop_array(rows_dim, cols_dim) & myTests(j)) Then
Clearly, the variable j should be defined depending on the test you want to use and this kind of method would allow you to define several arrays of operators (one array for operators like +, - etc., another one for values like 3, 5 etc.)
NOTE If you don't know it yet, the Application.Evaluate() method will evaluate the expression and returning the result as Excel would do. It's basically using the same code that Excel uses to evaluate what you write in a cell:
Application.Evaluate("2+3") --> 5
Application.Evaluate("2 < 3") --> True
Application.Evaluate("IF(2=3,1,2)") --> 2
'etc.
If you wanted to make the "magic number" 2 into a variable, then you would use an array item in place of the 2.If, however, you wanted separate logic, then you use use a Select Case structure.

Building Variable Names with Concatenate

I have a function that takes optional arguments in pairs: firstRange_1, secondRange_2; firstRange_2, secondRange_2; etc.
For each optional argument I need to execute a series of statements if the argument is passed to the function.
For example
dim firstRange_1 as range
dim secondRange_1 as range
dim firstRange_2 as range
dim secondRange_2 as range
etc.
dim firstCell_1 as string
dim lastCell_1 as string
dim firstCell_2 as string
dim lastCell_2 as string
etc.
If IsMissing(firstRange_1) = False Then
firstCell_1 = secondRange_1.Cells(1,1).Address
lastCell_1 = secondRange_1.Cells(secondRange_1.Rows.Count, secondRange_1.Rows.Count)
End if
if IsMissing(firstRange_2) = False Then
firstCell_2 = secondRange_2.Cells(1,1).Address
lastCell_2 = secondRange_2.Cells(secondRange_2.Rows.Count, secondRange_2.Rows.Count)
End If
Is it possible to "build" (sorry if the terminology isn't correct, I'm not yet experienced in programming or vba) the variables on the fly?
for example a loop like
For n=1 to 100
If IsMissing(firstRange_ & "n") = False Then
firstCell_ & "n" = secondRange_ & "n".Cells(1,1).Address
lastCell_ & "n" = secondRange_ & "n".Cells(secondRange_ & "n".Rows.Count, secondRange_ & "n".Rows.Count)
End If
Next
Edit:
See my comments to Branislav Kollár for updates.
I think what you need to rewrite the function to use ParamArrays (see the "Using an Indefinite Number of Arguments" section). Something like this:
myFunction(ParamArray userRanges()) As Range'or whatever Data Types you need
This way, you could use the LBound and UBound functions to see how many arguments were passed into function, leaving the necessity to check if they are missing.
For example you can create a 2 new arrays inside the function (not the argument array) for determining the first and last cells of each argument range. This is not the only way, you can use 2D arrays or put everything into one array. This is just one way.
Function myFunction(ParamArray userRanges()) As Range
Dim firstCell() As Range
Dim lastCell() As Range
ReDim firstCell(UBound(userRanges))
ReDim lastCell(UBound(userRanges))
For x = 0 To UBound(userRanges)
Set firstCell(x) = userRanges(x).Range("A1")
Set lastCell(x) = firstCell_1(x).Offset(userRanges(x).Rows.Count - 1, userRanges(x).Columns.Count - 1)
Next x
'other code to actually do something with the cells
'...
End Function
Try this, if you have any trouble, please let us know.
One more link to learn about this Understanding the ParamArray
Edit 1
Based on comment from OP, I rewritten the code, so that now each input range userRanges will have firstCell and lastCell stored in appropriate arrays. I didn't realize the limitation of my previous post before.
The only think to keed in mind now, is that the index 0 is first range; 1 is second range; 2 is third range; etc.
Or you can use Option Base 1 to make it more naturally indexed, but that is not recommended for some reason.
You can't dynamically name variables, but you can use Arrays. They are stupid powerful, so it's worth learning about them.
Essentially you will make 2 arrays. One for your input (variable1_n) and one of your outputs (output_1_n).
Dim inputArray(1 to 100) as String 'or whatever type these are supposed to be
Dim outputArray(1 to 100) as Range 'perhaps these are ranges?
For i = 1 to 100
Set outputArray(i) = function(inputArray(i))
Next i
Now you have an array full of ranges!

Resources