Passing parameter into function to create variable name - excel

I want to use global variables in my workbook and in the ThisWorkbook code. I declared the following varaibles
Public position_1 as string
Public position_2 as string
If I want to see the value of those variables I believe they need to be fully qualified so
Debug.Print ThisWorkbook.position_1
Debug.Print ThisWorkbook.position_2
I have written a UDF which I will pass in an integer to represent which variable I am looking for. I will only be passing in a single number and not a full variable name. I am trying to find a way to use this integer to concatenate with "position_" to display the value of the global variable, ThisWorkbook.position_1, ThisWorkbook.position_2, etc.
Function Test_Global_Var(position as Integer)
Dim variable_name As String
variable_name = "position_" & position
Debug.Print ThisWorkbook.variable_name
End Function
So when I call
Test_Global_Var(1)
my immediate window should display the value of
ThisWorkbook.position_1

The code below produces the following debug output
2 values defined.
ThisWorkbook.Position(0)
First Value
ThisWorkbook.Position(1)
Second Value
It uses a private array in the workbook named m_position. The contents are accessed by a global property ThisWorkbook.Position(index).
In a module have the following code:
Option Explicit
Public Sub Test()
If ThisWorkbook.NoValues Then
ThisWorkbook.FillValues "First Value", "Second Value"
End If
Debug.Print CStr(ThisWorkbook.Count) & " values defined."
Test_Global_Var 0
Test_Global_Var 1
End Sub
Public Sub Test_Global_Var(ByVal index As Long)
' Part of a UDF
Debug.Print "ThisWorkbook.Position(" & CStr(index) & ")"
Debug.Print ThisWorkbook.Position(index)
End Sub
In ThisWorkbook have the following code:
Option Explicit
Private m_position() As Variant
Private Sub Workbook_Open()
Call DefaultValues
End Sub
Public Property Get Position(ByVal index As Long) As Variant
Position = m_position(index)
End Property
Public Sub DefaultValues()
m_position = Array("First", "Second")
End Sub
Public Sub FillValues(ParamArray args() As Variant)
m_position = args
End Sub
Public Property Get Count() As Long
Count = UBound(m_position) - LBound(m_position) + 1
End Property
Public Property Get NoValues() As Boolean
On Error GoTo ArrUndefined:
Dim n As Long
n = UBound(m_position)
NoValues = False
On Error GoTo 0
Exit Sub
ArrUndefined:
NoValues = True
On Error GoTo 0
End Property
PS. In VBA never use Integer, but instead use Long. Integer is a 16bit type, while Long is the standard 32bit type that all other programming languages consider as an integer.

It is possible to consider a global dictionary variable and pass data through it from the UDF.
First add reference to Microsoft Scripting Runtime:
Thus, build the dictionary like this:
Public myDictionary As Dictionary
To initialize the myDictionary variable, consider adding it to a Workbook_Open event:
Private Sub Workbook_Open()
Set myDictionary = New Dictionary
End Sub
Then the UDF would look like this:
Public Function FillDicitonary(myVal As Long) As String
If myDictionary.Exists(myVal) Then
myDictionary(myVal) = "position " & myVal
Else
myDictionary.Add myVal, "position " & myVal
End If
FillDicitonary = "Filled with " & myVal
End Function
And it would overwrite every key in the dictionary, if it exists. At the end, the values could be printed:
Public Sub PrintDictionary()
Dim myKey As Variant
For Each myKey In myDictionary
Debug.Print myDictionary(myKey)
Next
End Sub

Related

Error handling of multiple class attributes

