VBA Function Passing Multi Variables back to Sub - vba

I have a large string over 500 char which is called strEssay. I want to use a function(since I will need to look for several patterns) to return two values if (for example the name) Frank is found or not.
This is the function I'm trying to use:
Function NameFinder(strEssay as String, strName as String)
Dim varNameCounter as Variant
Dim strNameFinderResult as String
varNameCounter = 0
strNameFinderResult = ""
If strEssay like "*" & strName & "*" Then
strNameFinderResult = strName
varNameFinderCounter = 1
Else
strNameFinderResult = ""
varNameFinderCounter = .001
EndIf
End Function
I want to be able to return back to my subroutine both 'strNameFinderResult' and 'varNameFinderCounter'.
Is there any way that I can return both values?
If I can't return both simultaneously can I return one through the function and the other through a textbox or something? What would calling the function look like in the subroutine and/or how would I need to change my function?

NameFinder() function, returning array of 3 elements. It is called and returned by TestMe(), writing the following to the console:
Function NameFinder(essay As String, name As String)
Dim nameFinderResult As String
Dim namefinderCounter As String
nameFinderResult = "" & essay & name
namefinderCounter = 0.001 + 12
NameFinder = Array(nameFinderResult, namefinderCounter, "something else")
End Function
Public Sub TestMe()
Dim myArray As Variant
myArray = NameFinder("foo", "bar")
Dim i As Long
For i = LBound(myArray) To UBound(myArray)
Debug.Print myArray(i)
Next i
End Sub

As a general rule, you have to give the routine a type like this:
Function NameFinder(strEssay as String, strName as String) as string
But, that returns only ONE value.
So, a function (as opposed to a sub) returns one value (as a general rule).
However, you CAN also return parameters that you pass. I mean, in above, you can't make TWO assignments to one variable, can you?
So, you can use a Sub like this:
Sub NameFinder(strEssay as String, strName as String, _
strNameFinderResult as string, _
varNameFinderCounter as double)
If strEssay like "*" & strName & "*" Then
strNameFinderResult = strName
varNameFinderCounter = 1
Else
strNameFinderResult = ""
varNameFinderCounter = .001
EndIf
So in code, you now can go:
dim strMyResult as string
dim strFinderCount as Double
Call NameFinder("MyEassy", "Joe Blow", strMyResult, strFinderCount)
So, you can return values with the parameters.
Now, I suppose it possible for some strange reason, that you want to use a function to return two values with a single assignment?
What you would do is this in your code module.
Define a custom type, and use that.
eg this:
Option Compare Database
Option Explicit
Type SearchResult
strName As String
FindCount As Double
End Type
Function NameFinder(strEssay As String, strName As String) As SearchResult
NameFinder.FindCount = 0
NameFinder.strName = ""
If strEssay Like "*" & strName & "*" Then
NameFinder.strName = strName
NameFinder.FindCount = 1
Else
NameFinder.strName = ""
NameFinder.FindCount = 0.001
End If
End Function
So, now to use in code? You can go like this:
dim MyResults as SearchResult
MyResults = NameFinder("My eassy", "Joe Blow")
debug.print "Name found result = " & MyResults.strName
debug.print "Count of find = " & MyResult.FindCount
The VERY nice thing about above is you get full intel-sense in your code editor.
eg this:
So by building a custom data type, you can use "one" assignment for the return type. And you get nice type checking and inteli-sense in the VBA code editor.
And you can even do this:
But, to get both variables, then you would in theory wind up calling the function two times. So, you can actually use the function without declarer of variables like this:
Debug.Print NameFinder("MyEassy", "Joe blow").strName
Debug.Print NameFinder("MyEassy", "Joe blow").FindCount
So, I don't recommend the above, but in the case in which you ONLY want one of the return values, then the raw expression (function) like above would be a use case (and no need to even declare a return variable).
But, without a doubt, define a custom type in code as per above. The reason is now you get a really nice VBA editor type-checking, inteli-sense, and also that you only have to declare "one" variable that holds two values.
In fact, the results are very much like JavaScript, or even c# in which you declare a "class" type. So with a custom "type" you are declaring a data type of your own. And the beauty of this is if you need say 3 values, then once again you create a type with 3 "inside" values.
The you ONLY have to declare that one variable as the custom type.
With this you get:
Very valuable compile time syntax and data type checking of the var types you are using.
You get GREAT VBA inteli-sense while coding - which means less coding mistakes.
And you type far less typing in the VBA editor as it will pop-up the choices for you as you write code. And you can't type or choose the wrong sub - type, as the compiler will catch this.

