Search code examples
arraysvbalibreoffice-calc

VBA UDF for LO Calc that takes several arrays as arguments or produces array


I have trouble finding easy examples how to write simple UDF's (user defined functions) for LO Calc, that uses arrays as arguments or gives arrays as output e.g. array functions.

I need simple example UDF that takes array and gives single number as output, like count() function.

I need simple example where UDF that is array function, that takes two arrays and produces array that is Cartesian product of two arrays.

It would be nice if to have a comment for every step.


Solution

  • The simplest UDF for the first case looks like this:

    Function MyCount1(aSourceData As Variant) As Long 
    Dim i As Long, j As Long    ' Loop variables for two dimensions of an array '
    Dim iResult As Long     ' Temporary variable for counting non-empty values (counter) '
        iResult = 0         ' Init counter '
        For i = LBound(aSourceData,1) To UBound(aSourceData,1)      ' Loop row by row (first dimension) '
            For j = LBound(aSourceData,2) To UBound(aSourceData,2)  ' Loop cell by cell in current row (second dimension) '
                If Not IsEmpty(aSourceData(i,j)) Then iResult = iResult + 1 ' If the cell contains any value (not empty) - increase the counter '
            Next j
        Next i
        MyCount1 = iResult  ' Set counter as result of UDF '
    End Function
    

    This will work correctly if you pass a range of cells or an array of values to the function as a parameter (=MYCOUNT1(A2:C10) or =MYCOUNT1({1;2;3;4;5;;7;8}))

    However, if you leave the parameter list empty (=MYCOUNT1()), the program will stop with an error

    Empty Param Err

    If you specify not a range, but only one cell as a parameter(=MYCOUNT1(B5)), then the program will stop with another error

    Not Array Err

    To avoid such errors, you can use this technique

    Function MyCount2(Optional aSourceData As Variant) As Variant
    Rem Here param aSourceData is optional, can be skipped
    Dim i As Long, j As Long, iResult As Long   ' Same as previous
        On Error GoTo wrongData ' On any error jump to the end of function
        iResult = 0
        For i = LBound(aSourceData,1) To UBound(aSourceData,1)
            For j = LBound(aSourceData,2) To UBound(aSourceData,2)
                If Not IsEmpty(aSourceData(i,j)) Then   ' Skip empty cells `
                    If IsNumeric(aSourceData(i,j)) Then iResult = iResult + 1   ' Count numeric values only (skip texts) '
                EndIf 
            Next j
        Next i
        MyCount2 = iResult
        Exit Function 
    wrongData:
        MyCount2 = "Wrong param"
    End Function
    

    For the function to return either a counter value (number) or message text (string), the function type must be specified As Variant.

    This solution is also not good enough. For example, a user may want to process arrays with more than two dimensions, but the program cannot do this. To cover all use cases, the program will have to be greatly complicated.

    To return the result of the UDF as an array, form it inside the function in any convenient way and return the result As Variant. Keep in mind that the one-dimensional array will be a string - its values will appear in the table cells from left to right:

    Function MyCartesian(Optional aSourceA As Variant, Optional aSourceB As Variant) As Variant
    Dim iA As Long, jA As Long  ' Loop by first array
    Dim iB As Long, jB As Long  ' Loop by second array
    Dim aResult As Variant, iLen As Long    ' Result array and count rows (height of array)
        On Error GoTo wrongData ' On any error jump to the end of function
        iLen = (UBound(aSourceA,1)-LBound(aSourceA,1)+1) * _
            (UBound(aSourceA,2)-LBound(aSourceA,2)+1) * _
            (UBound(aSourceB,1)-LBound(aSourceB,1)+1) * _
            (UBound(aSourceB,2)-LBound(aSourceB,2)+1)
    Rem Create result array
        ReDim aResult(1 To iLen, 1 To 2)
        iLen = 0
    Rem Loop by each element of first array:
        For iA = LBound(aSourceA,1) To UBound(aSourceA,1)   
            For jA = LBound(aSourceA,2) To UBound(aSourceA,2)
    Rem Loop by each element of second array:
                For iB = LBound(aSourceB,1) To UBound(aSourceB,1)
                    For jB = LBound(aSourceB,2) To UBound(aSourceB,2)
    Rem Fill result array
                        iLen = iLen + 1
                        aResult(iLen, 1) = aSourceA(iA,jA)
                        aResult(iLen, 2) = aSourceB(iB,jB)
                    Next jB
                Next iB
            Next jA
        Next iA
        MyCartesian = aResult
        Exit Function 
    wrongData:
        MyCartesian = "Wrong param"
    End Function
    

    Enter something like =MYCARTESIAN(A2:C10; F2:G6) in a cell, press Ctrl+Shift+Enter and get an array