ThreeWave Writing Your Own Functions In VBA

This page describes how to write your own worksheet functions in VBA.
ShortFadeBar


While Excel provides a plethora of built-in functions, especially so if you include functions in the Analysis Took Pack (in Excel 2007, the functions that used to be in the ATP are now native Excel functions) you may find it useful to create your own custom function for things that Excel cannot (easily) do with the built-in functions. While it takes longer for Excel to calculate a VBA function than it does to calculate a worksheet formula, all else being equal, the flexibility of VBA often makes a VBA function the better choice. The rest of this page assumes that you are familiar with the basics of VBA programming.

Simple User Defined Functions

A User Defined Function (or UDF) is a Function procedure that typically (but not necessarily) accepts some inputs and returns a result. A UDF can only return a value to the cell(s) whence it was called -- it must not modify the contents or formatting of any cell and must not modify the operating environment of Excel. If you attempt to change anything, the function will terminate immediately and return a #VALUE error to the calling cell. In Excel 97 and 2000, a UDF cannot use the Find method of a Range object, even though that method does not change anything in Excel. This was fixed with Excel 2002.
The following is an example of a simple UDF that calculates the area of a rectangle:

Function RectangleArea(Height As Double, Width As Double) As Double
    RectangleArea = Height * Width
End Function

This function takes as inputs two Double type variables, Height and Width, and returns a Double as its result. Once you have defined the UDF in a code module, you can call it from a worksheet cell with a formula like:

=RectangleArea(A1,B1)

where A1 and B1 contain the Height and Width of the rectangle.

Because functions take inputs and return a value, they are not displayed in the list of procedures in the Macros dialog.

SectionBreak

Where To Put The Code

The code for a UDF should be placed in a standard code module, not one of the Sheet modules and not in the ThisWorkbook module. In the VBA editor, go to the Insert menu and choose Module. This will insert a new code module into the project. A module can contain any number functions, so you can put many functions into a single code module. You can change the name of a module from Module1 to something more meaningful by pressing the F4 key to display the Properties window and changing the Name property to whatever you want.

You can call a function from the same workbook by using just the function name. For example:

=RectangleArea(12,34)

It is possible, but strongly recommended against, to have two functions with the same name is two separate code modules within the same workbook. You would call them using the module name from cells with formulas like:

=Module1.MyFunction(123)
=Module2.MyFunction(123)


Doing this will lead only to confusion, so just because it is possible doesn't mean you should do it. Don't do it.

Do not give the same name to both a module and a function (regardless of whether that module contains that function). Doing so will cause an untrappable error.

You can call a UDF that is contained in another (open) workbook by using the workbook name in the formula. For example,

='MyBook.xls'!RectangleArea(A1,A2)

will call the function RectangleArea defined in the workbook MyBook.xls. If a function is defined in an Add-In (either an XLA or an Automation Add-In; see this page for information about writing Automation Add-Ins in VB6), you don't need to include the name of the Add-In file. The function name alone is sufficient for calling a function in an Add-In.

CAUTION: Excel does not handle well the case when a workbook contains a function with the same name as a function in an Add-In. Suppose both Book1.xls and MyAddIn.xla have a function named Test defined as:

Function Test() As String
    Test = ThisWorkbook.Name
End Function

The function Test in each workbook simply returns the name of the workbook in which the code resides, so the function Test defined in Book1.xls returns the string "Book1.xls" and the function Test defined in MyAddIn.xla returns the string "MyAddIn.xla". In Book1.xls, enter the formula =Test() in cell A1 and enter the formula =MyAddin.xla!Test() in cell A2. The functions will work properly when you first enter the formulas, but if you edit the formula in A2 (e.g., select the cell, then press the F2 key followed by the ENTER key), the name Test is recognized as a function in Book1.xls so Excel will change the function call in cell A2 from =MyAddIn.xla!Test() to simply =Test(), and this will call the function Test from Book1.xls not MyAddIn.xla. This will almost certainly return an incorrect result. This problem occurs only when the workbook and an Add-In both have a function with the same name. It does not occurs if two workbooks have functions with the same name. This has not been fixed in Excel 2007.

SectionBreak

UDFs And Calcuations

As a general rule, you should pass into the function all the values it needs to properly calculate the result. That means that your UDF should not make explicit refences to other cells. If you reference other cells directly from within the function, Excel may not recalculate the function when that cell is changed. For example, a poorly written UDF is as follows:

Public Function BadRectangleArea(Height As Double) As Double
    BadRectangleArea = Height * Range("A1").Value
