Using Subroutine for output instead of a function - excel

I thought at this point that the role of functions and subroutines was very clear to me. But now I am not so sure... I see it written all the time
"Functions can return values / subroutines cannot return a value."
and
"a function can only return a single value" (I realize they can return arrays and such too).
But it seems as though I can effectively "return a value from a subroutine" if I pass the "result" variable into the subroutine... Is this considered a "poor practice?" or am I missing some other key concept here...
Method # 1 (Using a Function):
Sub test1()
Dim x As Integer
Dim y As Integer
Dim z As Integer
x = 2
y = 3
z = test2(x, y)
End Sub
Function test2(var1 As Integer, var2 As Integer) As Integer
test2 = var1 + var2
End Function
Method # 2 (Using a Subroutine):
Sub test3()
Dim x As Integer
Dim y As Integer
Dim z As Integer
Call test4(x, y, z)
End Sub
Sub test4(var1 As Integer, var2 As Integer, var3 As Integer)
var1 = 2
var2 = 3
var3 = var1 + var2
End Sub

Usually, it is bad practice to change the value of a parameter. Just look at you examples - it is obvious that your function does something with the 2 parameters and returns a value (which you write to z). In the second example, you don't see what will happen unless you look to the subroutine - and not only to the function definition, you need to read the complete code so that you can tell what parameter will manipulated and what not.
In software development, when you call a subroutine, you don't want to look at this subroutine - and often it is even not available for you. Let the subroutine do it's job, but without any side effects.
Use a function whenever possible, it keeps your code much more readable. Trust me...
There are (very few) cases when you want to receive more than one result from a subroutine. In that case, I would advice to put explicitly the keyword ByRef in front of the parameter (even if in VBA this is technically not necessary because it's the default). And put a comment that tells why it is the case. You will thank yourself when you look at your code weeks, months or years later.

Related

VBA reading global from cell

I am a complete VBA newbie, so apologies in advance for the trivial question. Consider the following code:
Dim frog As Double
frog = Range("A1").Value
Function test_func(ByVal a As Double, ByVal b As Double)
test_func = a ^ b
End Function
Private Sub btnAddNumbersFunction_Click()
MsgBox test_func(frog, 3)
End Sub
When I try to compile this, I get an error of "invalid outside procedure" with the "A1" highlighted. (I am trying to define a model with some user-settable parameters, and so this would be useful to have).
This code sample will compile, I have moved the variable assignment into the sub:
Dim frog As Double
Function test_func(ByVal a As Double, ByVal b As Double)
test_func = a ^ b
End Function
Private Sub btnAddNumbersFunction_Click()
'Assign values inside subs or functions
frog = Range("A1").Value
MsgBox test_func(frog, 3)
End Sub
It seems that introducing another "initialization" sub, where the globals are initialized, does the trick. The suggestions in the other answers are not satisfactory for the reasons explained in the question: having to initialize (or even mention) the parameters in every procedure that implicitly uses them defeats the purpose. I am a bit mystified about why this is the design (this is unlike any other language I know), but MSFT moves in mysterious ways its wonders to perform.

Why declare function's type?

Sorry if this has been asked, but seriously can't find anything, so would also appreciate on how to search for this stuff.
So my question: what is the point of declaring the function's type in general? E.g. here 'as double'
Function myFunction(ByVal j As Integer) As Double
Return 3.87 * j
End Function
For a normal variable it has tons of benefits, like less memory, easier to see typos, but why here?
Edit: so, it's good because we can avoid errors, like it giving back a different type of values than expected.
Functions RETURN something. That type is the type of the return.
In your function:
Function myFunction(ByVal j As Integer) As Double
Return 3.87 * j
End Function
You are returning a decimal, so type Double make sense.
If you don't return anything, then you can declare it as a Sub.
And, for clarification, your function would throw a compile error. Unlike other languages, in VBA to return, we set the function name's value to the thing we want to return:
Function myFunction(ByVal j As Integer) As Double
myFunction=3.87 * j
End Function
Now we can call this function to get the Double value that it creates:
Sub testSub()
msgbox("This is the result of the function: " & myFunction(10))
End Sub
Which would launch a message box saying "This is the result of the function: 38.7"
Since I can't mark a comment the answer, let me quote:
#John Coleman
My opinion is that it a good thing to declare your return types because it increases the likelihood that the compiler will complain when you are doing something that really doesn't make sense.
Excel VBA is different from other programming languages in that it centers around a particular application: Excel.
Functions are useful in Excel VBA primarily because they can be typed directly into a cell on a sheet by an end user. User defined functions provide near infinite flexibility. The value the user defined function prints to Excel is formatted based on the function's type--and in a program which is about data visualization, formatting is a huge part.
For example, try putting these four functions into a blank worksheet module:
Function myInt(x, y) As Integer
myInt = x / y
End Function
Function myDouble(x, y) As Double
myDouble = x / y
End Function
Function myString(x, y) As String
myString = x / y
End Function
Function myVariant(x, y)
myVariant = x / y
End Function
Next, enter each of these functions into a different cell in the workbook. Use x=1 and y=2.
myInt produces "=0"
myDouble produces "=0.5"
myString produces "'0"
myVariant produces "=0.5"
If you're okay with Excel deciding how to format your result, that's your choice, but specifying the type offers an entire new level of control. For example, by simply declaring a function an integer, you can avoid having to devote a line of code to rounding. By declaring a function to be a string, you can avoid several lines of formatting code trying to get a number to be saved as text instead.

How to implement an ISBETWEEN-like function for arrays of values without having to calculate the array more than once?

An ISBETWEEN function tests whether a value falls between a lower bound and a higher bound. With no native ISBETWEEN function in Excel, the value under test must be compared twice; first with '>' and then with '<' (or '>=' and '<=' for an ISBETWEEN test that is inclusive of the bounds.)
Comparing the value twice means having to calculate it twice, and this can be extremely expensive when that value is an array. With array functions being somewhat cryptic even at the best of times, doubling up on such a calculation also sends the readability of the function plummeting.
My question is whether anyone knows of a technique that delivers ISBETWEEN-like functionality for an array of values without the double calculation of that array? My preference is to do this with native Excel functionality but, if anyone has some great VBA, that would be good too.
Many thanks for your time!
Will
Building from my comment above: This doesn't provide a 100% answer to your question, but since it was pretty generic, I think this is the closest to an answer that I can get.
Imagine a spreadsheet set up like:
We can get a count of all the values that are between 3 and 5 using CTE/Array formula:
={SUM(IF(LOOKUP(A1:A6,{3,"B";6,"C"})="B",1,0))}
Results:
5
That's a pretty round-about way of doing this, but the array of A1:A6 only needs to be referenced once. Which is pretty cool.
Note that the squirrely brackets in the above formula aren't actually entered, but are placed by excel when you enter the array formula to indicate that it's an array formula... you probably already know that though if you've read this far.
So I've been able to develop a piece of VBA, based on the idea here.
Dim vValueArg As Variant, vLowerArg As Variant, vUpperArg As Variant, vTestLower As Variant, vTestUpper As Variant
Function ISBETWEEN(vValue As Variant, vLower As Variant, vUpper As Variant, Optional bInc As Boolean = True) As Variant
vValueArg = vValue
vLowerArg = vLower
vUpperArg = vUpper
If bInc Then
vTestLower = [GetValue() >= GetLower()]
vTestUpper = [GetValue() <= GetUpper()]
Else
vTestLower = [GetValue() > GetLower()]
vTestUpper = [GetValue() < GetUpper()]
End If
ISBETWEEN = [IF((GetTestLower() * GetTestUpper()) = 1, TRUE, FALSE)]
End Function
Function GetValue() As Variant
GetValue = vValueArg
End Function
Function GetLower() As Variant
GetLower = vLowerArg
End Function
Function GetUpper() As Variant
GetUpper = vUpperArg
End Function
Function GetTestLower() As Variant
GetTestLower = vTestLower
End Function
Function GetTestUpper() As Variant
GetTestUpper = vTestUpper
End Function
The first argument can be a single value, range or array. If a single value, then the next two arguments must also be single values (but this kinda defeats the purpose of the code!)
The second and third arguments can also be a single value, range or array. If a range consisting of multiple cells or array of multiple values, then the dimensions of these arguments must match those of the first argument. (NB - I have NOT tested the code with 2 dimensional ranges or arrays!)
The final, optional, argument determines whether the ISBETWEEN test is performed including or excluding the bounds. TRUE = include bounds; i.e. arg2 <= arg1 <= arg3 (the default, and can therefore be omitted). FALSE = exclude bounds; i.e. arg2 < arg1 < arg3.
While this might not be the prettiest code in the world, it is compact, fast (no loops) and copes with ranges and arrays of any size.
Hope some of you find this useful! :)

