Why does my function assume a missing argument is there? - vba

I have a function which updates a form, "LoadingInterface". The function looks like this:
Private Sub updateLoadingBar(Optional tekst As String, Optional barOnePerc As Long, Optional barTwoPerc As Long)
If Not IsMissing(tekst) Then
LoadingInterface.Label1.Caption = tekst
End If
If Not IsMissing(barOnePerc) Then
LoadingInterface.Bar.Width = barOnePerc * 1.68
LoadingInterface.prosent.Caption = barOnePerc & "%"
LoadingInterface.prosent.Left = barOnePerc * 1.68 / 2 - 6
End If
If Not IsMissing(barTwoPerc) Then
LoadingInterface.SubBar.Width = barTwoPerc * 1.68
End If
LoadingInterface.Repaint
End Sub
I then call the function like this, expecting it to only update the textfield, since the other two arguments are missing.
Call updateLoadingBar(tekst:="Test")
This works fine for updating Label1, but unfortunately the other two values are updated too - it seems that not including any values in the function-call makes VBA assume the two variables values are 0. What's more, it appears that the IsMissing function does not detect that the two values are missing when the function is called, which is the bigger problem. Stepping through the code using F8 confirms that all the if-statements are indeed entered.
Is there any way to make the code skip the two lowermost if-statements in my function, if no values are provided for the parameters barOnePerc and barTwoPerc?

IsMissing only works if the argument is declared as a Variant.
I don't think you can validly distinguish between 0 and no passed parameter for the Long. In that case you would need to declare as Variant in the signature. You can later cast if required.
I guess you could put a default (unlikely number) and test for that. Note: I wouldn't advise this. This just screams "Bug".
IsMissing:
IsMissing returns a Boolean value indicating whether an optional Variant argument has been passed to a procedure.
Syntax: IsMissing(argname)
The required argname argument contains the name of an optional Variant
procedure argument.
Remarks: Use the IsMissing function to detect
whether or not optional Variant arguments have been provided in
calling a procedure. IsMissing returns True if no value has been
passed for the specified argument; otherwise, it returns False.
Both methods:
Option Explicit
Public Sub Test()
RetVal
RetVal2
End Sub
Public Function RetVal(Optional ByVal num As Long = 1000000) As Long
If num = 1000000 Then
MsgBox "No value passed"
RetVal = num
Else
MsgBox "Value passed " & num
RetVal = num
End If
End Function
Public Function RetVal2(Optional ByVal num As Variant) As Long
If IsMissing(num) Then
MsgBox "No value passed"
Else
MsgBox "Value passed " & num
RetVal2 = CLng(num)
End If
End Function

Related

Is there a VBA equivalent (or way to replicate) passing parameters as 'Out' like C#?

