VBA join array elements to then pass as multiple parameters to function - vba

[Update: The question has been formed so that the functions can be copied directly into a VBA module and tested by running the test_callback_loop_function_works_with_multiple_parameters method]
I am using the Application.Run function to dynamically call methods within my VBA. The idea is that this helper will save me looping through dictionaries throughout various functions/subs within my VBA. Instead I can just call the following helper which will do the looping for me:
Public Function user_func_dictionary_loop(Dictionary As Dictionary, _
MethodCallback As String, _
Optional Params As Variant) As Boolean
Dim Key As Variant
For Each Key In Dictionary
If IsMissing(Params) Then
Application.Run MethodCallback, Dictionary(Key)
Else
Application.Run MethodCallback, user_param_replace(Dictionary(Key), Params)
End If
Next Key
End Function
If no Parameters are supplied to the function then it simply runs the MethodCallback with the Dictionary's key value. If there are parameters then an additional step is triggered below:
Private Function user_param_replace(Item As Variant, Optional Params As Variant) As Variant
Dim i As Long
Dim strTest As String
Dim Output As Variant
Output = replace_dictionary_values(Item, Params)
If IsArray(Output) Then
ReDim Preserve Output(0 To UBound(Output))
user_param_replace = Join(Output, ",")
Exit Function
End If
user_param_replace = Output
End Function
Private Function replace_dictionary_values(Item As Variant, Optional Params As Variant) As Variant
Dim l As Long
Dim varTemp() As Variant
Dim Param_Item As Variant
l = 0
If IsMissing(Params) Or Not IsArray(Params) Then
replace_dictionary_values = Replace$(Params, "{D:Value}", Item)
Exit Function
Else
ReDim varTemp(0 To UBound(Params))
For Each Param_Item In Params
varTemp(l) = Replace$(Param_Item, "{D:Value}", Item)
l = l + 1
Next Param_Item
End If
replace_dictionary_values = varTemp
End Function
The steps above allow a user to pass in parameters which contain {D:Value} which will then be replaced with the Dictionary's key value.
I've made a small unit test below with the idea that it should test the functionality of my method. At present I'm getting an "Argument not optional" error:
Function test_callback_loop_function_works_with_multiple_parameters() As Boolean
Dim dictTest As New Dictionary
dictTest.Add 1, "1 - Foo"
dictTest.Add 2, "2 - Foo"
dictTest.Add 3, "3 - Foo"
Dim MyArray(0 To 1) As Variant
MyArray(0) = "{D:Value}"
MyArray(1) = "Bar"
user_func_dictionary_loop dictTest, "custom_debug_print_multiple_params", MyArray
test_callback_loop_function_works_with_multiple_parameters = True
End Function
Function custom_debug_print_multiple_params(strPrint As String, strPrint2 As String) As String
Debug.Print strPrint & strPrint2
End Function
The output should be:
1 - FooBar
2 - FooBar
3 - FooBar
But I'm getting an
Run-time error '449' - Argument not optional
error on the Application.Run MethodCallback, user_param_replace(Dictionary(Key), Params) line.
My hunch is that because I'm trying to join array elements together with a "," to then pass through as parameters (in the Join(Output, ",") line) to the method, it's causing the test to fail.
So my question is, within VBA, is it possible to join the elements of an array together so they can then be passed, dynamically, to another method/function?

There is a problem with this line of code.
replace_dictionary_values = Replace$(Params, "{D:Value}", Item)
This line is called when IsMissing(Params) = True and, predictably, returns an error.
I also found that your test procedure can't work.
Function custom_debug_print_multiple_params(strPrint As String, strPrint2 As String) As String
Debug.Print strPrint & strPrint2
End Function
All your variables are variants but the two parameters of the above function are of string type. The arguments should be declared ByVal if variants of string type are to be passed. I recommend to test each function individually and make sure that it works before using its return value as parameters for other functions.
I suspect that part of your problem may be caused by your rather indiscriminate use of variants. For example, the Replace function you invoke in the faulty line quoted above requires 3 strings as arguments. In your code, both Item and Params (if it would exist) are variants. There is a good chance that your plan could actually work, but when something doesn't quite work, as is the case here, all those corners that were cut will have to be checked, adding more time to the debugging effort than could be saved during coding.
In the first example below the calling procedure supplies the two strings required by the called procedure. Variants of string type are passed which are converted to strings by the ByVal argument.
Function Test_TestPrint() As Boolean
Dim dictTest As New Scripting.Dictionary
Dim MyArray(0 To 1) As Variant
dictTest.Add 1, "1 - Foo"
dictTest.Add 2, "2 - Foo"
dictTest.Add 3, "3 - Foo"
MyArray(0) = "{D:Value}"
MyArray(1) = "Bar"
TestPrint MyArray(0), MyArray(1)
' user_func_dictionary_loop dictTest, "TestPrint", MyArray
Test_TestPrint = True
End Function
Sub TestPrint(ByVal strPrint As String, ByVal strPrint2 As String)
Debug.Print strPrint & strPrint2
End Sub
In the code below the array is passed to the executing procedure which expects such an array and prints out its elements.
Function Test_TestPrint2() As Boolean
Dim dictTest As New Scripting.Dictionary
Dim MyArray(0 To 1) As Variant
dictTest.Add 1, "1 - Foo"
dictTest.Add 2, "2 - Foo"
dictTest.Add 3, "3 - Foo"
MyArray(0) = "{D:Value}"
MyArray(1) = "Bar"
Sub TestPrint2 MyArray
' user_func_dictionary_loop dictTest, "TestPrint", MyArray
Test_TestPrint2 = True
End Function

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

