Evaluate variable based on user input - vba

Coming from Python, I cannot figure out a way to accomplish my end goal since a function like eval() does not exist with Excel VBA.
Below is what I am trying to achieve by using a simple example that I can scale:
Dim userinput as String
userinput = inputbox("A=John, B=Kate, C=Tim", "Enter a letter that corresponds with your name: ")
Dim A as String
Dim B as String
Dim C as String
A = John
B = Kate
C = Tim
Dim Phrase as Variant
phrase = msgbox("Hello, my name is " & userinput)
Result:
User enters a C
msg box pops up as "Hello, my name is Tim"
Regardless of what I try to do, I cannot have a userinput correspond to a variable that is then interpreted while in a string. My macro would be useless without a user's input referencing a much longer and complex phrase that would be too hard for a user to enter on their own.

You're presenting a finite, pre-determined set of options to the user.
Yet you let them enter quite literally anything, not validated, and roll with it.
A much better alternative would be to ditch the InputBox approach and present the user with an actual UserForm, so that the user's input is constrained to a finite, pre-determined set of options - a ComboBox control comes to mind:
The UserForm's code-behind is pretty straightforward - hide the form when a button is clicked, treat "X-out" as a cancellation, expose the user's Selection and whether the form was cancelled via Property Get members, and populate the dropdown with the possible values to pick from:
Option Explicit
Private isCancelled As Boolean
Public Property Get Cancelled() As Boolean
Cancelled = isCancelled
End Property
Public Property Get Selection() As String
Selection = ComboBox1.Value
End Property
Private Sub CancelButton_Click()
isCancelled = True
Hide
End Sub
Private Sub OkButton_Click()
Hide
End Sub
Private Sub UserForm_Activate()
With ComboBox1
.Clear
.AddItem "Lorem"
.AddItem "Ipsum"
'...
End With
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
Cancel = True
isCancelled = True
Hide
End If
End Sub
And now you can do this:
Public Sub Test()
With New UserForm1
.Show
If Not .Cancelled Then
MsgBox .Selection
End If
End With
End Sub
This handles the user cancelling out of the prompt, and if your ComboBox control's Style property is set to fmStyleDropDownList, there's absolutely no way the user can supply a value you're not expecting. And you can extend it further by supplying Property Get and Property Let members to configure the Title and Instructions labels as needed, or make the calling code responsible for supplying the list of valid values to populate the ComboBox with.
That's one part of the problem. The next part is "mapping" inputs with another string value. The best data structure for this is a Dictionary, but a keyed Collection can work just as well:
Dim values As Collection
Set values = New Collection
values.Add "string 1", "Lorem"
values.Add "string 2", "Ipsum"
'...
With New UserForm1
.Show
If Not .Cancelled Then
MsgBox values(.Selection)
End If
End With
Of course there are many ways to populate the data you need here, both for the combobox values and the mapped strings in the collection; you can hard-code them like this, or get them from a Range on a worksheet, or from a database, whatever rocks your boat.

The user is typing a letter. You need code to more explicitly translate that into a name. There are a lot of methods - if you have a long list or a user-entered list you may need a solution that is more dynamic. However here is a fix for your code:
Dim userinput as String
userinput = inputbox("A=John, B=Kate, C=Tim", "Enter a letter that corresponds with your name: ")
Dim A as String
Dim B as String
Dim C as String
Dim nameOut as String
select case userinput
case "A"
nameOut = "John"
case "B"
nameOut = "Kate"
case "C"
nameOut = "Tim"
end select
Dim Phrase as Variant
phrase = MsgBox("Hello, my name is " & nameOut)
Note: don't forget to enclose strings in double quotes.

Not to take away from Scott's answer but for a potentially more dynamic approach it seems class modules and callbyname have an answer:
Class module - class1
Private pa As String
Public Property Get a() As String
a = pa
End Property
Public Property Let a(value As String)
pa = value
End Property
Test sub
Sub testing()
Dim cls1 As Class1
Set cls1 = New Class1
cls1.a = "tim"
userinput = "a"
Debug.Print CallByName(cls1, userinput, VbGet)
End Sub