I generally use VBA but have been reading up on programming techniques in The C# Programming Yellow Book which, obviously, is more specific to C#. Anyway, it mentions a technique of passing parameters using the Out keyword.
I already know that VBA supports byVal and byRef and am fairly certain there is no direct equivalent for Out. Passing parameters using Out is subtly different to passing parameters by Ref.
This Answer https://stackoverflow.com/a/388781/3451115 seems to give a good explanation of the difference between Out & Ref.
The Ref modifier means that:
The value is already set and
The method can read and modify it.
The Out modifier means that:
The Value isn't set and can't be read by the method until it is set.
The method must set it before returning.
In the code base that I've inherited there are several places where values are assigned to variables using methods that accept parameters byRef. It seems to me that while passing byRef does the job, passing by Out would be safer... So (and here is the question) is there a way of safely / reliably replicating Out in VBA?
In my first iteration (original question) I imagined that the code would have a pattern like:
Sub byOutExample(byRef foo As String)
' Check before running code:
' foo must = vbNullString
If foo <> vbNullString then Err.Raise(someError)
' Do Something to assign foo
foo = someString
' Check before exiting:
' foo must <> vbNullString
If foo = vbNullString then Err.Raise(someError)
End Sub
Other considerations: is it worth doing, is there a better way, what could go wrong?
Edit: I noticed in the comments for the above definition of Ref vs Out that the passed parameter need not be null, nothing, empty etc. it can be preassigned - the main criteria seems that it is re-assigned.
In light of #ThunderFrame's answer below and the comment that a parameter passed by Out can be pre-assigned (and used), perhaps the following is a better approach:
Sub byOutExample(ByRef foo As String)
Dim barTemp As String
barTemp = foo
' Do Something to assign a new value to foo (via barTemp)
barTemp = someString
' Must assign new variable
foo = barTemp
End Sub
In which case would it be true to say that, as long as foo only appears in the 2 locations shown above, the above code is an accurate way to replicate passing a parameter by Out in VBA?
The answer is unequivocally 'no' you cannot replicate the C# out parameter modifier in VBA. From https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-parameter-modifier:
Variables passed as out arguments do not have to be initialized before
being passed in a method call. However, the called method is required
to assign a value before the method returns.
These aspects simply don't exist in VBA. All variables in VBA are initialised with default values, ie the concept of an unitialised variable does not exist in VBA, so point 1 isn't possible; and the compiler cannot object if a specified parameter has not had a value assigned within the procedure, so point 2 isn't possible either.
Even the coding patterns in your example would rely on the Do Something to assign foo not to resolve to the relevant data type's default value (which is obviously not the same as being unitialised). The following, for example, would wrongly throw an error:
Public Sub Main()
Dim income As Long, costs As Long
Dim result As Long
income = 1000
costs = 500
ProcessSpend income, costs, result
End Sub
Private Sub ProcessSpend(income As Long, costs As Long, ByRef outValue As Long)
Const TAX_RATE As Long = 2
Dim netCosts As Long
Dim vbDefaultValue As Long
netCosts = costs * TAX_RATE
outValue = income - netCosts
If outValue = vbDefaultValue Then Err.Raise 5, , "Unassigned value"
End Sub
So we're really left with the question of is there a way of getting close to the characteristics of out in VBA?
Unitialised variables: the closest I can think of are a Variant or Object type which by default initialise to Empty and Nothing respectively.
Assign value within the procedure: the simplest way would be to test if the address of the assigning procedure matches your desired procedure address.
It's all leaning towards a helper class:
Option Explicit
Private mNumber As Long
Private mTargetProc As LongPtr
Private mAssignedInProc As Boolean
Public Sub SetTargetProc(targetProc As LongPtr)
mTargetProc = targetProc
End Sub
Public Sub SetNumber(currentProc As LongPtr, val As Long)
mAssignedInProc = (currentProc = mTargetProc)
mNumber = val
End Sub
Public Property Get Number() As Long
If mAssignedInProc Then
Number = mNumber
Else
Err.Raise 5, , "Unassigned value"
End If
End Property
And then the previous example would look like this:
Public Sub Main()
Dim income As Long, costs As Long
Dim result As clsOut
income = 1000
costs = 500
ProcessSpend income, costs, result
Debug.Print result.Number
End Sub
Private Sub ProcessSpend(income As Long, costs As Long, outValue As clsOut)
Const TAX_RATE As Long = 2
Dim netCosts As Long
If outValue Is Nothing Then
Set outValue = New clsOut
End If
outValue.SetTargetProc AddressOf ProcessSpend
netCosts = costs * TAX_RATE
outValue.SetNumber AddressOf ProcessSpend, income - netCosts
End Sub
But that's all getting very onerous... and it really feels as if we are trying to force another language's syntax onto VBA. Stepping back a little from the out characteristics and developing in a syntax for which VBA was designed, then a function which returns a Variant seems the most obvious way to go. You could test if you forgot to set the 'out' value by checking if the function returns an Empty variant (which suits point 1 and 2 of the out characteristics):
Public Sub Main()
Dim income As Long, costs As Long
Dim result As Variant
income = 1000
costs = 500
result = ProcessedSpend(income, costs)
If IsEmpty(result) Then Err.Raise 5, , "Unassigned value"
End Sub
Private Function ProcessedSpend(income As Long, costs As Long) As Variant
Const TAX_RATE As Long = 2
Dim netCosts As Long
netCosts = costs * TAX_RATE
'Comment out the line below to throw the unassigned error
ProcessedSpend = income - netCosts
End Function
And if you wanted the option of passing in a pre-assigned value, then could just define an optional argument as a parameter to the function.
You can pseudo enforce an out type parameter in VBA by passing it in ByRef, and then checking that it is Nothing (or the default value for a value type) before continuing, much as you have done with the String in your example.
I wouldn't impose the exit condition - sometimes an empty string is a perfectly valid return value, as is a Nothing reference.

VBA Call method with optional parameters

