What is the benefit of using ParamArray (vs a Variant array)? - vba

I've used the ParamArray statement for years when I wanted to accept a variable number of arguments. One good example is this MinVal function:
Function MinVal(ParamArray Values() As Variant)
Dim ReturnVal As Variant, v As Variant
If UBound(Values) < 0 Then
ReturnVal = Null
Else
ReturnVal = Values(0)
For Each v In Values
If v < ReturnVal Then ReturnVal = v
Next v
End If
MinVal = ReturnVal
End Function
' Debug.Print MinVal(10, 23, 4, 17)
' 4
This could be re-written without the ParamArray as:
Function MinVal(Optional Values As Variant)
Dim ReturnVal As Variant, v As Variant
If IsMissing(Values) Or IsNull(Values) Then
ReturnVal = Null
Else
ReturnVal = Values(0)
For Each v In Values
If v < ReturnVal Then ReturnVal = v
Next v
End If
MinVal = ReturnVal
End Function
' Debug.Print MinVal(Array(10, 23, 4, 17))
' 4
Note in the second example use of the Array() function in the call to MinVal.
The second approach has the advantage of being able to pass the parameter array to another function that also accepts arrays. This provides flexibility if I ever wanted to be able to pass the parameter array in MinVal on to some other function.
I've begun thinking I should always favor this approach and just stop using ParamArray altogether.
One could argue that using ParamArray makes for more explicitly readable code. However, there's no advantage in compile-time checks because ParamArray must be an array of Variants. Can anyone offer a compelling reason to ever use ParamArray?

Most of my ParamArray functions have std Array versions that do the heavy lifting like this:
Private Sub Command2_Click()
Process 1, 2, 3
End Sub
Private Sub Process(ParamArray A() As Variant)
ProcessArray CVar(A)
End Sub
Private Sub ProcessArray(B As Variant)
Debug.Print UBound(B)
End Sub
This does not work for output params though, so yes replacing ParamArray with Array gets really fast very inconvenient for output params.

You've already noted one reason to use Paramarray - sometimes it makes code more clear. In fact, I think in your own example that:
Debug.Print MinVal(10, 23, 4, 17)
is preferable to:
Debug.Print MinVal(Array(10, 23, 4, 17))
Of course, how "compelling" this is is a matter of opinion. Code clarity rarely matters much in small examples, but in a large code base it can add up to be very important.
If you're using VBA with Excel, then there is a somewhat-compelling reason to use ParamArray. Consider a UDF of the following form:
Public Function mungeRanges(ParamArray ranges())
'do something with a bunch of ranges
End Function
The built-in Excel function MIN works like this in fact - you can pass in multiple arguments, including ranges or arrays, and it looks through all of them to find the smallest individual value. Anything that follows a similar pattern would need to use ParamArray.

Related

VBA Debug Print ParamArray Error 13 Type Mismatch Values

In Access VBA, I am trying to print the values of a parsed Parameter array but keep getting a Runtime Error 13 - Type Mismatch. The values in the array are mixed types i.e. Double, String, Long.
Code as follows:
Function MyArray() as Variant
Dim MyParams(2) as Variant
MyParams(0) = "3459"
MyParams(1) = "3345"
MyParams(2) = "34.666"
MyArray = MyParams
End Function
Sub PrintArray(ParamArray Params() As Variant)
Dim p_param as Variant
For Each p_param in Params
Debug.Print params < - Error occurs here
Next p_param
End Sub
I tried converting to string etc but it still wont work.
Any suggestions?
In order to iterate the ParamArray values, you need to understand what arguments you're receiving.
Say you have this:
Public Sub DoSomething(ParamArray values() As Variant)
The cool thing about ParamArray is that it allows the calling code to do this:
DoSomething 1, 2, "test"
If you're in DoSomething, what you receive in values() is 3 items: the numbers 1 & 2, and a string containing the word test.
However what's happening in your case, is that you're doing something like this:
DoSomething Array(1, 2, "test")
And when you're in DoSomething, what you receive in values() is 1 item: an array containing the numbers 1 & 2, and a string containing the word test.
The bad news is that you can't control how the calling code will be invoking that function.
The good news is that you can be flexible about it:
Public Sub DoSomething(ParamArray values() As Variant)
If ArrayLenth(values) = 1 Then
If IsArray(values(0)) Then
PrintArray values(0)
End If
Else
PrintArray values
End If
End Sub
Public Function ArrayLength(ByRef target As Variant) As Long
Debug.Assert IsArray(target)
ArrayLength = UBound(target) - LBound(target) + 1
End Function
Now either way can work:
DoSomething 1, 2, "test"
DoSomething Array(1, 2, "test")
If an element of the passed in Params() array is an array itself then treat it as such, otherwise just print...
Private Sub PrintArray(ParamArray Params() As Variant)
Dim p_param As Variant
Dim i As Long
For Each p_param In Params
If IsArray(p_param) Then
For i = LBound(p_param) To UBound(p_param)
Debug.Print p_param(i)
Next
Else
Debug.Print p_param
End If
Next p_param
End Sub