Related

VB.Net | Is there a way to reference a dynamic amount of variables as arguments to function/sub?

I'm trying to pass a dynamic amount of variables to a Sub by using ByRef;
Essentially I'm trying to create a module that I can easily import into my projects and make handling the file saving/loading process automated.
The Sub/Function would take a number of variables as references and then loop through them changing each one's value.
I realize I'm missing a crucial point in how visual basic's syntax works but I haven't been able to figure out what I need to do.
The code I've written for this is:
Public Sub LoadSaveToVars(ByRef KeyNamesAndVars() As Object, ByVal FileLoc As String = "")
If isEven(KeyNamesAndVars.Length) Then
Dim Contents As String = My.Computer.FileSystem.ReadAllText(FileLoc)
Dim isOnName As Boolean = True
Dim CurrentVal As String = ""
For i = 0 To KeyNamesAndVars.Length - 1
If isOnName Then
CurrentVal = GetStringValue(KeyNamesAndVars(i), Contents) 'Get the value of the key with the key name in the array
isOnName = False
Else
KeyNamesAndVars(i) = CurrentVal 'Set the variable referenced in the array to the value
isOnName = True
End If
Next
Else
Throw New ArgumentOutOfRangeException("The key names and variables supplied are not even.", "Error loading to variables!")
End If
End Sub
And here's how I try to use this function:
Dim TestVar1 As String = ""
Dim TestVar2 As String = ""
LoadSaveToVars({"key1", TestVar1, "key2", TestVar2})
To keep this question clean I did not include the other functions, but I did make a poor attempt at drawing what I want to happen: https://gyazo.com/eee34b8dff766401f73772bb0fef981a
In the end, I want TestVar1 to be equal to "val1" and TestVar2 to be equal to "val2" and to be able to extend this to a dynamic number of variables. Is this possible?

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/

VB: Referencing a variable by name in a conditional

I am an amateur programmer and in working with a friend and cannot find a solution to our coding dilemma.
We need to be able to compare if a Variable matches data found in second variable, but the first Variable to be searched is to be dependent on the contents of a third variable. (The third variable would name the first variable to be searched)
Var1, Var2, Var3 ... Var100 'Each with their own values and datatypes;
Var45 = 25
Vartocheck1 = "Var45"
Vattocheck2 = 25
If Vartocheck1 = Vartocheck2 Then
(Stuff)
End If
Essentially, I was wondering if there was a good way to compare two variables, most likely in an If-then statement, where one of the two variables is decided by a third variable's contents.
The idea is Vartocheck1 would be a string, containing the NAME of the variable whose value I want to check against Vartocheck2. The issue is that the variables in the code (in my example: Var1, Var2, Var3 ... Var100) are defined as the process runs, but an external excel chart, when referenced, can change certain variables during the program's execution. I could accomplish what I need with about a million nested if-then statements, but that is slow and messy, and I am hoping there is another way.
I have looked into arrays, but implementing the massive number and size of the arrays would be daunting and require an entire project rewrite.
Is there any good method for comparing a variable like this?
What you are looking for is a concept called reflection. This SO question explains it:
How to get the variable names types, and values in the current class or Method in VB.net?
Based on that I have quickly created the following class:
Public Class Class1
Public This As String
Public That As Boolean
Public Function ListVar() As Boolean
Dim fields As System.Reflection.FieldInfo() = Me.GetType().GetFields()
For Each fld As System.Reflection.FieldInfo In fields
Dim name As String = fld.Name
Dim value = fld.GetValue(Me)
Dim typ As Type = fld.FieldType
Debug.Print(name)
Next
Return True
End Function
End Class
You can call the ListVar function from anywhere by doing this:
Dim c As New Class1
c.ListVar()
Obviously this is not production ready, but should give you a start.
While I still think this is an XY problem, one convenient container in .net is a dictionary. This allows you to store key-value pairs which can be of any type. This gives you some of the tools you would get with a database (which may be a better solution in this case). For example :
Imports System.Collections.Generic
Module Module1
Dim ValueDict As New Dictionary(Of String, Integer)
Sub Main()
Dim r As New Random
'Fill the dictionary with keys "Var1" -> "Var100"
'Fill the values with random integers
For i As Integer = 1 To 100
ValueDict.Add("Var" & i.ToString, r.Next)
Next
'Extract a variable by name
Dim extractedVar As Integer
If ValueDict.TryGetValue("Var23", extractedVar) Then
Console.WriteLine("Var23 has value :" & extractedVar.ToString())
Else
Console.WriteLine("Var23 does not exist in the dictionary")
End If
'enumerate all values
For Each valuePair As KeyValuePair(Of String, Integer) In ValueDict
Console.WriteLine("Variable " & valuePair.Key & _
" = " & valuePair.Value.ToString())
Next
'Get a variable by number
Dim varNumber As Integer = 72
If ValueDict.TryGetValue("Var" & varNumber.ToString(), extractedVar) Then
Console.WriteLine("Var" & varNumber.ToString() & _
" has value :" & extractedVar.ToString())
Else
Console.WriteLine("Var" & varNumber.ToString() & _
" does not exist in the dictionary")
End If
Console.ReadLine()
End Sub
End Module
Other types of operations :
'Check if value exists, Assign a new value or update an existing value
Dim newVal As Integer = 12345
Dim varName As String = "Var147"
If Not ValueDict.ContainsKey(varName) Then
Console.WriteLine(varName & " does not currently exist")
End If
ValueDict.Item(varName) = newVal
Console.WriteLine(varName & " now has value :" & ValueDict.Item(varName).ToString())
'Delete a value
ValueDict.Remove(varName)
If Not ValueDict.ContainsKey(varName) Then
Console.WriteLine(varName & " does not currently exist")
End If