Posted just because it's a different approach. It finds the position of the inputted letter in an array of all three, and then finds the corresponding item in an array of the names.
Sub x()
Dim userinput As String
Dim Phrase As String
Dim v
userinput = InputBox("A=John, B=Kate, C=Tim", "Enter a letter that corresponds with your name: ")
v = Array("John", "Kate", "Tim")
With Application
If IsNumeric(.Match(userinput, Array("A", "B", "C"), 0)) Then
Phrase = .Index(v, .Match(userinput, Array("A", "B", "C"), 0))
MsgBox "Hello, my name is " & Phrase
Else
MsgBox "Wrong letter"
End If
End With
End Sub

Related

3 Dimentional Dictionary

I'm trying make to a 3 Dimension Dictionary to store the data in the form of tools(material)(part)(attribute), and I have managed to create the Dictionary like this:
Dim Tools As New Dictionary(Of String, Dictionary(Of String, Dictionary(Of String, Decimal)))
And what I basically want to do is have some subs that manage that for me instead of dealing with that mess, and I want it to be like this like this:
Add_Attribute("Iron", "Pickaxe Head", "Durability", 204)
Get_Attribute("Stone", "Pickaxe Head", "Mining Speed")
Any answers would be greatly be appreciated.
My comment was not worded properly.
Create a class with add/get attributes function that accepts 3 parameters.
Concatenate the parameters and use it as dictionary key.
Option Explicit
Dim oDict As Dictionary
Public Function Add_Attribute(psParam1 As String, psParam2 As String, psParam3 As String, psValue As String)
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
oDict.Item(sKey) = psValue
Else
oDict.Add sKey, psValue
End If
End Function
Public Function Get_Attribute(psParam1 As String, psParam2 As String, psParam3 As String) As String
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
Get_Attribute = oDict.Item(sKey)
Else
Get_Attribute = ""
End If
End Function
Private Sub Class_Initialize()
Set oDict = New Dictionary
End Sub
Private Function BuildKey(psParam1 As String, psParam2 As String, psParam3 As String) As String
BuildKey = Join(Array(psParam1, psParam2, psParam3), "#")
End Function
Private Sub Class_Terminate()
Set oDict = Nothing
End Sub
Jules' answer of a custom class and concatenation of the three strings as a key will work very nicely for you and is a neat solution to your problem.
I'm posting another answer here for anyone who wants more of a dot notation style of solution. So one of the lines in your example could look something like:
mTools("Pickaxe Head").Attr("Durability").Material("Iron") = 204
I'm guessing you're deriving the values from a comboxbox or something similar, so working with strings might serve you fine. However, if you wished, you could go one stage further and create objects for the Attributes and Material parameters to achieve true dot notation (I didn't do the Parts parameter but you could do that one too):
mTools("Pickaxe Head").Durability.OnIron = 204
From a development point of view, the time consuming part would be to create all the parameter objects and keys, but if you are intending to manipulate the data anything more than trivially, it could make your life easier further down the track.
For your own project, are you certain that the data is genuinely 3 dimensional? Perhaps it's just the variable names that you've picked, but it seems as though you have one main object, ie the part (Pickaxe Head) which has some attributes (Durability and Mining Speed) which themselves have values based on the material they're operating on (Stone and Iron). Structurally, could it look like this?:
In terms of the code for this solution, create three classes. I've called them clsKeys, clsMaterials and clsPart.
For your clsKeys, the code is simply your field names:
Public Durability As String
Public MiningSpeed As String
Public Iron As String
Public Stone As String
For clsPart, the code contains the object names and a means of accessing them by string:
Public Name As String
Public Durability As New clsMaterials
Public MiningSpeed As New clsMaterials
Private mProperties As New Collection
Public Property Get Attr(field As String) As clsMaterials
Set Attr = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add Durability, .Durability
mProperties.Add MiningSpeed, .MiningSpeed
End With
End Sub
clsMaterials is similar:
Public OnStone As Integer
Public OnIron As Integer
Private mProperties As New Collection
Public Property Let Material(field As String, value As Variant)
mProperties.Remove field
mProperties.Add value, field
End Property
Public Property Get Material(field As String) As Variant
Material = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add OnStone, .Stone
mProperties.Add OnIron, .Iron
End With
End Sub
These classes can take as many objects as you like. You'll note I've instantiated the objects in the declaration which isn't best form but I've done it in the interest of space.
Finally, in a Module you need 3 routines: one to create the field keys, one to populate the data and one to retrieve it.
For the keys:
Option Explicit
Public Keys As clsKeys
Private mTools As Collection
Sub CreateKeys()
Set Keys = New clsKeys
With Keys
.Durability = "Durability"
.MiningSpeed = "Mining Speed"
.Iron = "Iron"
.Stone = "Stone"
End With
End Sub
For data population:
Sub PopulateData()
Dim oPart As clsPart
Set mTools = New Collection
Set oPart = New clsPart
With oPart
.Name = "Pickaxe Head"
'You could use dot notation
.Durability.OnIron = 204
.Durability.OnStone = 100
'Or plain strings
.Attr("Mining Speed").Material("Stone") = 50
.Attr("Mining Speed").Material("Iron") = 200
mTools.Add oPart, .Name
End With
End Sub
and for data retrieval:
Sub RetrieveValue()
Dim oPart As clsPart
Dim v As Variant
Set oPart = mTools("Pickaxe Head")
With oPart
'Using dot notation
v = oPart.Durability.OnIron
Debug.Print v
'Using plain strings
v = oPart.Attr("Durability").Material("Stone")
Debug.Print v
End With
'Or even without assigning the oPart variable
v = mTools("Pickaxe Head").Attr("Mining Speed").Material("Iron")
Debug.Print v
End Sub

excel VBA byref argument passed to function not being changed

I have been struggling with this for a while and have found some related questions but none seem to solve my problem so it might just be my coding. The goal is to populate a form from a user defined data type (Customer) and then retrieve the information from the form back into the data object after the user has updated the form. When I run this code with IOType = "O" it works fine, the data elements are all put into the form. When I run it with IOType = "I" it reads the text from the form into the DataObjectOrType variable but the customer object that I sent in is not updated (i.e. CurCustomer.name and CurCustomer.territory are both = ""). The underlying data elements of CurCustomer are all strings if that matters. I have tried this as a sub and a function, with and without parentheses, with and without the byref but nothing seems to make a difference.
update - I have simplified some things and added some code below. Madgui, you are right that it works with a data type (sub CustomerFormIO2). It also works with just a couple of strings (sub CustomerFormIO3). I put the code from the Customer class module below too in case that helps. I have a feeling this isn't going to work. Could it have something to do with the Get and Let properties using intermediary variables? Is there any way to get this to work?
Thanks.
Private Type structCustomer
Name As String
Territory As String
Rep As String
End Type
Public Sub CustomerFormIO1()
Dim CurCustomer As Customer
Dim IOType As String
Set CurCustomer = New Customer
IOType = "I"
Call ObjectIO("X", CurCustomer.Name, IOType)
Call ObjectIO("Y", CurCustomer.Territory, IOType)
ObjectIO "Z", CurCustomer.Rep, IOType
End Sub
Public Sub CustomerFormIO2()
Dim CurCustomer As structCustomer
Dim IOType As String
IOType = "I"
Call ObjectIO("X", CurCustomer.Name, IOType)
Call ObjectIO("Y", CurCustomer.Territory, IOType)
ObjectIO "Z", CurCustomer.Rep, IOType
End Sub
Public Sub CustomerFormIO3()
Dim CurCustomerName As String
Dim CurCustomerTerritory As String
Dim CurCustomerRep As String
Dim IOType As String
IOType = "I"
Call ObjectIO("X", CurCustomerName, IOType)
Call ObjectIO("Y", CurCustomerTerritory, IOType)
ObjectIO "Z", CurCustomerRep, IOType
End Sub
Public Function ObjectIO(FormObject As String, ByRef DataObjectOrValue As Variant, Optional IOType As String) As Variant
If IOType = "I" Then
DataObjectOrValue = FormObject
Else '"O"
FormObject = DataObjectOrValue
End If
ObjectIO = True
End Function
Customer Class Module:
Private c_CustName As String
Private c_Rep As String
Private c_Territory As String
Public Property Get Name() As String
Name = c_CustName
End Property
Public Property Let Name(CName As String)
c_CustName = CName
End Property
Public Property Get Rep() As String
Rep = c_Rep
End Property
Public Property Let Rep(CRep As String)
c_Rep = CRep
End Property
Public Property Get Territory() As String
Territory = c_Territory
End Property
Public Property Let Territory(CTerritory As String)
c_Territory = CTerritory
End Property
Private Sub Class_Initialize()
c_CustName = ""
c_Rep = ""
c_Territory = ""
End Sub
Oddly, it works if you define your Customer class as a Type (= static structure ) :
Private Type structCustomer
Name As String
Territory As String
Rep As String
End Type
In your code change
Dim CurCustomer As Customer
by
Dim CurCustomer As structCustomer
and comment the line :
'Set CurCustomer = New Customer
(Old post, but adding this since there's no definitive answer here.)
This can be done by passing a reference to the full object CurCustomer, but not by passing the property directly. Properties are evaluated and passed as values whether or not ByRef is specified.
When passing a Customer object, you can add the property directly within the receiving function (DataObjectOrValue.Name), but doing so would require extra logic to handle multiple properties in the same function.
You can also pass the property name and use CallByName to keep the function flexible.
See the example below.
Public Sub CustomerFormIO4()
Dim CurCustomer As Customer
Dim IOType As String
Set CurCustomer = New Customer
IOType = "I"
Call ObjectIO("X", CurCustomer, "Name", IOType)
Call ObjectIO("Y", CurCustomer, "Territory", IOType)
ObjectIO "Z", CurCustomer, "Rep", IOType
End Sub
Public Function ObjectIO(FormObject As String, _
ByRef DataObjectOrValue As Variant, PropName As String, _
Optional IOType As String) As Variant
If IOType = "I" Then
CallByName DataObjectOrValue, PropName, VbLet, FormObject
Else '"O"
CallByName DataObjectOrValue, PropName, VbGet, FormObject
End If
ObjectIO = True
End Function
You can verify that the issue is not unique to user-defined classes by trying the same approach with a worksheet object:
Sub wsNameByRefTest()
Dim ws As Object
Set ws = Sheets.Add
ws.Name = "It failed!"
changeNameByRef ws.Name
End Sub
Sub changeNameByRef(ByRef wsName As String)
wsName = "It worked!"
End Sub
For reference, see this MSDN Article. It's officially documentation on VBScript, but I've tested in VBA, and all points in the article apply. (ByRef is the default for parameters in both, for example.) I could find no similar reference for VBA specifically.
ByRef Parameters Passed by Value
If the parameter is specified as ByRef, the argument can still be passed by value. This occurs if one of the following is true:
The argument is in parentheses, and the parentheses do not apply to the argument list.
The variable sent as an argument is in a class.
The argument being passed is not a variable, for example, 12.

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

Access variables of sub in VBA

I have the following sub
Public Static Sub Varib()
Device_ = Sheet1.DeviceType_.Text
Model_ = Sheet1.Model_.Text
Security_ = Sheet1.SecurityGroup_.Text
Catagory_ = Application.Index(Worksheets("Temp_for_varible_lists").Range("b:b"), Application.Match(x, Worksheets("Temp_for_varible_lists").Range("A:A"), 0))
End Sub
It in fact carries on and in total produces a whole bunch of vaules of various datatypes based on the users input.
So the user choses from a few check boxes, list boxes, fills in some text boxes and hits a submit button and this sub populates a number of varibles from that, that are then uterlised by other funcation and sub in the application.
Now I could make all the varibles Global and access them in that fassion. But I was hoping for something more like what I have seen with c# and VB.net
where you can get the value by using
sub.varible name
example for the code above.
Sub Main()
x = Varib.Device_
msgbox(x)
end sub
is there a simmular way to do this in VBA?
Cheers
aaron
What you're asking cannot be done. The solution is not to make your variables global either (generally a bad idea, with some exceptions, this case not being one of them).
One possibility is to create a user-defined type:
Type Varib
Device_ As String
Model_ As String
Security_ As String
Category_ As String
End Type
and a sub to populate it from your sheet:
Sub LoadVaribFromSheet(v As Varib)
With v
.Device_ = Sheet1.DeviceType_.Text
.Model_ = Sheet1.Model_.Text
.Security_ = Sheet1.SecurityGroup_.Text
.Category_ = _
Application.Index(Worksheets("Temp_for_varible_lists").Range("b:b"), _
Application.Match(x, _
Worksheets("Temp_for_varible_lists").Range("A:A"), 0))
End With
End Sub
You can then use this as follows:
Sub Main()
Dim myVarib As Varib
LoadVaribFromSheet myVarib
' Now do stuff with myVarib ...
MsgBox myVarib.Device_
End Sub
you can use encapsulation for this
Private value As String
Private value1 As String
Public Function setValue(val As String)
value = val
End Function
Public Function setValue1(val As String)
value1 = val
End Function
Public Function getValue() As String
getValue = value
End Function
Public Function getValue1() As String
getValue1 = value1
End Function
-------------------------------------------------------------------------
Sub test()
MsgBox getValue & vbCrLf & getValue1
setValue "myValue"
setValue1 "myValue1"
MsgBox getValue & vbCrLf & getValue1
End Sub

VBA Clean use of many Constants

My Excel VBA takes ~300 XLS files and grabs 8 cells to deposit into their own row. (Office11) I have several subs and functions that use location constants for the sourceData and destination locations. Grand Total I have 23 constant locations with Column numbers, cell locations.
Question: Any suggestions on how to clean this up for readability and keeping constants all in one location? I was trying to avoid public variables but not sure of a better method. How do you do Arrays containing constant values?
partial example,Public pstrQLocations(1 To 8) As String
pstrQLocations(1) = "B7"
pstrQLocations(2) = "B6"
pstrQLocations(3) = "B5"
pstrQLocations(4) = "B8"
pstrQLocations(5) = "A3"
pstrQLocations(6) = "C8"
You can store your Constants in a Collection. The advantage is, that you can give your elements names.
Option Explicit
Dim pstrQLocations As Collection
Private Sub initializeConstants()
Set pstrQLocations = New Collection
pstrQLocations.Add "B7", "Title"
pstrQLocations.Add "B6", "User"
End Sub
Private Sub showConstants()
initializeConstants
Debug.Print Me.Range(pstrQLocations("Title")).Value
Debug.Print Me.Range(pstrQLocations("User")).Value
End Sub
3D Version:
Option Explicit
Dim pstrQLocations As Collection
Private Sub initializeConstants()
Dim title As New Collection
Set pstrQLocations = New Collection
title.Add "B7", "source"
title.Add "A6", "destination"
pstrQLocations.Add title, "Title"
End Sub
Private Sub showConstants()
Dim y As Collection
initializeConstants
Debug.Print pstrQLocations("Title")("source")
Debug.Print pstrQLocations("Title")("destination")
End Sub