I just figured out that setting optional parameters requires "Call" infront of the method.
Public Sub Test()
Call abc("aaa")
Call abc("aaa", 2)
abc("aaa") ' is fine
abc("aaa", 2) ' is a syntax error
End Sub
Function abc(a As String, Optional iCol As Long = 3)
MsgBox (iCol)
End Function
Can you add a "why does this make sense?" to my new information?
Greetings,
Peter
Edit: PS the function abc for no other use than to simplify the question.
Documentation
Call is an optional keyword, but the one caveat is that if you use it you must include the parentheses around the arguments, but if you omit it you must not include the parentheses.
Quote from MSDN:
You are not required to use the Call keyword when calling a procedure.
However, if you use the Call keyword to call a procedure that requires arguments, argumentlist must be enclosed in parentheses. If you omit the Call keyword, you also must omit the parentheses around argumentlist. If you use either Call syntax to call any intrinsic or user-defined function, the function's return value is discarded.
To pass a whole array to a procedure, use the array name followed by empty parentheses.
Link: https://msdn.microsoft.com/en-us/library/office/gg251710.aspx
In Practice
This means that the following syntaxes are allowed:
Call abc("aaa")
Call abc("aaa", 2)
abc "aaa", 2
abc("aaa") ' <- Parantheses here do not create an argument list
abc(((("aaa")))) ' <- Parantheses here do not create an argument list
The following syntaxes are not allowed:
Call abc "aaa", 2
abc("aaa", 2) ' <- Parantheses here create an argument list
Function Return Values
This doesn't take effect when using a function to get a return value, for example if you were to do the following you need parentheses:
Function abc(a As String, Optional iCol As Long = 3)
abc = iCol
End Function
'## IMMEDIATE WINDOW ##
?abc("aaa", 2) 'this works
?abc "aaa, 2 'this will not work
?Call abc "aaa", 2 'this will not work
?Call abc("aaa", 2) 'this will not work
If you are using Call on a Function then consider changing it to a Sub instead, functions are meant to return a value as in the cases above.
Something like this would work:
Option Explicit
Public Sub Test()
Dim strText As String
strText = "E"
Call Abc(strText, 3)
Call Abc(strText, 2)
Abc (strText)
' Abc (strtext,5)
Abc strText, 2
Abc strText
End Sub
Public Sub Abc(strText As String, Optional iCol As Long = 5)
Debug.Print iCol
End Sub
Absolutely no idea why the commented code does not work...
Call is an optional keyword, as already described in detailed in the answer above.
for your second option
abc("aaa", 2) ' is a syntax error
Just use:
abc "aaa", 2
Note: there is little use to have a Function if you don't return anything, you could have a regular Sub instead.
to have a Function that return a String for instance (just something made-up quick):
Function abc(a As String, Optional iCol As Long = 3) As String
abc = a & CStr(iCol)
End Function
and then call it:
Public Sub Test()
d = abc("aaa", 2)
MsgBox d
End Sub

VBA ByRef Argument Type Mismatch When Using Function In If Statement

I am having a weird issue when I'm calling a function from another function inside an if statement. I defined a Sub, which I am using to test this function, and it calls on a function which relies on another function I use to compare values. The code is below, which should make things clear. Essentially, I don't understand how it's possible for the code to work fine in the print statement, and then throw an error in the GetMatch function. I appreciate any help.
Edit: All of a sudden everything works. Do debugging breakpoints affect the program? I haven't changed anything, but CStr() is no longer required when calling GetMatch. I haven't touched any of the subs or functions, but I did clear some breakpoints. If I find what caused it, I'll post a solution. Thanks for the help everyone.
Edit2: Maybe this is a bug with VBA? If I add the CStr() option to the indexOrder(...) calls, things work. Before, without the CStr() options, things did not work. Now, strangely enough, after using the CStr(), I am able to remove the CStr()'s entirely from the program, and things work again. It breaks if I undo to the point where they weren't there originally though. I don't know what this could be, but if anyone has an explanation, I'm very interested. Thanks
Sub testFind()
Dim SortOrder() As Variant
Dim indexOrder() As Variant
SortOrder = Array("Contact Email", "Last Name", "First Name", "Attempt #", "Customization", "Template #")
indexOrder = Array("First Name", "First Name", "Template #", "Customization")
findAndReplace(indexOrder, SortOrder)
End Sub
Function findAndReplace(indexOrder As Variant, list As Variant) As Variant
Dim indexLength As Integer
Dim listLength As Integer
Debug.Print TypeName(indexOrder(0)) ' Identifies as String
indexLength = getVariantLength(indexOrder)
listLength = getVariantLength(list)
Debug.Print GetMatch(CStr(indexOrder(1)), CStr(indexOrder(1))) ' This works fine. Returns 0 as it should
If GetMatch(indexOrder(1), indexOrder(1)) = 0 Then ' Fails with ByRef error
Debug.Print ("Why don't I work?")
End If
End Function
Function GetMatch(A As String, B As String) As Integer
A = Trim(A)
B = Trim(B)
If (IsEmpty(A) Or Trim(A) = "") Then
GetMatch = 1
Exit Function
ElseIf (IsEmpty(B) Or Trim(B) = "") Then
GetMatch = -1
Exit Function
End If
GetMatch = StrComp(A, B, vbTextCompare)
End Function
Function getVariantLength(vari As Variant) As Integer
If IsNull(index) Then
getVariantLength = 0
Else
getVariantLength = UBound(vari) - LBound(vari) + 1
End If
End Function
You don't have a Sub vartest() or Function vartest(), it's trying to call a sub/function that doesn't exist, or at least it isn't included.
Edit: You aren't doing anything with the function. A function will return a value, a sub will 'do' something. You need to assign a variable to whatever it returns, or do a MessageBox or some other way of returning the value.
The next issue is it's trying to call a function getVariantLength() that isn't defined or listed.