I have a class that holds data in separate attributes (I chose this design instead of an array or scripting.dictionary because I will be using these data for the construction of a decision tree and I want to make use of IntelliType while I construct it.).
The data are loaded from an excel spreadsheet in the form of floats, so I am storing them in a long datatype, but from time to time it happens so that a value is missing and is replaced by an "NA" string.
I would like to create an error-handling routine, that, if a non-numeric value is encountered, would replace the content with a value -1.
I know I could do this with checking with IsNumeric() or error-handling, but I don't know how to make it work for every one of the many attributes the class holds and I don't really like the solution to write a specific error-handling code for every one of them (storing each of them in a separate attribute is not elegant as well, but I find this a price I am willing to pay for the advantage of shorter syntax in the decision tree).
Is there a way to pass a value to a variable, that just encountered a type-mismatch error, by the error-handling code independent of the variable name?
A simple example with several of the attributes:
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub SetData(data as Range)
On Error Goto err_handler
B_LYM = data.Cells(1, 1)
B_mem = data.Cells(1, 2)
B_CXCR3 = data.Cells(1, 3)
B_CXCR4_MFI = data.Cells(1, 4)
err_handler:
'I need something like this:
'if valuebeingstored = "NA" then targetvariable = -1
End Sub
It could be that some other approach could be better and I am gladly open to options, I only want to emphasize that I would really like make use of IntelliType when constructing the decision tree. I was considering using scripting.dictionary, but the syntax will bloat the code very quickly.
You said you have a Class and therefore could include the function to check the input and return -1 inside the class and use the Get and Let properties to call the function.
Here is an example class (named clsDataStuff) demonstrating this:
Option Explicit
Private c_B_LYM As Double
Private c_B_mem As Double
Private c_B_CXCR3 As Double
Private c_B_CXCR4_MFI As Double
Public Property Let B_LYM(varValue As Variant)
c_B_LYM = ParseDouble(varValue)
End Property
Public Property Get B_LYM()
B_LYM = c_B_LYM
End Property
Public Property Let B_mem(varValue As Variant)
c_B_mem = ParseDouble(varValue)
End Property
Public Property Get B_mem()
B_mem = c_B_mem
End Property
Public Property Let B_CXCR3(varValue As Variant)
c_B_CXCR3 = ParseDouble(varValue)
End Property
Public Property Get B_CXCR3()
B_CXCR3 = c_B_CXCR3
End Property
Public Property Let B_CXCR4_MFI(varValue As Variant)
c_B_CXCR4_MFI = ParseDouble(varValue)
End Property
Public Property Get B_CXCR4_MFI()
B_CXCR4_MFI = c_B_CXCR4_MFI
End Property
Private Function ParseDouble(varValue As Variant) As Double
If IsNumeric(varValue) Then
ParseDouble = CDbl(varValue)
Else
ParseDouble = -1
End If
End Function
Noting that:
the Let property expects a Variant because you say your input could be a number, or a string
the Get property returns Double as you said your inputs are floats so Double is better than Long
the ParseDouble function simply checks for a numeric input and returns -1 otherwise
Then, in your module code:
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub Test()
Dim objDataStuff As clsDataStuff
Set objDataStuff = New clsDataStuff
objDataStuff.B_LYM = 1 'data.Cells(1, 1)
objDataStuff.B_mem = 2 'data.Cells(1, 2)
objDataStuff.B_CXCR3 = "a" 'data.Cells(1, 3)
objDataStuff.B_CXCR4_MFI = True 'data.Cells(1, 4)
Debug.Print objDataStuff.B_LYM
Debug.Print objDataStuff.B_mem
Debug.Print objDataStuff.B_CXCR3
Debug.Print objDataStuff.B_CXCR4_MFI
End Sub
Returns an output of:
1
2
-1
-1
Intellisense is available and you get validation of the input:
Edit - regarding the comment on dynamically setting a target variable.
Your class can be:
Option Explicit
Public B_LYM As Double
Public B_mem As Double
Public B_CXCR3 As Double
Public B_CXCR4_MFI As Double
Public Sub SetVar(ByVal strVarName As String, ByVal varValue As Variant)
Dim dblValue As Double
Dim strToEval As String
If Not MemberExists(strVarName) Then Exit Sub
dblValue = ParseDouble(varValue) ' do the parse
CallByName Me, strVarName, VbLet, dblValue ' dynamically assign the value
End Sub
Private Function ParseDouble(varValue As Variant) As Double
If IsNumeric(varValue) Then
ParseDouble = CDbl(varValue)
Else
ParseDouble = -1
End If
End Function
Private Function MemberExists(strVarName) As Boolean
Dim blnTest As Boolean
Dim varValue As Variant
On Error GoTo ErrHandler
varValue = CallByName(Me, strVarName, VbGet)
blnTest = True
GoTo ExitFunction
ErrHandler:
blnTest = False
ExitFunction:
MemberExists = blnTest
End Function
Where:
All the variables are Public and you still get Intellisense but avoid all the repetitive Let and Get code
A single SetVar method uses CallByName to dynamically set a target variable
Two problems:
You need the clunky MemberExists function to prevent SetVar trying to assign a value to a member that does not exist - otherwise this generates an error (438) but perhaps this is something you need in your logic ?
You can still assign values to the target variable with e.g. objDataStuff.B_CXR3 = "foo" which alsos produces an error for anything other than a number.
The example code shows the problem below. But sticking with SetVar method will produce the same output as above.
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub Test()
Dim objDataStuff As clsDataStuff
Set objDataStuff = New clsDataStuff
objDataStuff.SetVar "B_LYM", 1
objDataStuff.SetVar "B_mem", 2
objDataStuff.SetVar "B_CXCR3", -1
objDataStuff.SetVar "B_CXCR4_MFI", True
objDataStuff.SetVar "foobar", 999
' working around SetVar here generates an error
objDataStuff.B_CXCR3 = "bad"
Debug.Print objDataStuff.B_LYM
Debug.Print objDataStuff.B_mem
Debug.Print objDataStuff.B_CXCR3
Debug.Print objDataStuff.B_CXCR4_MFI
End Sub