Excel VBA - Call application function (left, right) by name

I would like to be able to call application functions such as left() and right() using a string variable.
The reason is that my current code works fine, but has multiple instances of left() and right() that may need to change every time I run it. I'd like to be able to only change it once ("globally") every time.
After googling, I tried CallByName and Application.Run. It seems that they only work with a custom class/macro. Is this true? Is there anything else I should look into? I don't need specific code, thank you!
You can build a custom function where you pass if you want Left or Right.
Option Explicit
Sub Test()
Debug.Print LeftRight("L", "StackOverflow", 5)
Debug.Print LeftRight("R", "StackOverflow", 8)
End Sub
Function LeftRight(sWhich As String, sValue As String, iLength As Integer) As String
Select Case sWhich
Case "L": LeftRight = Left(sValue, iLength)
Case "R": LeftRight = Right(sValue, iLength)
End Select
End Function
You just use "L" or "R" as needed. Change it once and pass as sWhich each time.
You can even use a cell reference for this and update the cell before running code.
Results
Stack
Overflow
The easiest way around this is to replace all your Left and Right calls with a generic function, e.g. instead of
x = Left("abc", 2)
say
x = LeftOrRight("abc", 2)
and then have a function
Function LeftOrRight(str As Variant, len As Long) As Variant
'Uncomment this line for Left
LeftOrRight = Left(str, len)
'Uncomment this line for Right
LeftOrRight = Right(str, len)
End Function
Then you can just change the one function as required.
You could also use SWITCH to implement Scott's idea (no error handling if string length is invalid):
Sub Test()
Debug.Print LeftRight("L", "StackOverflow", 5)
Debug.Print LeftRight("R", "StackOverflow", 8)
End Sub
Function LeftRight(sWhich As String, sValue As String, iLength As Integer) As String
LeftRight = Switch(sWhich = "L", Left$(sValue, iLength), sWhich = "R", Right$(sValue, iLength))
End Function

passing range on another sheet to vlookup

I can't reference a range with a sheet for my function like I can with =vlookup.
This works: =MVLOOKUP(a2,B:C,2,",",", ")
This isn't: =MVLOOKUP(a2,Sheet3!B:C,2,",",", ")
The code:
Public Function MVLookup(Lookup_Values, Table_Array As Range, Col_Index_Num As Long, Input_Separator As String, Output_Separator As String) As String
Dim in0, out0, i
in0 = Split(Lookup_Values, Input_Separator)
ReDim out0(UBound(in0, 1))
For i = LBound(in0, 1) To UBound(in0, 1)
out0(i) = Application.WorksheetFunction.VLookup(in0(i), Table_Array, Col_Index_Num, False)
Next i
MVLookup = Join(out0, Output_Separator)
End Function
I don't know basic and I'm not planning to learn it, I rarely even use excel, so sorry for the lame question. I guess basic is really "basic" it took me 30 minutes to get to this point from the reference(reading included), but other 60 minutes in frustration because the above problem.
Help me so I can go back to my vba free life!
EDIT: Although the code above worked after an excel restart, Jeeped gave me a safer solution and more universal functionality. Thanks for that.
I was not planning to use it on other than strings but thanks for the addition, I wrongly assumed there is a check for data type every time and type passed along in the background and vlookup acting accordingly. I have also learned how to set default values to function input variables.
See solution.
Thanks again, Jeeped!
You are confusing 1 with "1" and regardless of your personal distaste for VBA, I really don't know of any programming language that treats them as identical values (with the possible exception of a worksheet's COUNTIF function).
Public Function MVLookup(Lookup_Values, table_Array As Range, col_Index_Num As Long, _
Optional Input_Separator As String = ",", _
Optional output_Separator As String = ", ") As String
Dim in0 As Variant, out0 As Variant, i As Long
in0 = Split(Lookup_Values, Input_Separator)
ReDim out0(UBound(in0))
For i = LBound(in0) To UBound(in0)
If IsNumeric(in0(i)) Then
If Not IsError(Application.Match(Val(in0(i)), Application.Index(table_Array, 0, 1), 0)) Then _
out0(i) = Application.VLookup(Val(in0(i)), table_Array, col_Index_Num, False)
Else
If Not IsError(Application.Match(in0(i), Application.Index(table_Array, 0, 1), 0)) Then _
out0(i) = Application.VLookup(in0(i), table_Array, col_Index_Num, False)
End If
Next i
MVLookup = Join(out0, output_Separator)
End Function
When you Split a string into a variant array, you end up with an array of string elements. Granted, they look like numbers but they are not true numbers; merely textual representational facsimiles of true numbers. The VLOOKUP function does not treat them as numbers when the first column in your table_array parameter is filled with true numbers.
The IsNumeric function can reconize a string that looks like a number and then the Val function can convert that text-that-looks-like-a-number into a true number.
I've also added a quick check to ensure what you are looking for is actually there before you attempt to stuff the return value into an array.
Your split strings are one-dimensioned variant arrays. There is no need to supply the rank in the LBound / UBound functions.
                    Sample data on Sheet3                                  Results from MVLOOKUP