Excel VBA: Failed to pass a string array to a Function

VBA Beginner here.
I am trying to pass an array of strings from a subroutine to a function which will then modify each string in the array. However I get the "Type:array or user-defined type expected" error message.
I have tried redefining different data types for the array so it is aligned with the data type entered in the function but to no avail.
Hope you can help! THank you so much!
Below is the dummy code:
Sub text()
Dim haha() As Variant
haha = Array("Tom", "Mary", "Adam")
testing (haha())
MsgBox Join(haha, " ")
End Sub
Function testing(ByRef check() As String) As String()
Dim track As Long
For track = LBound(check) To UBound(check)
check(track) = check(track) & " OMG"
Next
End Function
In orignial code, a string is not the same variant (I believe they both would need to be variant? someone can verify), you dont need the brackets after testing, only need brackets if you are setting to another value e.g.
haha2 = testing(haha())
Below code should be ok
Sub text()
Dim haha()
haha = Array("Tom", "Mary", "Adam")
testing haha()
MsgBox Join(haha, " ")
End Sub
Function testing(ByRef check()) As String
Dim track As Long
For track = LBound(check) To UBound(check)
check(track) = check(track) & " OMG"
Next
End Function
You have a few errors in your code:
There are two ways of invoking methods:
1) with Call keyword - in this case you must give all the parameters in brackets:
Call testing(haha)
2) without Call keyword - in this case you just give your parameters after the name of function:
testing haha
In your code you combined both of them and this is syntax error.
If you pass an array as a parameter to function you don't need to put brackets like that: testing (haha()).
The proper syntax is:
testing(haha)
Function testing requires as a parameter an array of String type, you cannot pass object of other type instead since it causes compile error Type mismatch. Currently you are trying to pass variable haha which is of Variant type.
You can change the type of haha variable to array of strings (to avoid the error described above):
Dim haha() As String
However, in this case you cannot assign the value of function Array to it, since the result of this function is of Variant type.
You would have to replace this code:
haha = Array("Tom", "Mary", "Adam")
with this:
ReDim haha(1 To 3)
haha(1) = "Tom"
haha(2) = "Mary"
haha(3) = "Adam"
A couple of suggestions to improve your code:
Dim haha() As String
You define the type of the entry in the array, not the array itself. Use the way mentioned by mielk to fill the array.
Function Testing(byref check as variant) As String
This will avoid problems with undefined variables. Not clear why you feel that the function should return a string though. Maybe even convert to a Sub instead.

Checking if a value is a member of a list