How to set global variables in VBA

I would like to define a global variable in VBA in one module and use this in other VBA modules.
I am trying to follow: How do I declare a global variable in VBA?
I have created a new module called "GlobalVariables", and first declared the Public variables and then set their value within a function (trying to do this in open code causes an error). My code is below.
But the Global variable StartYear does not seem to be available into other VBA modules. What am I doing wrong?
Option Explicit
Public StartYear As Integer
Public BaseYear As Integer
Function DeclareGlobalVariables()
StartYear = ActiveWorkbook.Worksheets("RunModel").Range("StartYear").Value
BaseYear = ActiveWorkbook.Worksheets("RunModel").Range("BaseYear").Value
End Function
Make sure you put your golobal variable in a module and not in worksheet scope, to make it accessible in other modules.
Your Function should be a Sub because it does not return anything.
Your code will error if your cells eg. contain text (strings). Never trust a user's input. Always validate!
So I suggest the following
Module 1
Option Explicit
Public StartYear As Long
Public BaseYear As Long
Public Function InitializeGlobalVariables() As Boolean
InitializeGlobalVariables = True
With ActiveWorkbook.Worksheets("RunModel").Range("StartYear")
If IsYear(.Value) Then
StartYear = CLng(.Value)
Else
InitializeGlobalVariables = False
MsgBox "StartYear needs to be a number"
End If
End With
With ActiveWorkbook.Worksheets("RunModel").Range("BaseYear")
If IsYear(.Value) Then
BaseYear = CLng(.Value)
Else
InitializeGlobalVariables = False
MsgBox "BaseYear needs to be a number"
End If
End With
End Function
'validate if the input value is a valid year
Private Function IsYear(ByVal InputValue As Variant) As Boolean
If IsNumeric(InputValue) Then
If CLng(InputValue) = InputValue And _
InputValue > 0 And InputValue < 9999 Then 'integer not decimal AND 4 digit year
IsYear = True
End If
End If
End Function
And you can access the variables in any other module like:
Module 2
Option Explicit
Public Sub TestOutput()
'before using the variables test if they are initialized (not 0)
If StartYear = 0 Or BaseYear = 0 Then
'they are not initalized so initalize them (and at the same time check if it was successful)
If InitializeGlobalVariables = False Then
'the function returns FALSE if the initialization process failed so we need to cancel this procedure or we use not initilized variables!
MsgBox "Variables were not intitialized. Trying to initialize them failed too. I cannot proceed."
Exit Sub
End If
End If
Debug.Print StartYear
Debug.Print BaseYear
End Sub

How to access the value of a string when defining within a loop

I would like to access the value of a string when declaring new variables so that I can declare new variables within a loop.
I have tried val(), creating a function. An simplified version of my problem can be found in the code below.
Function StudentValue(x As String) As String
StudentValue = x
End Function
Public Sub TEST()
Dim i As Integer
Dim strName As String
Dim n As Integer
n = 20
For i = 1 To n
strName = "Variable" & CStr(i)
'The problem occurs with the next two lines,
'once active they create a string with the name 'strName' and not the
'value of the string eg 'Variable1', 'Variable2', ect
'Attempt1
'Dim strName As String
'Attempt2
'Dim NameFunction(strName) As String
Next i
End Sub
The errors are as follows:
Dim strName As String results in "compile error: Duplicate declaration in current scope"
Dim NameFunction(strName) As String results in "compile error: Constant expression required"
Is there a function that allows you to access the value of a string when declaring variables?
Thank you in advance!
You are getting "Duplicate declaration" Error because you are trying to declare a variable by the same name.
You are getting the error "Constant expression required" Error because Dim XYZ() as string is the syntax for declaring an array. And the value inside the brackets specifies the size of the array and must be constant.
Here is a link on how to use arrays.
Use Option Explicit, it will help you solve problems before they are problems.
Here is your code using arrays.
Option Explicit
Function StudentValue(x As String) As String
StudentValue = CStr(x)
End Function
Public Sub TEST()
Const MaxNumNames As Integer = 20
Dim i As Integer
Dim strNames(1 To MaxNumNames) As String
For i = 1 To MaxNumNames
'This will populate the array of names
strNames(i) = "Variable" & CStr(i)
'To use the name in the loop
Debug.Print "In Loop:" & strNames(i)
Next i
'To use the name outside the loop (Show 5th name)
Debug.Print "Outside Loop: " & strNames(5)
' To use the name in your function outside the loop (Using 2nd Name)
Debug.Print "Using Function: " & StudentValue(strNames(2))
End Sub

