Function argument declaration without type - excel

For a class, I have stumbled upon this VBA code for an excel sheet. It shows the implementation of a function, where the parameter has not been declared to be of any type. When it is called, a String is used as this parameter.
Option Explicit
' The weird function
Function isXorEmpty(testvalue) As Boolean
If testvalue = "x" Or testvalue = "" Then
isXorEmpty = True
Else
isXorEmpty = False
End If
End Function
' Button click handler
Sub verify_input_conformity_Click()
Dim testvalue As String
testvalue = Sheets(0).Cells(0, SOME_COLUMN_CONSTANT)
If Not isXorEmpty(testvalue) Then
Call SomeFunction()
End If
End Sub
The code works correctly within Excel, i.e., it detects incorrect input values.
I have searched the internet but have come to no conclusion what this language feature of VBA is called, and how exactly it behaves when a argument type is left out like that. Can someone point me to documentation of this?

Function isXorEmpty(testvalue) As Boolean
This is implicitly equivalent to this:
Public Function isXorEmpty(ByRef testvalue As Variant) As Boolean
The Variant is implicitly converted to Variant/String (twice) for the two string comparisons; a run-time error could happen if anything other than a String (actually, anything that can't be implicitly converted to one) is passed through testValue.
Moreover, the implicit ByRef means it's possible for isXorEmpty to re-assign that parameter in its body, and thereby introduce subtle bugs in the calling code.
A better signature would be:
Public Function isXorEmpty(ByVal testvalue As String) As Boolean
That said the condition body is assigning a Boolean literal in both conditional branches - it can be rephrased as follows:
isXorEmpty = (testvalue = "x" Or testvalue = "")

Related

What happens if function is called and parameter's type is ommited [duplicate]

For a class, I have stumbled upon this VBA code for an excel sheet. It shows the implementation of a function, where the parameter has not been declared to be of any type. When it is called, a String is used as this parameter.
Option Explicit
' The weird function
Function isXorEmpty(testvalue) As Boolean
If testvalue = "x" Or testvalue = "" Then
isXorEmpty = True
Else
isXorEmpty = False
End If
End Function
' Button click handler
Sub verify_input_conformity_Click()
Dim testvalue As String
testvalue = Sheets(0).Cells(0, SOME_COLUMN_CONSTANT)
If Not isXorEmpty(testvalue) Then
Call SomeFunction()
End If
End Sub
The code works correctly within Excel, i.e., it detects incorrect input values.
I have searched the internet but have come to no conclusion what this language feature of VBA is called, and how exactly it behaves when a argument type is left out like that. Can someone point me to documentation of this?
Function isXorEmpty(testvalue) As Boolean
This is implicitly equivalent to this:
Public Function isXorEmpty(ByRef testvalue As Variant) As Boolean
The Variant is implicitly converted to Variant/String (twice) for the two string comparisons; a run-time error could happen if anything other than a String (actually, anything that can't be implicitly converted to one) is passed through testValue.
Moreover, the implicit ByRef means it's possible for isXorEmpty to re-assign that parameter in its body, and thereby introduce subtle bugs in the calling code.
A better signature would be:
Public Function isXorEmpty(ByVal testvalue As String) As Boolean
That said the condition body is assigning a Boolean literal in both conditional branches - it can be rephrased as follows:
isXorEmpty = (testvalue = "x" Or testvalue = "")

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.

How to make VBA function "VBA only" and disable it as UDF

I'm writing a VBA function which I want to be publically available in other VBA modules within the same document, however I don't want it to be available as a UDF (User defined function).
If I use the public access modifier however my function is also be available as a UDF-formula that can be called from the cells within the workbook. I don't want this.
Is there an access modifier or other way that can help me obtain this "VBA only" behaviour?
Kind regards
If you use Option Private Module in the module in which the function appears, the function can be declared as Public and used in any of your other modules within your VBA project, but won't be accessible by other applications or projects, including Excel itself.
This will return a #VALUE error if used in Excel.
Function VBAOnly() As Variant
If TypeName(Application.Caller) <> "Range" Then
VBAOnly = 1 'or some other return value
Else
VBAOnly = CVErr(xlErrValue)
End If
End Function
There's a little bit of confusion among answers, so here's a more encompassing explanation:
Technically... all functions in the standard VBA Modules can be called if the exact formula is entered. Even Formulas in Private Functions within Private Modules
Example
Option Private Module
Private Function hiddenEverythingExample() As String
hiddenEverythingExample= "NOPE!!!"
End Function
Will still return a value if a cell has =hiddenEverythingExample()
However, I believe the objective of the OP was to avoid having the itellisense populate these functions in the Excel Formula bar.
The most frequent way I accomplish this is create a specific Module for all VBA-Only functions and put Option Private Module in the module definition (area above the functions).
This ensures all functions in this module will NOT appear in itellisense, but still be accessible to other modules along with itellisense.
Defining asPrivate Function also accomplishes this, but then the function is scoped exclusively to that Module, which may or may not be the requirement.
Note that YowE3K infers that a function must be both Private Function AND Option Private Module, but only one OR the other is necessary to eliminate the itellisense.
Instead of writing a Function, write a Sub, and set the return via a ByRef argument. This way your function will be invisible to Excel (except via Alt F8, or Developer tab > Macros) and won't appear in Excel's intellisense.
Instead of
Function Add(Num1 As Double, Num2 As Double)
Add = Num1 + Num2
End Function
use
Sub AddInvisible(ByRef Result As Double, Num1 As Double, Num2 As Double)
Result = Num1 + Num2
End Sub
Note
That ByRef is not strictly necessary (since arguments are by default by reference in VBA) but it serves as a useful reminder that Result carries the return value.
You will need to make the necessary changes to your code, for example:
z = Add(x,y)
would become
AddInvisible z,x,y
as demonstrated below:
Sub DemoAddInvisible()
Dim Num1 As Double
Dim Num2 As Double
Dim Result As Double 'Result initialises to 0
Num1 = 1
Num2 = 2
AddInvisible Result, Num1, Num2
MsgBox Result ' See that Result has become 3
End Sub
A downside of all this is that the new code is somewhat harder to understand.
pass a parameter that only allows the function to run if a "magic* value is given to it.
Example - This will give the error #NAME! unless you know what the key is:
Function VBAOnly(key As Long)
If key <> 12345 Then
VBAOnly = CVErr(xlErrName)
Exit Function
End If
VBAOnly = True
End Function
Using the Private modifier should only allow the execution in the module the function exists in.

Returning user-defined types on Excel VBA

I've been fiddling with Excel VBA for some time, but I never had any formal training whatsoever. I have the following piece of code on a XLSM module :
Public Type ks_solution
W As Integer
NF As String
ID As Integer
End Type
Public Sub MySub()
//does some things
MyKSSolution = MyFunction(params)
End Sub
Public Function MyFunction(params) as ks_solution
//does stuff and returns a ks_solution
End Function
When I try to run the code, VBA compiler returns a "Only user-defined types defined in public object modules can be coerced to or from a variant or passed to late-bound functions" error.
Any help would be greatly appreciated.
--Yuri
Er, nevermind. Looks like the problem was in the var declarations.
I thought
Dim v1, v2 as ks_solution
was the same as
Dim v1 as ks_solution, v2 as ks_solution
but apparently, it isn't. In the first case, v1 gets declared as a Variant. Sorry to take your time.

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