I wrote a VBA function that calls the SUMIFS worksheet function. It works perfectly but I would like to get rid of the Select statement. That requires that the Criteria range
and Criteria
argument pairs, nominally unlimited in number, be passed to SUMIFS as one array which is assembled in the function.
Here is my current code.
Function SUMIFS(SumRng As Range, _
ParamArray Ifs() As Variant) As Double
' each element of Ifs is an array of 3 elements:
' 0 = Criteria range, 1 = Operator, 2 = Criterium
Const Symbols As String = "=,<>,>,<,<=,>="
Dim Symb() As String
Dim Tmp As Variant
Dim i As Long ' Ifs index
Symb = Split(Symbols, ",")
For i = LBound(Ifs) To UBound(Ifs)
Tmp = Ifs(i)(1)
If VarType(Ifs(i)(2)) = vbDate Then
Ifs(i)(1) = Format(Ifs(i)(2), Ifs(i)(0).Cells(1).NumberFormat)
Else
Ifs(i)(1) = Ifs(i)(2)
End If
If Val(Tmp) Then Ifs(i)(1) = Symb(Tmp) & Ifs(i)(1)
Next i
Select Case UBound(Ifs)
Case 0
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Ifs(0)(0), Ifs(0)(1))
Case 1
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Ifs(0)(0), Ifs(0)(1), _
Ifs(1)(0), Ifs(1)(1))
Case 2
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Ifs(0)(0), Ifs(0)(1), _
Ifs(1)(0), Ifs(1)(1), _
Ifs(2)(0), Ifs(2)(1))
Case 3
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Ifs(0)(0), Ifs(0)(1), _
Ifs(1)(0), Ifs(1)(1), _
Ifs(2)(0), Ifs(2)(1), _
Ifs(3)(0), Ifs(3)(1))
Case 4
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Ifs(0)(0), Ifs(0)(1), _
Ifs(1)(0), Ifs(1)(1), _
Ifs(2)(0), Ifs(2)(1), _
Ifs(3)(0), Ifs(3)(1), _
Ifs(4)(0), Ifs(4)(1))
End Select
End Function
Note that the operator is passed to this function as a number (enum) between 0 and 5 which specifies one of the elements of Symb()
.
As you see, there are 4 different function calls in this procedure and if the function were to be called with 5 criteria pairs it would fail. Meanwhile, the differences between the 4 calls are minute and systematic, and a fifth one could be added in a minute.
I'm looking for a way to pass an assembled array of arguments to a single call of the worksheet function. I know this is possible by creating the corresponding worksheet function string and using the Evaluate
function but when I write worksheet functions in VBA they turn out messy, meaning they are difficult to maintain and evolve. I like the clear structure of my above code and wouldn't like to sacrifice it for a little more efficiency, meaning I am open to arguments of much greater efficiency or pleasing design, whichever might be on offer.
EDIT 20 Jan 2021
I thought I had a solution with @GSerg's idea but the function call only accepts 2 sets of criteria. It doesn't seem to make sense because the third criterium is created in just the same way as the second. I'm wondering if I've become blind to a simple flaw. Please take a look.
Private Sub Test_SUMIFS()
Dim SumRng As Range
Set SumRng = Range("A2:A11")
Debug.Print SUMIFS(SumRng, Array(Range("B2:B11"), 0, "A"))
Debug.Print SUMIFS(SumRng, Array(Range("B2:B11"), 0, "A"), _
Array(Range("C2:C11"), 0, 10))
' Debug.Print SUMIFS(SumRng, Array(Range("B2:B11"), 0, "A"), _
' Array(Range("C2:C11"), 0, 10), _
' Array(Range("D2:D11"), 0, "Z"))
End Sub
Function SUMIFS(SumRng As Range, _
ParamArray Ifs() As Variant) As Double
' each element of Ifs is an array of 3 elements:
' 0 = Criteria range, 1 = Operator, 2 = Criterium
Const Symbols As String = "=,<>,>,<,<=,>="
Dim Symb() As String
Dim Fun() As Variant ' Converterd Ifs()
Dim Tmp As Variant
Dim i As Long ' Ifs index
ReDim Fun(2, 1) ' extend to a maximum of 14 if required
Symb = Split(Symbols, ",")
For i = LBound(Ifs) To 1
If i > UBound(Ifs) Then
Fun(i, 0) = SetMissing()
Fun(i, 1) = SetMissing()
Else
Set Fun(i, 0) = Ifs(i)(0)
Fun(i, 1) = Ifs(i)(2)
Tmp = Ifs(i)(1)
If VarType(Ifs(i)(2)) = vbDate Then
Fun(i)(1) = Format(Ifs(i)(2), Ifs(i)(0).Cells(1).NumberFormat)
Else
Fun(i, 1) = Ifs(i)(2)
End If
If Val(Tmp) Then Fun(i)(1) = Symb(Tmp) & Fun(i)(1)
End If
Next i
' this function call works for both calls
SUMIFS = WorksheetFunction.SUMIFS(SumRng, Fun(0, 0), Fun(0, 1), _
Fun(1, 0), Fun(1, 1))
' this function call doesn't work
' SUMIFS = WorksheetFunction.SUMIFS(SumRng, Fun(0, 0), Fun(0, 1), _
' Fun(1, 0), Fun(1, 1), _
' Fun(2, 0), Fun(2, 1))
End Function
Private Function SetMissing(Optional ByVal MissingValue As Variant) As Variant
' assign the value of "Missing" to an uninitialized variant
If IsMissing(MissingValue) Then
SetMissing = MissingValue
Else
Err.Raise 5, , "Wrong use of function: The parameter must be missing!"
End If
End Function
If you run the code as it is it will work once the test ranges have been set up in the ActiveSheet, but not with the second worksheet function call.