VBA: Creating session persistent objects (hash) in Excel

Is it possible in a VBA function (UDF) to create an object that has global scope? I.e persists beyond the runtime of the function? I would want to stick it in a hash with a unique key that I can pass to other functions. I know you can do this in c#/c++ dll's.
The motivation is a heavy duty piece of processing that I don't want to repeat across hundreds of function calls: I want to cache the results so I only need to do once. E.g let's imagine I have a UDF which builds the results object in Cell A1:
=CreateResultsObject(arg1, arg2, arg3...)
The function does the heavy work and returns a unique ID string (the key for the object stored in the persistent hash). Cell A1 now contains this string value which I can then pass to other functions: they can then access the cached object in the hash with the key.
Is this possible? If so how?
The variables you declare in a module are persistent.
This code in a module might go into the direction you want:
Option Explicit
Dim col As New Collection
Public Function GetValue(ByVal strName As String) As String
GetValue = col.Item(strName)
End Function
Public Sub SetValue(ByVal strName As String, ByVal strValue As String)
col.Add strValue, strName
End Sub
Note:
For duplicate or missing names the code will fail.
Instead of a string value any kind of object could be passed by modifying the function signatures accordingly.
Addendum:
The same code with a bit more intelligence - for existing keys in the collection the value will be replaced instead of failing with an error.
Option Explicit
Dim col As New Collection
Public Function GetValue(ByVal strName As String) As String
GetValue = col.Item(strName)
End Function
Public Sub SetValue(ByVal strName As String, ByVal strValue As String)
If HasValue(strName) Then
col.Remove (strName)
End If
col.Add strValue, strName
End Sub
Private Function HasValue(ByVal strName As String) As Boolean
Dim val As Variant
Dim bRes As Boolean
bRes = True
On Error Resume Next
val = col.Item(strName)
If Err.Number <> 0 Then
bRes = False
Err.Clear
End If
On Error GoTo 0
HasValue = bRes
End Function
What about using a global variable within a module?
Something like this:
Option Explicit
Dim sHash As String
Function CreateResultsObject()
'very long code
sHash = "MyTest"
CreateResultsObject = "ok"
End Function
Function displayresultsobject()
displayresultsobject = sHash
End Function
Note that your Hash will be recalculated only when you call CreateResultsObject() in your worksheet and each time you ask for recalculation.

How do I declare a global variable in VBA?