I have to check a piece of user input against a list of items; if the input is in the list of items, then direct the flow one way. If not, direct the flow to another.
This list is NOT visible on the worksheet itself; it has to be obfuscated under code.
I have thought of two strategies to do this:
Declare as an enum and check if input is part of this enum, although I'm not sure on the syntax for this - do I need to initialise the enum every time I want to use it?
Declare as an array and check if input is part of this array.
I was wondering for VBA which is better in terms of efficiency and readability?
You can run a simple array test as below where you add the words to a single list:
Sub Main1()
arrList = Array("cat", "dog", "dogfish", "mouse")
Debug.Print "dog", Test("dog") 'True
Debug.Print "horse", Test("horse") 'False
End Sub
Function Test(strIn As String) As Boolean
Test = Not (IsError(Application.Match(strIn, arrList, 0)))
End Function
Or if you wanted to do a more detailed search and return a list of sub-string matches for further work then use Filter. This code would return the following via vFilter if looking up dog
dog, dogfish
In this particular case the code then checks for an exact match for dog.
Sub Main2()
arrList = Array("cat", "dog", "dogfish", "mouse")
Debug.Print "dog", Test1("dog")
Debug.Print "horse", Test1("horse")
End Sub
Function Test1(strIn As String) As Boolean
Dim vFilter
Dim lngCnt As Long
vFilter = Filter(arrList, strIn, True)
For lngCnt = 0 To UBound(vFilter)
If vFilter(lngCnt) = strIn Then
Test1 = True
Exit For
End If
Next
End Function
Unlike in .NET languages VBA does not expose Enum as text. It strictly is a number and there is no .ToString() method that would expose the name of the Enum. It's possible to create your own ToString() method and return a String representation of an enum. It's also possible to enumerate an Enum type. Although all is achievable I wouldn't recommend doing it this way as things are overcomplicated for such a single task.
How about you create a Dictionary collection of the items and simply use Exist method and some sort of error handling (or simple if/else statements) to check whether whatever user inputs in the input box exists in your list.
For instance:
Sub Main()
Dim myList As Object
Set myList = CreateObject("Scripting.Dictionary")
myList.Add "item1", 1
myList.Add "item2", 2
myList.Add "item3", 3
Dim userInput As String
userInput = InputBox("Type something:")
If myList.Exists(userInput) Then
MsgBox userInput & " exists in the list"
Else
MsgBox userInput & " does not exist in the list"
End If
End Sub
Note: If you add references to Microsoft Scripting Runtime library you then will be able to use the intelli-sense with the myList object as it would have been early bound replacing
Dim myList As Object
Set myList = CreateObject("Scripting.Dictionary")
with
Dim myList as Dictionary
Set myList = new Dictionary
It's up to you which way you want to go about this and what is more convenient. Note that you don't need to add references if you go with the Late Binding while references are required if you want Early Binding with the intelli-sense.
Just for the sake of readers to be able to visualize the version using Enum let me demonstrate how this mechanism could possibly work
Enum EList
item1
item2
item3
[_Min] = item1
[_Max] = item3
End Enum
Function ToString(eItem As EList) As String
Select Case eItem
Case EList.item1
ToString = "item1"
Case EList.item2
ToString = "item2"
Case EList.item3
ToString = "item3"
End Select
End Function
Function Exists(userInput As String) As Boolean
Dim i As EList
For i = EList.[_Min] To EList.[_Max]
If userInput = ToString(i) Then
Exists = True
Exit Function
End If
Next
Exists = False
End Function
Sub Main()
Dim userInput As String
userInput = InputBox("type something:")
MsgBox Exists(userInput)
End Sub
First you declare your List as Enum. I have added only 3 items for the example to be as simple as possible. [_Min] and [_Max] indicate the minimum value and maximum value of enum (it's possible to tweak this but again, let's keep it simple for now). You declare them both to be able to iterate over your EList.
ToString() method returns a String representation of Enum. Any VBA developer realizes at some point that it's too bad VBA is missing this as a built in feature. Anyway, you've got your own implementation now.
Exists takes whatever userInput stores and while iterating over the Enum EList matches against a String representation of your Enum. It's an overkill because you need to call many methods and loop over the enum to be able to achieve what a simple Dictionary's Exists method does in one go. This is mainly why I wouldn't recommend using Enums for your specific problem.
Then in the end you have the Main sub which simply gathers the input from the user and calls the Exists method. It shows a Message Box with either true or false which indicates if the String exists as an Enum type.
Just use the Select Case with a list:
Select Case entry
Case item1,item2, ite3,item4 ' add up to limit for Case, add more Case if limit exceeded
do stuff for being in the list
Case Else
do stuff for not being in list
End Select