Dynamic arrays and VBA user defined functions (UDFs)

Office 365 subscribers now have Excel’s new calculation engine available to them, meaning lots of users suddenly have access to dynamic arrays.  This got me thinking about whether VBA user-defined functions (UDFs) can make use of the new functionality too.

I’m not an expert in this; I’m just sharing as much as I’ve learned to date.  If you’ve got a better or different approach, then please share it in the comments at the bottom.

If you’re not familiar with dynamic arrays, I recommend reading this post: Dynamic arrays in Excel.

Download the example file: Join the free Insiders Program and gain access to the example file used for this post.

File name: 0018 Dynamic array UDF.zip

How do dynamic array calculations work?

To understand dynamic arrays, we first must understand how functions operate.  Each argument in a function accepts different data types (text, numbers, ranges, etc.).  If we put text where there should be a number, or a range where there should be text, we will encounter the #VALUE! error.

The data types required for each argument are visible in the Formula Arguments window.

Function Arguments Window

The screenshot above shows the INDEX function has 3 arguments, each of which has a required data type:

  • Array: Reference – can be a range or an array constant.
  • Row_num and Column_num: Number – can be a cell containing a number or a single number constant.

Many functions, just like INDEX, contain arguments that expect a single value (this value is known as a scalar).  If we provide multiple values in that argument (such as range or array constant), Excel performs a calculation for every item in that array/range (this is known as lifting).  Excel has always operated in the way, but we only ever saw multiple results if we knew how to use Ctrl+Shift+Enter.  Instead, dynamic arrays display all the calculated results by spilling them into cells across and below.

Let’s look at two examples.

Example 1 – Non-dynamic array

The standard INDEX function calculates a single value, as shown by cell G4 below.  Notice that Row_num and Column_num arguments are individual values.

Standard INDEX Function

Example 2 – Dynamic array

Now let’s give the Row_num and Column_num multiple values as ranges.

Dynamic array INDEX Function

Look at the screenshot above.  Cell G6 contains the formula, but since the Row_num and Column_num are both 2 values, the calculated results are pushed into the cells across and below.

Using UDFs

Let’s try a user-defined function to see what happens.

The UDF used for the example calculates the volume of a sphere.  It only requires a single input, which is the radius.

Function VolumeOfSphere(Radius As Double)

VolumeOfSphere = (4 / 3) * Application.WorksheetFunction.Pi() * Radius ^ 3

End Function

When using a single value, the UDF operates correctly.  Look at the screenshot below, cell D2 calculates the volume using the radius in cell A2.

Non Dynamic Function - with scalar

Now let’s apply the dynamic array principles, and select multiple cells… oh dear, it doesn’t work.  The UDF has been given a range as an input (cells A2:A4), but it calculates as a #VALUE! error (cell D4).

Non Dynamic - Multiple Scalars

OK, now let’s see if we can make this work.

Create a dynamic array UDF

For a UDF to spill results into other cells, it needs to output an array.  Also, to accept multiple scalars in an argument, it needs to allow input arrays.  The size of the range/array is determined by the user through the arguments, therefore the easiest option for this is to use the Variant data type.

A reasonable understanding of VBA arrays is required to change the UDF into a dynamic array compatible function.  Check out my post about VBA arrays for further reading.

Here is the updated UDF.  As the Radius argument now allows multiple scalars, the function must be able to perform a calculation for each item in the range or array.

Function VolumeOfSphereDA(Radius As Variant) As Variant

Dim i As Long
Dim j As Long
Dim test2DArray As Long
Dim tempArray() As Variant
Dim resultArray As Variant


'Resize Radius if 1D array
If TypeName(Radius) = "Variant()" Then
    On Error Resume Next
    test2DArray = UBound(Radius, 2)
    On Error GoTo 0
    If test2DArray = 0 Then
        ReDim temp(1 To 1, LBound(Radius) To UBound(Radius))
        For j = LBound(temp, 2) To UBound(temp, 2)
            temp(1, j) = Radius(j)
        Next j
    Radius = temp
    End If
End If


'Force the resultArray to be the same size as the input array
resultArray = Radius


'Calculate the result
For i = LBound(resultArray) To UBound(resultArray)
    For j = LBound(resultArray, 2) To UBound(resultArray, 2)
            resultArray(i, j) = (4 / 3) * Application.WorksheetFunction.Pi() _
                * Radius(i, j) ^ 3
    Next j
Next i

VolumeOfSphereDA = resultArray

End Function