VBA: Set variable to "Empty" or "Missing"? Handling multiple optional arguments?

Is it possible, or desirable, to set objects/data to an "Empty" or "Missing" variant?
I want to be able to conditionally pass optional arguments to a function. Sometimes I want to use an optional argument, sometimes I don't.
In Python, you could easily pass through whichever optional arguments you wanted by using **kwdargs to unpack a dictionary or list into your function arguments. Is there something similar (or a way to hack it in VBA) so you can pass in Empty/Missing optional arguments?
In particular, I'm trying to use Application.Run with an arbitrary number of arguments.
EDIT:
I'm basically trying to do this:
Public Function bob(Optional arg1 = 0, Optional arg2 = 0, Optional arg3 = 0, Optional arg4 = 0)
bob = arg1 + arg2 + arg3 + arg4
End Function
Public Function joe(Optional arg1)
joe = arg1 * 4
End Function
Public Sub RunArbitraryFunctions()
'Run a giant list of arbitrary functions pseudocode
Dim flist(1 To 500)
flist(1) = "bob"
flist(2) = "joe"
flist(3) = "more arbitrary functions of arbitrary names"
flist(N) = ".... and so on"
Dim arglist1(1 To 4) 'arguments for bob
Dim arglist2(1 To 1) 'arguments for joe
Dim arglist3(1 To M number of arguments for each ith function)
For i = 1 To N
'Execute Application.Run,
'making sure the right number of arguments are passed in somehow.
'It'd also be nice if there was a way to automatically unpack arglisti
Application.Run flist(i) arglisti(1), arglisti(2), arglisti(3), ....
Next i
End Sub
Because the number of arguments changes for each function call, what is the acceptable way to make sure the right number of inputs are input into Application.Run?
The equivalent Python code would be
funclist = ['bob', 'joe', 'etc']
arglists = [[1,2,3],[1,2],[1,2,3,4,5], etc]
for args, funcs in zip(arglists, funclist):
func1 = eval(funcs)
output = func1(*args)
in VBA you use ParamArray to enter option inputs to functions.
See Pearson Material
There are two ways in which a routine can change the number of arguments that has to be provided to it:
declare some of the trailing arguments as Optional
declare the last argument as ParamArray
A single routine can use either or both.
An Optional parameter may have a strict type (e.g. Optional s As String), but then it will be impossible to detect whether it was passed. If you don't pass a value for such argument, the correct flavour of "blank" will be used, which is indistinguishable from passing that blank value manually.
So, having Public Sub Bob(Optional S As String), you cannot detect from inside of Bob whether it was called as Bob or as Bob vbNullString.
An optional parameter may have a default value, which suffers from the same problem. So, having Public Sub Bob(Optional S As String = "Default Value"), you cannot detect if Bob was called as Bob or as Bob "Default Value".
To be able to truly detect whether an optional parameter was passed, they have to be typed as Variant. Then a special function, IsMissing, can be used inside the routine to detect if a parameter was passed.
Public Sub Bob(Optional a, Optional b, Optional c, Optional d)
Debug.Print IsMissing(a), IsMissing(b), IsMissing(c), IsMissing(d)
End Sub
Bob 1, , 3 ' Prints False, True, False, True
ParamArray can only be the last argument, and it allows an infinite* number of arguments to be passed starting from this position. All these arguments arrive packed in a single Variant array (no option for static typing here).
The IsMissing function does not work on the ParamArray argument (always returns False). The way to know how many arguments were passed is to compare UBound(args) with LBound(args). Note that this only tells you how many argument "slots" were used, but some of them can be in fact missing!
Public Sub BobArray(ParamArray a())
Dim i As Long
For i = LBound(a) To UBound(a)
Debug.Print IsMissing(a(i)), ;
Next
Debug.Print
End Sub
BobArray ' Prints empty line (the For loop is not entered due to UBound < LBound)
Sheet1.BobArray 1, 2, 3 ' Prints False, False, False
Sheet1.BobArray 1, , 3 ' Prints False, True, False
Note that you cannot pass "missing" value for the trailing arguments of the ParamArray, i.e. this is illegal:
Sheet1.BobArray 1, , 3, ' Does not compile
However, you can work around this using the trick described below.
An interesting use case that you touch in your question is preparing an array of all arguments in advance, passing it to the function, filling all the arguments "placeholders", but still expecting the function to detect that some of the arguments are missing (not passed).
Normally this is not possible, because if anything is passed (even "blank" values, such as Empty, Null, Nothing of vbNullString), then it still counts as passed, and IsMissing() will return False.
Fortunately, the special Missing value is nothing but a specially constructed Variant, and even without knowing how to construct that value manually, we can trick the compiler to give it away:
Public Function GetMissingValue(Optional ByVal IgnoreMe As Variant) As Variant
If IsMissing(IgnoreMe) Then
GetMissingValue = IgnoreMe
Else
Err.Raise 5, , "I told you to ignore me, didn't I"
End If
End Function
Dim missing As Variant
missing = GetMissingValue()
Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = "!"
Bob arglist1(1), arglist1(2), arglist1(3), arglist1(4) ' Prints False, True, True, False
Now, we can work around the inability to pass "missing" to the trailing "slots" of ParamArray:
Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = missing
BobArray arglist1(1), arglist1(2), arglist1(3), arglist1(4) ' Prints False, True, True, True
Note, however, that this workaround will only work if you call BobArray directly. If you use Application.Run, it will not work because the Run method will discard any trailing "missing" arguments before passing them onto the called routine:
Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = missing
Application.Run "BobArray", arglist1(1), arglist1(2), arglist1(3), arglist1(4)
' Prints False, because only one argument is passed
Further to #GSerg's very comprehensive answer (I don't have enough reputation just to comment), the 'special' value assigned to a Missing argument has the 'appearance' of being an Error value - it converts to "Error 448" (Named argument not found) using CStr(), and responds to IsError() as TRUE. However, an attempt to preset the argument using CvErr(448) before passing to a procedure (in the hope that it will be recognised as Missing) fails, perhaps because the value is 'not quite' the same as the Error value in some way.
#GSerg suggested a method of 'recording' the value actually passed by the compiler when an argument is missing and using that to preset a dummy argument prior to passing to the procedure needing to be fooled. This method, indeed, does work and I have simply extended #GSerg's function to replace his error message (if it is inadvertently called with an argument) by a recursive call without an argument which ensures a successful outcome either way. Usage is simply to preset the dummy variable(s) before passing to a procedure (where it/they will then be treated as missing): Dummy_Var = Missing().
Public Function Missing(Optional ByVal X As Variant) As Variant
If IsMissing(X) Then 'correctly called
Missing = X
Else 'bad user call
Missing = Missing() 'recursive call (no arg!)
End If
End Function
I have just done a quick trial with Application.Run. Early embedded 'missing' arguments (ie, followed by 'normal' ones) appear to be successfully registered as 'missing' in the called procedure. So, too, however, are final trailing 'missing' arguments - whether actually passed by the Run method, or truncated (as suggested by #GSerg), but still filled in by the compiler as genuinely missing.
Interestingly, and usefully (to a niche market), additional 'missing' arguments (beyond those defined by the procedure) appear to be tolerated by the compiler without generating the 'Wrong number of arguments' message associated with extra 'normal' arguments. This opens up the possibility of procedure calls using Application.Run (when a variable number of arguments is desired) being implemented by a single universal call (with up to 30 arguments if necessary) padded out with fake 'missing' arguments instead of having to provided several alternative calls of different lengths and/or argument configurations to cope with exact procedure definitions.
So addressing the question of optionally using arguments it looks like my question in Calling vba macro from python with unknown number of arguments, check it out accordingly.
Hence:
Using Python:
def run_vba_macro(str_path, str_modulename, str_macroname, **kwargs):
if os.path.exists(str_path):
xl=win32com.client.DispatchEx("Excel.Application")
wb=xl.Workbooks.Open(str_path, ReadOnly=0)
xl.Visible = True
if kwargs:
params_for_excel = list(kwargs.values())
xl.Application.Run(os.path.basename(str_path)+"!"+str_modulename+'.'+str_macroname,
*params_for_excel,)
else:
xl.Application.Run(os.path.basename(str_path)
+"!"+str_modulename
+'.'+str_macroname)
wb.Close(SaveChanges=0)
xl.Application.Quit()
del xl
#example
kwargs={'str_file':r'blablab'}
run_vba_macro(r'D:\arch_v14.xlsm',
str_modulename="Module1",
str_macroname='macro1',
**kwargs)
#other example
kwargs={'arg1':1,'arg2':2}
run_vba_macro(r'D:\arch_v14.xlsm',
str_modulename="Module1",
str_macroname='macro_other',
**kwargs)
Using VBA:
Sub macro1(ParamArray args() as Variant)
MsgBox("success the str_file argument was passed as =" & args(0))
End Sub
Sub macro_other(ParamArray args() as Variant)
MsgBox("success the arguments have passed as =" & str(args(0)) & " and " & str(args(1)))
End Sub
Also another use case only using VBA is here for reference. It is a question that has not been answered and is around for long, although recently it was updated by the community server automatically with some good ideas related links accordingly.
Here is an answer you can do it if you use this:
Sub pass_one()
Call flexible("a")
End Sub
Sub pass_other()
Call flexible("a", 2)
End Sub
Sub flexible(ParamArray args() As Variant)
Dim i As Long
MsgBox ("I have received " & _
Str(UBound(args) + 1) & _
" parameters.")
For i = 0 To UBound(args)
MsgBox (TypeName(args(i)))
Next i
End Sub
Only for developers that also use Python:
If you are using Python's kwargs, simply starr expression and pass a Python tuple.
Here it is (it is related with my question in Calling vba macro from python with unknown number of arguments)
Cheers.

Function Overloading and UDF in Excel VBA

I'm using Excel VBA to a write a UDF. I would like to overload my own UDF with a couple of different versions so that different arguments will call different functions.
As VBA doesn't seem to support this, could anyone suggest a good, non-messy way of achieving the same goal? Should I be using Optional arguments or is there a better way?
Declare your arguments as Optional Variants, then you can test to see if they're missing using IsMissing() or check their type using TypeName(), as shown in the following example:
Public Function Foo(Optional v As Variant) As Variant
If IsMissing(v) Then
Foo = "Missing argument"
ElseIf TypeName(v) = "String" Then
Foo = v & " plus one"
Else
Foo = v + 1
End If
End Function
This can be called from a worksheet as =FOO(), =FOO(number), or =FOO("string").
If you can distinguish by parameter count, then something like this would work:
Public Function Morph(ParamArray Args())
Select Case UBound(Args)
Case -1 '' nothing supplied
Morph = Morph_NoParams()
Case 0
Morph = Morph_One_Param(Args(0))
Case 1
Morph = Two_Param_Morph(Args(0), Args(1))
Case Else
Morph = CVErr(xlErrRef)
End Select
End Function
Private Function Morph_NoParams()
Morph_NoParams = "I'm parameterless"
End Function
Private Function Morph_One_Param(arg)
Morph_One_Param = "I has a parameter, it's " & arg
End Function
Private Function Two_Param_Morph(arg0, arg1)
Two_Param_Morph = "I is in 2-params and they is " & arg0 & "," & arg1
End Function
If the only way to distinguish the function is by types, then you're effectively going to have to do what C++ and other languages with overridden functions do, which is to call by signature. I'd suggest making the call look something like this:
Public Function MorphBySig(ParamArray args())
Dim sig As String
Dim idx As Long
Dim MorphInstance As MorphClass
For idx = LBound(args) To UBound(args)
sig = sig & TypeName(args(idx))
Next
Set MorphInstance = New MorphClass
MorphBySig = CallByName(MorphInstance, "Morph_" & sig, VbMethod, args)
End Function
and creating a class with a number of methods that match the signatures you expect. You'll probably need some error-handling though, and be warned that the types that are recognizable are limited: dates are TypeName Double, for example.
VBA is messy. I'm not sure there is an easy way to do fake overloads:
In the past I've either used lots of Optionals, or used varied functions. For instance
Foo_DescriptiveName1()
Foo_DescriptiveName2()
I'd say go with Optional arguments that have sensible defaults unless the argument list is going to get stupid, then create separate functions to call for your cases.
You mighta also want to consider using a variant data type for your arguments list and then figure out what's what type using the TypeOf statement, and then call the appropriate functions when you figure out what's what...

Resources