End Function

In this function, the Width is assumed to be in cell A1. The problem here is that Excel doesn't know that this function depends on cell A1 and therefore will not recalculate the formula when A1 is changed. Thus, the cell calling the function call will not contain the correct result when cell A1 is changed. You can force Excel to recalculate a UDF whenever any calculation is made by adding the line

Application.Volatile True

as the first line in the function. For example,

Function BadRectangleArea(Height As Double) As Double
    Application.Volatile True
    BadRectangleArea = Height * Range("A1").Value
End Function

This has the drawback, however, that the function is recalculated even if it doesn't need to be recalculated, which can cause a performance problem. In general, you shouldn't use Application.Volatile but instead design your UDF to accept as inputs everything it needs to properly caclulate the result.

SectionBreak

Returning Arrays From Functions

See the Returning Arrays From User Defined Functions page for information about returning arrays as the result of your User Defined Function.

SectionBreak

Returning Errors From Functions

You can return an error value from a UDF if an incorrect input parameter is passed in. To do this, the function must return a Variant data type and use the CVErr function to create an error-type Variant result. For example, the function Divide below will return a #DIV/0 error if the divisor is 0.

Function Divide(A As Double, B As Double) As Variant
    If B = 0 Then
        Divide = CVErr(xlErrDiv0)
    Else
        Divide = A / B
    End If
End Function

You can use any of the following error constants with the CVErr function to return an error to Excel:

  • xlErrDiv0 for a #DIV/0 error
  • xlErrNA for a #N/A error
  • xlErrName for a #NAME? error
  • xlErrNull for a #NULL error
  • xlErrNum for a #NUM error
  • xlErrRef for a #REF error
  • xlErrValue for a #VALUE error
If any other value is passed to CVErr, Excel will treat it as a #VALUE error. It is generally good practice to validate the input parameters and return an error value with CVErr rather than letting the VBA code error out with #VALUE errors. If a run-time error occurs in your code, or you attempt to change anything in Excel, such other cells, VBA terminates the function and returns a #VALUE error to Excel.

SectionBreak

Determining The Range From Which Your UDF Was Called

Under nearly all circumstances, it is not necessary to know the actual address of the range from which your UDF was called. Indeed, you should avoid have the need for such information. Your function should work the same regardless of where it was called from. However, you may well need to know the size of the range from which your UDF was called if it was array entered into a range of cells. The Application.Caller object will return a reference to the range from which your function was called, regardless of whether that range is a single cell or a range of cells.

CAUTION: Application.Caller will be a Range object only when the function in which it appears was called from a worksheet cell. If the function was called from another VB procedure, Application.Caller will be an Error-type Variant and most any attempt to use it will result in a Type Mismatch (13) error. If the code containing Application.Caller was called via the OnAction property of a Shape object on a worksheet, Application.Caller will be a String containing the name of the sheet. Therefore, if your function might be called from another VB procedure rather than only from a worksheet cell, you should test Application.Caller with the IsObject function to ensure that it is indeed an object before attempting to access any of its properties.

CAUTION: In Excel 2003, a new object, Application.ThisCell, was introduced. It is similar in nature to Application.Caller, but differs when a UDF is array entered into a range of more than one cell. Application.Caller will return the a Range reference to the entire range in which the UDF was array-entered. Application.ThisCell returns a reference to the first (upper left) cell in the range from which the UDF was called. Frankly, I'm not sure why Application.ThisCell was introduced in the first place.

You can get the properties of Application.Caller with code like the following:

    Function Test()
        Dim CallerRows As Long
        Dim CallerCols As Long
        Dim CallerAddr As String
        With Application.Caller
            CallerRows = .Rows.Count
            CallerCols = .Columns.Count
            CallerAddr = .Address
        End With
        Test = 1234
    End Function

SectionBreak

Using A Variable Number Of Parameters

You can define a function to accept a variable number of parameters in one of two somewhat different ways. You can use a specified number of optional parameters, or you can allow the function to accept any number of parameters, including none at all, using a ParamArray Variant parameter. The two methods are mutually exclusive. You cannot use both optional parameters and a ParamArray in the same function.

Optional Variant Parameters

You can define one or more parameters as Optional Variant types. For example:

Function OptParam(D As Double, Optional B As Variant) As Variant
    If IsMissing(B) = True Then 
        OptParam = D
    Else
        If IsNumeric(B) = True Then 
            OptParam = D + B
        Else
            OptParam = CVErr(xlErrNum)
        End If
    End If
End Function