When multiple scalars are provided to the UDF above, the values spill into the cells across and below.  It behaves just like dynamic arrays should.  Look at the screenshot below, the formula in cell D6 accepts a range as input and spills the result into D7 and D8.

Dynamic with multiple scalars

The UDF will also work with array constants.

NOTES:

All ranges are automatically treated as having two-dimensions, even if they are just a single row.  By contrast, single row array constants, such as {1,2,3}, as always treated as having one-dimension.

As a result, the UDF converts one-dimension arrays to two-dimension arrays to prevent errors in the calculation process.

Thank you to two regular readers; Aryeh Nielsen and David Newell for helping me with this.

Working with many multiple scalars

What happens when we have more than one argument which accepts scalars?  Native Excel uses a concept known as pairwise lifting.  This is where the rows and columns are matched by their relative position.  The first cells in each range are used to calculate the first result.  The second cells in each range are used to calculate the second result, etc, etc.

If one of the arguments contains a single scalar, Excel resizes the scalar to be an array with a consistent value for each element; this is known as broadcasting.  Once broadcasting has been completed the arrays are the same size, so pairwise lifting can continue as normal.

UDF’s don’t inherently use broadcasting, or pairwise lifting, we have to explicitly code in a way which creates the equivalent result.  This makes things significantly more complex.  To understand this, let’s change our UDF to calculate the volume of a cylinder.

The UDF takes two arguments both of which are scalars.

  • Radius: The radius of the cylinder
  • Length: The length of the cylinder
Function VolumeOfCylinderDA(Radius As Variant, Length As Variant) As Variant

Dim i As Long
Dim j As Long
Dim test2DArray As Long
Dim temp As Variant
Dim resultArray As Variant
Dim resultArrayRows As Long
Dim resultArrayColumns As Long
Dim radiusArray As Variant
Dim lengthArray As Variant


'Resize Radius if 1D array
If TypeName(Radius) = "Variant()" Then
    On Error Resume Next
        test2DArray = UBound(Radius, 2)
    On Error GoTo 0
    If test2DArray = 0 Then
        ReDim temp(1 To 1, LBound(Radius) To UBound(Radius))
        For j = LBound(temp, 2) To UBound(temp, 2)
            temp(1, j) = Radius(j)
        Next j
    Radius = temp
    End If
End If


'Resize Length if 1D array
If TypeName(Length) = "Variant()" Then
    On Error Resume Next
    test2DArray = UBound(Length, 2)
    On Error GoTo 0
    If test2DArray = 0 Then
        ReDim temp(1 To 1, LBound(Length) To UBound(Length))
        For j = LBound(temp, 2) To UBound(temp, 2)
            temp(1, j) = Length(j)
        Next j
    Length = temp
    End If
End If


'Convert ranges to arrays or retain if not range
radiusArray = Radius
lengthArray = Length


'Get dimensions of biggest array/range, ignore if not array/range
On Error Resume Next

If TypeName(radiusArray) = "Variant()" Then
    If UBound(radiusArray) > resultArrayRows Then _
        resultArrayRows = UBound(radiusArray)
    If UBound(radiusArray, 2) > resultArrayColumns Then _
        resultArrayColumns = UBound(radiusArray, 2)
End If

If TypeName(lengthArray) = "Variant()" Then
    If UBound(lengthArray) > resultArrayRows Then _
        resultArrayRows = UBound(lengthArray)
    If UBound(lengthArray, 2) > resultArrayColumns Then _
        resultArrayColumns = UBound(lengthArray, 2)
End If

If resultArrayRows = 0 Then resultArrayRows = 1
If resultArrayColumns = 0 Then resultArrayColumns = 1

On Error GoTo 0


'Force the resultArray to be dimensions of biggest arrays
ReDim resultArray(1 To resultArrayRows, 1 To resultArrayColumns)


'Broadcast for radiusArray
If TypeName(radiusArray) <> "Variant()" Then
    ReDim temp(1 To resultArrayRows, 1 To resultArrayColumns)
    For i = LBound(temp) To UBound(temp)
        For j = LBound(temp, 2) To UBound(temp, 2)
            temp(i, j) = radiusArray
        Next j
    Next i
    radiusArray = temp
End If


'Broadcast for lengthArray
If TypeName(lengthArray) <> "Variant()" Then
    ReDim temp(1 To resultArrayRows, 1 To resultArrayColumns)
    For i = LBound(temp) To UBound(temp)
        For j = LBound(temp, 2) To UBound(temp, 2)
            temp(i, j) = lengthArray
        Next j
    Next i
    lengthArray = temp
End If