I wrote the following code:
Function find_results_idle()
Public iRaw As Integer
Public iColumn As Integer
iRaw = 1
iColumn = 1
And I get the error message:
"invalid attribute in Sub or Function"
Do you know what I did wrong?
I tried to use Global instead of Public, but got the same problem.
I tried to declare the function itself as `Public, but that also did no good.
What do I need to do to create the global variable?
You need to declare the variables outside the function:
Public iRaw As Integer
Public iColumn As Integer
Function find_results_idle()
iRaw = 1
iColumn = 1
This is a question about scope.
If you only want the variables to last the lifetime of the function, use Dim (short for Dimension) inside the function or sub to declare the variables:
Function AddSomeNumbers() As Integer
Dim intA As Integer
Dim intB As Integer
intA = 2
intB = 3
AddSomeNumbers = intA + intB
End Function
'intA and intB are no longer available since the function ended
A global variable (as SLaks pointed out) is declared outside of the function using the Public keyword. This variable will be available during the life of your running application. In the case of Excel, this means the variables will be available as long as that particular Excel workbook is open.
Public intA As Integer
Private intB As Integer
Function AddSomeNumbers() As Integer
intA = 2
intB = 3
AddSomeNumbers = intA + intB
End Function
'intA and intB are still both available. However, because intA is public, '
'it can also be referenced from code in other modules. Because intB is private,'
'it will be hidden from other modules.
You can also have variables that are only accessible within a particular module (or class) by declaring them with the Private keyword.
If you're building a big application and feel a need to use global variables, I would recommend creating a separate module just for your global variables. This should help you keep track of them in one place.
To use global variables, Insert New Module from VBA Project UI and declare variables using Global
Global iRaw As Integer
Global iColumn As Integer
The question is really about scope, as the other guy put it.
In short, consider this "module":
Public Var1 As variant 'Var1 can be used in all
'modules, class modules and userforms of
'thisworkbook and will preserve any values
'assigned to it until either the workbook
'is closed or the project is reset.
Dim Var2 As Variant 'Var2 and Var3 can be used anywhere on the
Private Var3 As Variant ''current module and will preserve any values
''they're assigned until either the workbook
''is closed or the project is reset.
Sub MySub() 'Var4 can only be used within the procedure MySub
Dim Var4 as Variant ''and will only store values until the procedure
End Sub ''ends.
Sub MyOtherSub() 'You can even declare another Var4 within a
Dim Var4 as Variant ''different procedure without generating an
End Sub ''error (only possible confusion).
You can check out this MSDN reference for more on variable declaration and this other Stack Overflow Question for more on how variables go out of scope.
Two other quick things:
Be organized when using workbook level variables, so your code doesn't get confusing. Prefer Functions (with proper data types) or passing arguments ByRef.
If you want a variable to preserve its value between calls, you can use the Static statement.
If this function is in a module/class, you could just write them outside of the function, so it has Global Scope. Global Scope means the variable can be accessed by another function in the same module/class (if you use dim as declaration statement, use public if you want the variables can be accessed by all function in all modules) :
Dim iRaw As Integer
Dim iColumn As Integer
Function find_results_idle()
iRaw = 1
iColumn = 1
End Function
Function this_can_access_global()
iRaw = 2
iColumn = 2
End Function
Also you can use -
Private Const SrlNumber As Integer = 910
Private Sub Workbook_Open()
If SrlNumber > 900 Then
MsgBox "This serial number is valid"
Else
MsgBox "This serial number is not valid"
End If
End Sub
Its tested on office 2010
The best way I find is to assign a property to the Workbook
It's scope remains valid as long as the workbook is open
Public WhenOpened As Date
Private Sub Workbook_Open()
ThisWorkbook.WhenOpened = Now()
End Sub
Create a public integer in the General Declaration.
Then in your function you can increase its value each time.
See example (function to save attachements of an email as CSV).
Public Numerator As Integer
Public Sub saveAttachtoDisk(itm As Outlook.MailItem)
Dim objAtt As Outlook.Attachment
Dim saveFolder As String
Dim FileName As String
saveFolder = "c:\temp\"
For Each objAtt In itm.Attachments
FileName = objAtt.DisplayName & "_" & Numerator & "_" & Format(Now, "yyyy-mm-dd H-mm-ss") & ".CSV"
objAtt.SaveAsFile saveFolder & "\" & FileName
Numerator = Numerator + 1
Set objAtt = Nothing
Next
End Sub
A good way to create Public/Global variables is to treat the Form like a class object and declare properties and use Public Property Get [variable] to access property/method. Also you might need to reference or pass a Reference to the instantiated Form module. You will get errors if you call methods to forms/reports that are closed.
Example: pass Me.Form.Module.Parent into sub/function not inside form.
Option Compare Database
Option Explicit
''***********************************''
' Name: Date: Created Date Author: Name
' Current Version: 1.0
' Called by:
''***********************************''
' Notes: Explain Who what when why...
' This code Example requires properties to be filled in
''***********************************''
' Global Variables
Public GlobalData As Variant
''***********************************''
' Private Variables
Private ObjectReference As Object
Private ExampleVariable As Variant
Private ExampleData As Variant
''***********************************''
' Public properties
Public Property Get ObjectVariable() As Object
Set ObjectVariable = ObjectReference
End Property
Public Property Get Variable1() As Variant
'Recommend using variants to avoid data errors
Variable1 = ExampleVariable
End property
''***********************************''
' Public Functions that return values
Public Function DataReturn (Input As Variant) As Variant
DataReturn = ExampleData + Input
End Function
''***********************************''
' Public Sub Routines
Public Sub GlobalMethod()
'call local Functions/Subs outside of form
Me.Form.Refresh
End Sub
''***********************************''
' Private Functions/Subs used not visible outside
''***********************************''
End Code
So in the other module you would be able to access:
Public Sub Method1(objForm as Object)
'read/write data value
objForm.GlobalData
'Get object reference (need to add Public Property Set to change reference object)
objForm.ObjectVariable
'read only (needs Public property Let to change value)
objForm.Variable1
'Gets result of function with input
objForm.DataReturn([Input])
'runs sub/function from outside of normal scope
objForm.GlobalMethod
End Sub
If you use Late Binding like I do always check for Null values and objects that are Nothing before attempting to do any processing.

Resources