Function CallByName with args out of order

I'm using callbyname in vb.net to call another function with arguments. But the arguments order is only known in runtime. So I want to define the argument by it's name, not the position in the args array.
Private Sub mainSub()
Dim argArr as Object()
ReDim Preserve argArr(1)
argArr(0) = "argTwo:= "Argument Two""
argArr(1) = "argOne:= "Argument One""
callbyname(auxSub, CallType.Method, argArr)
End Sub
Private Sub auxSub(ByVal argOne as String, ByVal argTwo as String)
MsgBox("First Argument = " & argOne)
MsgBox("Second Argument = " & argTwo)
End Sub
In this example the messagebox would show the following messages:
First Argument - argTwo:= "Argument Two"
Second Argument - argOne:= "Argument One"
Instead of:
First Argument - Argument One
Second Argument - Argument Two
One possibly nasty way of doing this would be for your function to use a dictionary of argument names as the key and assign values from the argArr. If you're just using a one type of value e.g string values as in your example and the number of possible arguments is reasonably small, you could do it like the example below, but anything more complicated and as Hans said, you'd need to muck about with reflection.
Private Sub mainSub()
Dim argArr As Object()
ReDim Preserve argArr(1)
argArr(0) = "argTwo:= ""Argument Two"""
argArr(1) = "argOne:= ""Argument One"""
auxSub(argArr)
End Sub
Private Sub auxSub(argArray() As String)
'define a dictionary of arguments and add valid argument names
Dim ArgumentList As New Dictionary(Of String, String)
ArgumentList.Add("argOne", "")
ArgumentList.Add("argTwo", "")
'Iterate through the argument array and assign the appropriate value to its matching dictionary key
For Each arg As String In argArray
ArgumentList(Split(arg, " := ")(0)) = Split(arg, " := ")(1)
Next
End Sub
You would of course need more code to check that the argument names are valid of course.

vba passing dynamic array to class

i have an issue with dynamic arrays being passed to class byVal instead byRef, so simplified class, cArray
Option Explicit
Private mArray() As String
Public Sub init(ByRef iArray() As String)
mArray = iArray
End Sub
Public Property Get count() As Long
count = UBound(mArray) - LBound(mArray)
End Property
Public Property Get item(iIndex As Long) As String
item = mArray(iIndex)
End Property
and simple function in module
Private Sub arrTest()
Dim arr() As String, cont As cArray
ReDim arr(0 To 1)
arr(0) = "value0"
arr(1) = "value1"
Set cont = New cArray
cont.init arr
arr(1) = "newValue1"
Debug.Print cont.item(1), arr(1) 'will print value1, newValue1 even though is expected to be same
ReDim Preserve arr(0 To 2)
arr(2) = "value2"
Debug.Print cont.count 'will print 1
End Sub
so, question is, is this bug? normal behavior? something else?
Actually, the array is passed by reference. The problem comes at the assignment.
In VBA assigning one array variable to another array variable or one string variable to another string variable creates a copy though there are ways around this using Variants or CopyMemory, for example. If you're interested, drop a comment.
I can demonstrate this by using VarPtr to get the actual addresses for comparison. Let's add a few lines to your code...
cArray
Option Explicit
Private mArray() As String
Public Sub init(ByRef iArray() As String)
Debug.Print "The address of the function parameter is: " & VarPtr(iArray(0)) '<----- add this line
mArray = iArray
Debug.Print "The address of the mArray class member is: " & VarPtr(mArray(0)) '<----- add this line
End Sub
Public Property Get count() As Long
count = UBound(mArray) - LBound(mArray)
End Property
Public Property Get item(iIndex As Long) As String
item = mArray(iIndex)
End Property
mdlMain
Private Sub arrTest()
Dim arr() As String, cont As cArray
ReDim arr(0 To 1)
arr(0) = "value0"
arr(1) = "value1"
Debug.Print "The address of the newly dimensioned and initialized arr is: " & VarPtr(arr(0)) '<----- add this line
Set cont = New cArray
cont.init arr
arr(1) = "newValue1"
Debug.Print cont.item(1), arr(1) 'will print value1, newValue1 even though is expected to be same
ReDim Preserve arr(0 To 2)
arr(2) = "value2"
Debug.Print cont.count 'will print 1
End Sub
When I run this, I get (the memory addresses will likely be different for you):
The address of the newly dimensioned and initialized arr is: 192524056
The address of the function parameter is: 192524056 The address of the
mArray class member is: 192524040
value1 newValue1 1
So you can see that the actual function parameter WAS passed by reference, but the assignment created a copy.