'Calculate the result
For i = LBound(resultArray) To UBound(resultArray)
    For j = LBound(resultArray, 2) To UBound(resultArray, 2)
            resultArray(i, j) = Application.WorksheetFunction.Pi() _
                * radiusArray(i, j) ^ 3 * lengthArray(i, j)
    Next j
Next i

VolumeOfCylinderDA = resultArray

End Function

The UDF has the following steps:

  • One-dimension Radius or Length arrays are resized to two-dimension arrays.
  • Radius and Length ranges are converted to arrays – this helps to simplify future calculations.
  • Create an array to hold the result, the size is based on the maximum rows/columns of Radius and Length.
  • Where Radius or Length is a single scalar, create a broadcasting effect by resizing to a two-dimension array.
  • To calculate the result, the For Each loop matches values to create a pairwise lifting effect.

In the screenshot below, our UDF has applied logic that mimics pairwise lifting.  The first cell uses 1 and 10 as inputs, the second cell uses 2 and 20 as inputs, and so on.

Dynamic array UDF Volume of Cylinder

Conclusion

Well, that’s all I have.  Hopefully, this will help you to turn your UDFs into dynamic array UDFs.

If you’ve got any further ideas about this I’d love to hear them.


Discover how you can automate your work with our Excel courses and tools.

Excel Academy

Excel Academy
The complete program for saving time by automating Excel.

Excel Automation Secrets

Excel Automation Secrets
Discover the 7-step framework for automating Excel.

Office Scripts Course

Office Scripts: Automate Excel Everywhere
Start using Office Scripts and Power Automate to automate Excel in new ways.

7 thoughts on “Dynamic arrays and VBA user defined functions (UDFs)”

  1. There are four input possibilities, all of which must be converted to 2d Variant Arrays:
    Column Range, Row Range, Constant Column Array {x;y;z}, Constant Row Array {x,y,z}

    The below works for all forms of inputs. It determines which form the input is in, and converts them all to 2d Variant arrays. Perform at head of function, and use arrRadius instead of Radius elsewhere in function.

    If TypeName(Radius) = “Range” Then ‘ If Range
    If Radius.Rows.Count = 1 Then ‘ 1D Array as Row Range
    arrRadius = WorksheetFunction.Transpose(Radius.Value)
    Else
    arrRadius = Radius.Value ‘ 2D Array
    End If
    ElseIf TypeName(Radius) = “Variant()” Then ‘ If Array

    ‘ Test for columns
    On Error Resume Next
    test = UBound(Radius, 2)
    On Error GoTo 0

    If test = 0 Then ‘ Row Array
    arrRadius = WorksheetFunction.Transpose(Radius)
    Else ‘ Column Array
    arrRadius = Radius
    End If
    End If

    Reply
    • Hi Aryeh,

      Thank you for commenting, and point me in the right direction. Why do I find arrays in VBA so hard? Thanks for the code, I appreciate you taking the time to do this. I will review and add into the post.

      Reply
  2. VBA treats single row arrays is being just one dimension, while single column arrays are seen as two dimensions. I think it has something to do with (or is at least similar to) how the Array function in VBA works.

    For example…
    Array(1,2,3) returns a one-dimensional array
    Application.Transpose(Array(1,2,3)) returns a two-dimensional array

    This is somewhat counterintuitive since a one row range and a one column range are both seen as two-dimensional in VBA. So in this case, your code is always expecting a 2-D array, and the #VALUE! error is because it cannot get the upper and lower bounds of the second dimension for a one row array constant.

    Reply
    • D’oh! Of course!!!

      Thank you so much for your reply. In my mind I had a load of complex scenarios, none of which worked. It turned out to be something I should have known. Arrays catch me out once again…. won’t be the first, won’t be the last 🙂

      Reply
  3. Here is how I would write your VolumeOfSphere UDF so that it worked with ranges or arrays as the input and outputted a dynamic array spilling result (I deliberately omitted error checking to keep the code simple to digest). As written (see the ReDim statement), this function will always output a vertical range of values even if the input range is horizontal. An improvement would be to check the number of columns and rows in the input to the function and adjust the two arguments in the ReDim statement accordingly. Oh, and also notice how I handled retrieving the constant value for PI.

    Function VolumeOfSphere(Radius As Variant) As Variant
    Dim X As Long, V As Variant
    ReDim R(1 To Application.CountA(Radius), 1 To 1)
    For Each V In Radius
    X = X + 1
    R(X, 1) = [4*Pi()/3] * V ^ 3
    Next
    VolumeOfSphere = R
    End Function

    Reply

Leave a Comment