This is not a valid range reference ANYWHERE in Excel B:Sheet3!C.
Either use B:C or Sheet3!B:C
Edit. Corrected as per Jeeped's comment.

VBA array syntax

Looking over vba arrays and stumbled upon something and need someone to clear it up.
Sub AAATest()
Dim StaticArray(1 To 3) As Long
Dim N As Long
StaticArray(1) = 1
StaticArray(2) = 2
StaticArray(3) = 3
PopulatePassedArray Arr:=StaticArray
For N = LBound(StaticArray) To UBound(StaticArray)
Debug.Print StaticArray(N)
Next N
End Sub
AND
Sub PopulatePassedArray(ByRef Arr() As Long)
''''''''''''''''''''''''''''''''''''
' PopulatePassedArray
' This puts some values in Arr.
''''''''''''''''''''''''''''''''''''
Dim N As Long
For N = LBound(Arr) To UBound(Arr)
Arr(N) = N * 10
Next N
End Sub
What's happening at
PopulatePassedArray Arr:=StaticArray
in AAATest sub
There are two ways you can pass arguments to another procedure: using named arguments or in order. When you pass them in order, you must past them in the same order as the procedure definition.
Function DoTheThing(arg1 As Double, arg2 As String, arg3 As Boolean) As Double
When you call this function (in order), you call it like
x = DoTheThing(.01, "SomeString", TRUE)
When you call the function using named arguments, you use :=, the name of the argument, and the value of the argument. The := is not a special assignment operator - well I guess it kind of is. The upshot is that when you use named arguments, you can supply them in any order.
x = DoTheThing(arg2:="SomeString", arg3:=TRUE, arg1:=.01)
Some people also think that named arguments make your code more readable. I'm not one of those people. It clutters it up and if you're passing more than two or three arguments, you're doing it wrong anyway.

No max(x,y) function in Access