How to return array of user defined types, and then pass as an argument in VBA

I have a User Defined Type, Decision:
Public Type Decision
choice As String
cost As Double
End Type
I am trying to use an array of my UDTs to store the results of a dynamic program (choice and cost for a stage/state).
Public Function DPSolve(arg1, arg2, ...) as Decision
Dim Table() As Decision
ReDim Table(arg1, arg2+ 1)
'do stuff that fills each Table().choice and Table().cost
'return Table()
DPSolve = Table()
End Function
If I want to then pass the result of this function to a new function (to say, print the Table() in Excel, or do more work using the Table() result, how do I do this?
I am trying
Sub Main
Dim x as variant
x = DPSolve(arg1, arg2, ...)
Function2(x)
End Main
but am getting the following error:
I have tried making x an array, but I get a "cannot assign to array" error. I have also tried making x a Decision, but that did not work either. The code is in a module.
Thanks!
So DPSolve shall return an array of Decisions. And x()shall also be an array of Decisions.
Public Type Decision
choice As String
cost As Double
End Type
Public Function DPSolve(arg1, arg2) As Decision()
Dim Table() As Decision
ReDim Table(arg1, arg2 + 1)
'do stuff that fills each Table().choice and Table().cost
'return Table()
DPSolve = Table()
End Function
Sub Main()
Dim x() As Decision
x = DPSolve(2, 2)
End Sub
Works for me. Example:
Public Type Decision
choice As String
cost As Double
End Type
Public Function DPSolve(arg1, arg2) As Decision()
Dim Table() As Decision
ReDim Table(arg1, arg2 + 1)
'do stuff that fills each Table().choice and Table().cost
Table(1, 2).choice = "choice1,2"
Table(1, 2).cost = 123.45
'return Table()
DPSolve = Table()
End Function
Sub Main()
Dim x() As Decision
x = DPSolve(2, 2)
MsgBox x(1, 2).choice
MsgBox x(1, 2).cost
End Sub
To be clear with "Can't assign to an array". You can't assign an type and size dimensioned and filled array to another type and size dimensioned array. But you surely can assign an filled array to an type dimensioned but not size dimensioned array.
Sub test()
Dim arr1(3) As String
Dim arr2() As String
arr1(0) = "Value 0"
arr1(1) = "Value 1"
arr1(2) = "Value 2"
arr1(3) = "Value 3"
arr2 = arr1
MsgBox Join(arr2, ", ")
End Sub
The error mean that you can't assign a defined type to a variant.
So you either need to have the type defined on the variable
Public Type Decision
choice As String
cost As Double
End Type
Public Sub DPSolve(source, arg1, arg2)
ReDim source(arg1, arg2 + 1)
End Sub
Sub Main()
Dim x() As Decision
DPSolve x, 4, 4
End Sub
Or if you really want to use a variant, then you need to use a class:
Public Sub DPSolve(source, arg1, arg2)
Dim i&, j&
ReDim source(0 To arg1, 0 To arg2 + 1) As Decision
For i = 0 To arg1
For j = 0 To arg2 + 1
Set source(i, j) = New Decision
Next
Next
End Sub
Sub Main()
Dim x
DPSolve x, 4, 4
End Sub
Class Decision:
Public choice As String
Public cost As Double

VBA "Type mismatch: array or user-defined type expected” on String Arrays

I have a dynamic array of strings DMAs which I declare globally.
Dim DMAs() As String
I ReDim the array and assign values to it in the CreateArrayOf function which is of type String() that returns an array of type String()
DMAs = CreateArrayOf(Sites, 2, "", False)
Public Function CreateArrayOf( _
ByRef arrayFrom() As String, _
Optional ByVal numOfChars As Integer = 2, _
Optional ByVal filterChar As String = "", _
Optional ByVal filterCharIsInteger As Boolean = False _
) As String()
Dim i As Integer, _
j As Integer, _
strn As Variant, _
switch As Boolean, _
strArray() As String
'numOfChars 2 for DMA with no filterChar
'numOfChars 3 for W with filterChar "W"
'numOfChars 3 for A with filterChar "A"
'numofChars 2 for D with filterChar "D"
ReDim strArray(LBound(arrayFrom) To LBound(arrayFrom)) 'required in order to
'not throw error on first iteration
For i = LBound(arrayFrom) To UBound(arrayFrom) 'iterate through each site
switch = False
For Each strn In strArray 'iterate through the array to find whether the
'current site already exists
If strn = Mid(arrayFrom(i), 1, numOfChars) And Not strn = "" Then
switch = True
End If
Next strn
If switch = False Then 'if it doesn't exist add it to the array
ReDim Preserve strArray(1 To UBound(strArray) + 1)
strArray(UBound(strArray) - 1) = Mid(arrayFrom(i), 1, numOfChars)
End If
Next i
CreateArrayOf = strArray 'return the new array
End Function
When I attempt to pass the DMAs array to another function OutputAnArray
Private Sub OutputAnArray(ByRef arrayToOutput() As String)
Dim i As Variant
Dim x As Integer
x = 1
For Each i In arrayToOutput
Cells(x, 6).Value = i
x = x + 1
Next i
End Sub
I get the "Type mismatch: array or user-defined type expected". Throughout the whole process I only mess with string arrays.
If I take the content of the OutputAnArray function and put it in the parent function where I'm calling it from, everything's fine.
Any help is appreciated.
I changed all String definitions to Variants
Private Sub OutputAnArray(ByRef arrayToOutput() As Variant)
The culprit was still there, so then after a whole lot of attempts to get this to compile, I removed the () from the arrayToOutput parameter and it started working.
Private Sub OutputAnArray(ByRef arrayToOutput As Variant) 'fixed
What is still perplexing is the fact that in the following function definition, the () are needed for arrayFrom.
Public Function CreateArrayOf(ByRef arrayFrom() As Variant, _ ...
I really don't get it, if anyone has any idea of an explanation, I'd love to hear it.
From the documentation:
"Arrays of any type can't be returned, but a Variant containing an array can."
If follows that the function "CreateArrayOf" does not return an array of strings: it returns a variant containing an array of strings.
The variant cannot be passed as a parameter to a function expecting an array of strings:
Private Sub OutputAnArray(ByRef arrayToOutput() As String)
It can only be passed to a function expecting a variant:
Private Sub OutputAnArray(ByRef arrayToOutput as Variant)
Conversely, DMA is an array of strings:
Dim DMAs() As String
DMA can be passed to a function expecting an array of strings:
Public Function CreateArrayOf(ByRef arrayFrom() As String, _ .
And finally, "Type mismatch: array or user-defined type expected" is a generic type mismatch message. When you pass an array of the wrong type, or a variant array, and get the error "array expected", it's not particularly helpful.
There is no problem with returning typed arrays from functions or passing typed arrays to functions as arguments. The following works as expected:
Option Explicit
Sub asdfasf()
Dim DMAs() As String
DMAs = CreateAnArray()
OutputAnArray DMAs
End Sub
Private Function CreateAnArray() As String()
Dim arr() As String
ReDim arr(1 To 5)
Dim i As Long
For i = LBound(arr) To UBound(arr)
arr(i) = i
Next
CreateAnArray = arr
End Function
Private Sub OutputAnArray(ByRef arrayToOutput() As String)
Dim i As Long
For i = LBound(arrayToOutput) To UBound(arrayToOutput)
Debug.Print arrayToOutput(i)
Next
End Sub
Now, you never show how you actually pass the DMAs array to OutputAnArray.
I'm willing to make an educated guess that you are doing
OutputAnArray (DMAs)
which will indeed result in
Type mismatch: array or user-defined type expected
You cannot freely put parentheses in that manner. They have special meaning.
If you want parentheses to be used when calling a sub, you must use Call:
Call OutputAnArray(DMAs)
And if you don't care, omit the parentheses like in the example above:
OutputAnArray DMAs
I had the same error while passing an array (of user defined type) as an argument to a function ByRef.
In my case the problem was solved using the keyword "Call" in front of the function or the sub being called.
I don't really understand it, but to me it seems like VBA is trying to interpret the function/sub a couple of different ways in the absence of "Call" - which leads to the error message.
I personally try to avoid converting anything to a variant as long as possible.