This function defines the parameter B as an optional Variant and uses the IsMissing function to determine whether the parameter was passed. The IsMissing function can be used only with Variant type parameters. If IsMissing is used with any other data type (e.g., a Long), it will return False. More than one parameter may be Optional, but those parameters must be the last parameters accepted by the function. That is, once one parameter is specified as Optional, all the parameters that follow it must also be optional. You cannot have a required parameter following an optional parameter. If a parameter is declared as Optional but is not a Variant (e.g, it is a String or a Long) and that parameter is omitted, the IsMissing function will return False and the default value for that data type (0 or empty string) will be used. You can specify a default value for an optional parameter that should be used if the parameter is omitted. For example, the parameter B in the function below is optional with a default value of 2.

Function FF(A As Long, Optional B As Long = 2) As Variant
    If B = 0 Then
        FF = CVErr(xlErrDiv0)
    Else
        FF = A / B
    End If
End Function
In this code, the value 2 is used for the default value of B if B is omitted. When using a default value for a parameter, you don't call the IsMissing function. Your code should be written to use either a passed in parameter value or the default value of the parameter. With the code above, the following two worksheet functions are equivalent:

    =FF(1,2)
    =FF(1)

Variant ParamArray

The second method for working with optional parameters is to use a ParamArray Variant parameter. A ParamArray allows any number of parameters, including none at all, to be passed to the function. You can have one or more required parameters before the ParamArray, but you cannot have any optional parameters if you have a ParamArray. Moreover, the ParamArray variable must be the last parameter declared for a function. The ParamArray variables must be Variant types. You cannot have a ParamArray of other types, such as Long integers. If necessary, you should validate the values passed in the ParamArray, such as to ensure they are all numeric. If your function requires one or more inputs followed by a variable number of parameters, declare the required parameters explicitly and use a ParamArray only for the optional parameters. For example, the function SumOf below accepts any number of inputs and simply adds them up:

Function SumOf(ParamArray Nums() As Variant) As Variant
''''''''''''''''''''''''''''''''''
' Add up the numbers in Nums
''''''''''''''''''''''''''''''''''
    Dim N As Long
    Dim D As Double
    For N = LBound(Nums) To UBound(Nums)
        If IsNumeric(Nums(N)) = True Then
            D = D + Nums(N)
        Else
            SumOf = CVErr(xlErrNum)
            Exit Function
        End If
    Next N
    SumOf = D
End Function
In your function code, you can use:

Dim NumParams As Long
NumParams = UBound(Nums) - LBound(Nums) + 1

to determine how many parameters were passed in the ParamArray variable Nums. This will be 0 if no parameters were passed as the ParamArray. Of course, the code above counts the number of parameters within the ParamArray, not the total number of parameters to the function.

See Optional Paramateres To Procedures for a more in depth discussion of Optional parameters and ParamArray parameter type.

SectionBreak

Returning Arrays From Functions

Your function can return an array of values so that it can be entered as an array formula, either entered into an array of cells or to return an array to be aggregated by a function like SUM. (See this page for a discussion of Array Formulas.) The NumsUpTo function below returns an array of the integers from 1 to the input parameter L. For simplicity, L must be between 1 and 5. The function also requires that if the function is array entered, it must be in either a single row or a single column. A range with more than one row and more than one column will result in a #REF error. This restriction applies to this example only; it is not a limitation on UDF array functions in general. See the next section for example code that return values to a two dimensional range of cells.

In a UDF, Application.Caller returns a Range type object that references the cell(s) from which the formula was called. Using this, we can test whether we need a row array or a column array. If the function is called from a column of cells (e.g., array entered into A1:A5), the VBA array must be transposed before returning it to Excel. Note that there is also an object named Application.ThisCell that references the cell from which a function is called. In functions called from a single cell, Application.Caller and Application.ThisCell work the same. However, they differ when a function is called as an array formula. You should use Application.Caller, not Application.ThisCell.


Function NumsUpTo(L As Long) As Variant
''''''''''''''''''''''''''''''''''''
' Add up the integers from 1 To L.
''''''''''''''''''''''''''''''''''''

Dim V() As Long
Dim ArraySize As Long
Dim N As Long
Dim ResultAsColumn As Boolean
''''''''''''''''''''''''''''''''''''
' Allow inputs only between 0 and 5.
''''''''''''''''''''''''''''''''''''
If (L > 5) Or (L < 1) Then
    NumsUpTo = CVErr(xlErrValue)
    Exit Function
End If

