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

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

Related

When is it suitable to use a Sub-Procedure instead of a function?

In my class today I was told change some of my sub-procedures to functions, and when I asked why it's better my teacher struggled to answer, generally, i've always thought that functions should only really be used when a value is returned. In the two examples below; is there one method that should be used over the other, or does it not matter? And if it does matter why?
Thanks in advance.
Method 1 (Sub-Proc):
Sub EncryptString(ByVal unkString, ByRef encryptedString)
For i = 1 To Len(unkString)
encryptedString += "*"
Next
End Sub
Method 2 (Function):
[In main I assign the variable "encryptedString" to this function].
Function encryptString(ByVal unkString) As String
For i = 1 To Len(unkString)
encryptString += "*"
Next
End Function
You've misunderstood what they're trying to tell you. In your Function example there is no difference. What your teacher is expecting is like this:
Function EncryptString(ByVal unkString) As String
Dim encryptedString As String = ""
For i = 1 To Len(unkString)
encryptedString += "*"
Next
Return encryptedString
End Function
This is a cleaner and more reusable way than modifying a field, an argument passed ByRef, or the underlying variable of the function
Your example show one of the multiple reason, who initialize the data is unclear. With your sample code, the first option would append to the passed string while the second would create a new string.
The first method would have to specify if it needs an empty string or explain why it appends. While the second method clearly show that a new string will be returned.
Sub Main()
Dim u, e As String
u = "123"
e = "123"
EncryptString1(u, e)
Console.WriteLine(e) ' Display: 123***
u = "123"
e = "123"
e = encryptString2(u)
Console.WriteLine(e) ' Display: ***
Console.ReadLine()
End Sub
Sub EncryptString1(ByVal unkString As String, ByRef encryptedString As String)
For i As Integer = 1 To Len(unkString)
encryptedString += "*"
Next
End Sub
Function encryptString2(ByVal unkString As String) As String
encryptString2 = ""
For i As Integer = 1 To Len(unkString)
encryptString2 += "*"
Next
End Function
Please have option strict on. Also, personally, I rather create a variable instead of using the function name, use .Length instead of Len() and concatenate with & instead of +.
Function encryptString3(ByVal unkString As String) As String
Dim encryptedString As String = ""
For i As Integer = 1 To unkString.Length
encryptedString &= "*"
Next
Return encryptedString
End Function
Or just use the New operator of the String class.
Dim encryptedString as New String("*"c, unkString.Length)
Well, when I was learning this stuff, it was always to use functions to calculate values and subs to do other stuff. I guess for something very general, it doesn't really matter which methodology you use, as you have illustrated in your example. See the link below for further discussion on this topic.
http://analystcave.com/vba-function-vs-vba-sub-procedures/

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.

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

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.

Basic VB troubles

I'm simply wondering what symbol/character I can use to define any character in a string...
Basically I have a number of records with RR 2, RR#2, RR1, RR 1, etc. and I want to use a symbol that will define anything after the RR and replace it with nothing "". I know in SQL it's the "%" symbol, but not sure in VBA.
I am using the Replace function in ArcGIS field calculator.
I tried searching but cannot come up with the right question to find the answer I'm looking for.
Any ideas?
Since it's unclear if you want VBA or VB.Net,
Here's a VBA answer just use the ChopString function using the format shown in the Test sub:
Function ChopString(str As String, after As String, Optional caseInsensitive As Boolean = True) As String
Dim x As Long
If caseInsensitive Then
x = InStr(1, str, after, vbTextCompare)
Else
x = InStr(1, str, after, vbBinaryCompare)
End If
If x Then
str = Left(str, x + Len(after) - 1)
End If
ChopString = str
End Function
Sub Test()
Dim OriginalString As String
Dim choppedString As String
OriginalString = "1234RR this will be chopped"
choppedString = ChopString(OriginalString, "RR")
MsgBox choppedString
End Sub
Sadly the .net REPLACE() function doesn't support wildcard characters, you can use a function as described here but it's a bit long winded.