VBA for Access lacks a simple Max(x,y) function to find the mathematical maximum of two or more values. I'm accustomed to having such a function already in the base API coming from other languages such as perl/php/ruby/python etc.
I know it can be done: IIf(x > y, x,y). Are there any other solutions available?
I'll interpret the question to read:
How does one implement a function in Access that returns the Max/Min of an array of numbers? Here's the code I use (named "iMax" by analogy with IIf, i.e., "Immediate If"/"Immediate Max"):
Public Function iMax(ParamArray p()) As Variant
' Idea from Trevor Best in Usenet MessageID rib5dv45ko62adf2v0d1cot4kiu5t8mbdp#4ax.com
Dim i As Long
Dim v As Variant
v = p(LBound(p))
For i = LBound(p) + 1 To UBound(p)
If v < p(i) Then
v = p(i)
End If
Next
iMax = v
End Function
Public Function iMin(ParamArray p()) As Variant
' Idea from Trevor Best in Usenet MessageID rib5dv45ko62adf2v0d1cot4kiu5t8mbdp#4ax.com
Dim i As Long
Dim v As Variant
v = p(LBound(p))
For i = LBound(p) + 1 To UBound(p)
If v > p(i) Then
v = p(i)
End If
Next
iMin = v
End Function
As to why Access wouldn't implement it, it's not a very common thing to need, seems to me. It's not very "databasy", either. You've already got all the functions you need for finding Max/Min across domain and in sets of rows. It's also not very hard to implement, or to just code as a one-time comparison when you need it.
Maybe the above will help somebody.
Calling Excel VBA Functions from MS Access VBA
If you add a reference to Excel (Tools → References → Microsoft Excel x.xx Object Library) then you can use WorksheetFunction to call most Excel worksheet functions, including MAX (which can also be used on arrays).
Examples:
MsgBox WorksheetFunction.Max(42, 1999, 888)
or,
Dim arr(1 To 3) As Long
arr(1) = 42
arr(2) = 1999
arr(3) = 888
MsgBox WorksheetFunction.Max(arr)
The first call takes a second to respond (actually 1.1sec for me), but subsequent calls are much more reasonable (<0.002sec each for me).
Referring to Excel as an object
If you're using a lot of Excel functions in your procedure, you may be able to improve performance further by using an Application object to refer directly to Excel.
For example, this procedure iterates a set of records, repeatedly using Excel's MAX on a Byte Array to determine the "highest" ASCII character of each record.
Option Compare Text
Option Explicit
'requires reference to "Microsoft Excel x.xx Object Library"
Public excel As New excel.Application
Sub demo_ListMaxChars()
'list the character with the highest ASCII code for each of the first 100 records
Dim rs As Recordset, mx
Set rs = CurrentDb.OpenRecordset("select myField from tblMyTable")
With rs
.MoveFirst
Do
mx = maxChar(!myField)
Debug.Print !myField, mx & "(" & ChrW(mx) & ")" '(Hit CTRL+G to view)
.MoveNext
Loop Until .EOF
.Close
End With
Set rs = Nothing 'always clean up your objects when finished with them!
Set excel = Nothing
End Sub
Function maxChar(st As String)
Dim b() As Byte 'declare Byte Array
ReDim b(1 To Len(st)) 'resize Byte Array
b = StrConv(st, vbFromUnicode) 'convert String to Bytes
maxChar = excel.WorksheetFunction.Max(b) 'find maximum Byte (with Excel function)
End Function
Because they probably thought that you would use DMAX and DMIN or the sql MAX and only working with the database in access?
Im also curious about why.. Its seems like a overkill to have to create a temp-table and add form values to the table and then run a DMAX or MAX-query on the table to get the result...
I've been known to create a small projMax() function just to deal with these. Not that VBA will probably ever be enhanced, but just in case they ever do add a proper Max (and Min) function, it won't conflict with my functions. BTW, the original poster suggests doing IIF... That works, but in my function, I usually throw a couple of Nz()'s to prevent a null from ruining the function.
Both functions have problems with Null. I think this will be better.
Public Function iMin(ParamArray p()) As Variant
Dim vVal As Variant, vMinVal As Variant
vMinVal = Null
For Each vVal In p
If Not IsNull(vVal) And (IsNull(vMinVal) Or (vVal < vMinVal)) Then _
vMinVal = vVal
Next
iMin = vMinVal
End Function
I liked DGM's use of the IIF statement and David's use of the For/Next loop, so I am combining them together.
Because VBA in access does not have a strict type checking, I will be using varients to preserve all numerics, integer and decimal, and re-type the return value.
Kudos to HansUP for catching my parameter verification :)
Comments added to make code more friendlier.
Option Compare Database
Option Base 0
Option Explicit
Function f_var_Min(ParamArray NumericItems()) As Variant
If UBound(NumericItems) = -1 Then Exit Function ' No parameters
Dim vVal As Variant, vNumeric As Variant
vVal = NumericItems(0)
For Each vNumeric In NumericItems
vVal = IIf(vNumeric < vVal, vNumeric, vVal) ' Keep smaller of 2 values
Next
f_var_Min = vVal ' Return final value
End Function
Function f_var_Max(ParamArray NumericItems()) As Variant
If UBound(NumericItems) = -1 Then Exit Function ' No parameters
Dim vVal As Variant, vNumeric As Variant
vVal = NumericItems(0)
For Each vNumeric In NumericItems
vVal = IIf(vNumeric < vVal, vVal, vNumeric) ' Keep larger of 2 values
Next
f_var_Max = vVal ' Return final value
End Function
The only difference between the 2 functions is the order of vVal and vNumeric in the IIF statement.The for each clause uses internal VBA logic to handle the looping and array bounds checking, while "Base 0" starts the array index at 0.
You can call Excel functions in Access VBA:
Global gObjExcel As Excel.Application
Public Sub initXL()
Set gObjExcel = New Excel.Application
End Sub
Public Sub killXL()
gObjExcel.Quit
Set gObjExcel = Nothing
End Sub
Public Function xlMax(a As Double, b As Double) As Double
xlCeiling = gObjExcel.Application.Max(a, b)
End Function
You can do Worksheetfunction.max() or worksheetfunction.min() within Access VBA. Hope this helps.