''''''''''''''''''''''''''''''''''''
' Allow only one columns or one row.
''''''''''''''''''''''''''''''''''''
If Application.Caller.Rows.Count > 1 And _
    Application.Caller.Columns.Count > 1 Then
        NumsUpTo = CVErr(xlErrRef)
        Exit Function
End If

'''''''''''''''''''''''''''''''''''''''
' Test whether the result should be
' returned as a columns or row array.
'''''''''''''''''''''''''''''''''''''''
If Application.Caller.Rows.Count > 1 Then
    ResultAsColumn = True
Else
    ResultAsColumn = False
End If

''''''''''''''''''''''''''''''
' ReDim the array to hold L elements.
''''''''''''''''''''''''''''''
ReDim V(1 To L)

''''''''''''''''''''''''''''''
' Fill up the array
''''''''''''''''''''''''''''''
For N = 1 To UBound(V)
    V(N) = N
Next N
'''''''''''''''''''''''''''''''''
' Return the result, transposing
' if necessary.
'''''''''''''''''''''''''''''''''
If ResultAsColumn = True Then
    NumsUpTo = Application.Transpose(V)
Else
    NumsUpTo = V
End If

End Function

If the SumUpTo function is called from a range that has more than one row, the array must be transposed before it is returned, using the Application.Transpose function. The result of the function is an array of L integers from 1 to L. If the range from which the function is called has N cells, and N is less than L (the size of the result array), elements at the end of array are discarded and only the firt N elements are sent to the cells. If L is less than N (the function is entered into an array of cells larger than L), #N/A errors fill out the ending elements of the range on the worksheet. Since the result of NumsUpTo is an array, it can be used in an array formula, such as

=SUM(NumsUpTo(5))

which returns 15, the sum of the numbers from 1 to 5.

SectionBreak

Returning Arrays With Two Dimensions

To return an array to a range that contains more than one row and more than one column, create a two dimensional array with the first dimension equal to the number of rows in the range and the second dimension equal to the number of columns in the range. Then load that array, looping through the rows and columns and then return the array as the result.

The function AcrossThenDown below loads the calling cells with sequential integers, moving across each row and then moving down to the next row. The function DownThenAcross below loads the calling cells with sequential integers, moving down each column then moving right to the next column. The difference between the two function is in the For loops, whether the outer loop is for Rows or Columns. As noted before, use Application.Caller not Application.ThisCell to get a reference to the range of cells calling the function.

Function AcrossThenDown() As Variant
    Dim NumCols As Long
    Dim NumRows As Long
    Dim RowNdx As Long
    Dim ColNdx As Long
    Dim Result() As Variant
    Dim N As Long
    ''''''''''''''''''''''''''''''''''''''''''''
    ' Get the number of rows and columns in the 
    ' range that is calling this function.
    ''''''''''''''''''''''''''''''''''''''''''''
    NumCols = Application.Caller.Columns.Count
    NumRows = Application.Caller.Rows.Count

    ''''''''''''''''''''''''''''''''''''''''''''
    ' ReDim the Result array to the number 
    ' of rows and columns in the calling range.
    ''''''''''''''''''''''''''''''''''''''''''''
    ReDim Result(1 To NumRows, 1 To NumCols)
    
    For RowNdx = 1 To NumRows
        For ColNdx = 1 To NumCols
            N = N + 1
            Result(RowNdx, ColNdx) = N
        Next ColNdx
    Next RowNdx
    AcrossThenDown = Result
End Function


Function DownThenAcross() As Variant
    Dim NumCols As Long
    Dim NumRows As Long
    Dim RowNdx As Long
    Dim ColNdx As Long
    Dim Result() As Variant
    Dim N As Long
    
    ''''''''''''''''''''''''''''''''''''''''''''
    ' Get the number of rows and columns in the 
    ' range that is calling this function.
    ''''''''''''''''''''''''''''''''''''''''''''
    NumCols = Application.Caller.Columns.Count
    NumRows = Application.Caller.Rows.Count

    ''''''''''''''''''''''''''''''''''''''''''''
    ' ReDim the Result array to the number 
    ' of rows and columns in the calling range.
    ''''''''''''''''''''''''''''''''''''''''''''
    ReDim Result(1 To NumRows, 1 To NumCols)
    
    For ColNdx = 1 To NumCols
        For RowNdx = 1 To NumRows
            N = N + 1
            Result(RowNdx, ColNdx) = N
        Next RowNdx
    Next ColNdx
    DownThenAcross = Result
End Function

This page last updated: 1-Sept-2007

-->