Passing variants/arrays as arguments for a function

Example code;
Sub functiontester()
Dim testdata As Variant
Dim answer As Long
Dim result1 As Long
testdata = Sheets("worksheet1").Range("E1:E2").Value
result1 = testfunction(testdata)
result2 = testfunction2(testdata(2, 1))
End Sub
Function testfunction(stuff As Variant) As Long
testfunction = stuff(2, 1)
End Function
Function testfunction2(num As Long) As Long
testfunction2 = num
End Function
So from my days in python I'd expect result1 & result2 to both run fine, however this is not the case in VBA and if you try to run this you get
"compile error: Byref argument type mismatch" from result2; which I assume has something to do with limits of calculating values inside arguments of functions
So my question is: is there an easy way to make result2 work so that the variant reference just resolves to the specified element?
testdata(2, 1) likely will be of type Double, not Long.
You can use
CLng(testdata(2, 1))
to cast it to a Long.
So:
result2 = testfunction2(CLng(testdata(2, 1)))
should be fine
"compile error: Byref argument type mismatch" Actually refers to the fact it won't implicitly convert the datatype for you, because ByRef arguments (the default) are expected to be writeable. If converted arguments are written to , it gets lost when you return from the function/subroutine because the converted values are only temporary, they're not in any variable outside of the called function.
You can get around this complaint by making the receiving parameter ByValwhich means that it shouldn't be writable anyway:
Function testfunction2(ByVal num As Long) As Long

Having an optional parameter in a function

Just a very quick question: I want to create a function with an optional parameter because I can't find a need for a parameter in the function. As a result I have coded the following function in visual basic:
Sub characterListLength(ByVal Optional)
Dim rowCount As Integer
Dim endOfArray As Boolean
While endOfArray = False
If dataArray(0, rowCount) And dataArray(1, rowCount) = "" Then
arrayLength = rowCount
endOfArray = True
Else
rowCount += 1
End If
End While
End Sub
However on the first line:
Sub characterListLength(ByVal Optional)
There is an error where an identifier is expected where the code says (ByVal Optional). I am not sure how to fix this error and have the optional parameter. If anyone could explain what else I need to do to fix it, that would be very useful.
You need an actual variable, something like:
Sub characterListLength(Optional ByVal optionalNumber As Integer = 0)
If you said:
because I can't find a need for a parameter in the function
Then use method without parameters:
Sub characterListLength()
'Here your code
End Sub
You need to give the parameter a name and switch the order of the keywords
Sub characterListLength(Optional ByVal p = Nothing)
A better "dot-nettier" alternative to optional parameters is to use overloaded methods. Consider following:
Overloads Sub ShowMessage()
ShowMessage("This is the default alter message")
End Sub
Overloads Sub ShowMessage(ByVal Message As String)
Console.WriteLine(Message)
End Sub
Written like this you can call the above method both ways:
ShowMessage() 'will display default message
ShowMessage("This is custom message") 'will display method from the parameter
Demo: http://dotnetfiddle.net/OOi26i