Variable names don't match but still works - excel

I'm looking at a file that someone in my work created, it has the below:
Sub Inputs(zDOB As Date, zRetAge As Double, zRetDate As Date, zDOJ As Date, zEmployer As Double, zEmployee As Double, _
zSalary() As Double, zInflation As Double, zFund As Double, zAVCRate As Double, zEvalDate As Date, zAVCFund As Double, _
zCharge As Double, zFund2 As Double, zAVCFund2 As Double)
zDOB = Range("B1")
zRetAge = Range("B7")
zRetDate = Range("B8")
zDOJ = Range("B11")
zEmployer = Range("B15")
zEmployee = Range("B16")
zSalary(0) = Range("B14")
zInflation = Range("B19")
zFund = Range("B20")
zFund2 = Range("B20")
zAVCRate = Range("B24")
zAVCFund = Range("B27")
zAVCFund2 = Range("B27")
zEvalDate = Range("B6")
zCharge = Range("J7")
End Sub
Fair enough, this is setting up the inputs to be used later.
My problem is when this sub is called again:
Call Module3.Inputs(xDOfB, xRetirementAge, xDateRetire, xDOJ, xEmployer, xEmployee, xSalary, _
xInflation, xFund, xAVC, xEvalDate, xAVCFund, xCharge, xFund2, xAVCFund2)
The z is now x, does this make a difference? How does this work?

You may want to use public variables if you will use them across subscripts.
Public zDOB As Variant
Public zRetAte As Variant
Sub textSub()
Call Inputs
End Sub
Sub Inputs()
zDOB = Range("B1")
zRetAge = Range("B7")
End Sub

When you call a Function/Sub in VBA, you can pass values to it, normally referred to as arguments. The names of arguments and their types are defined in the routine's signature like you have here:
Sub Inputs(zDOB As Date, zRetAge As Double, zRetDate As Date, ...)
To call a routine with certain arguments, you can pass them from the calling code as raw values:
Inputs 1, 2, 4 ... 'equiv to the obsolete Call Inputs(1, 2, 4 ...)
OR, the values you supply when you call a routine can be stored in variables. So I can have a variable being passed as an argument:
Dim x as Date 'here I 'declare' x, you don't generally have to do this but it's advised
x = CDate(Range("A1")) 'here I assign a Value (the date in A1) to a variable (x)
Inputs x, 3, 4, ... 'and call Inputs passing a variable (x) and some values(3,4) as argments
or equivalently for your code:
Dim xDOfB, xRetirementAge, xDateRetire ...
'xDofB = blah 'we could initialise values here
'xRetirementAge = foo
Inputs xDOfB, xRetirementAge, xDateRetire, ... 'call a function with these variables as arguments
So xFoo and zFoo are just variables held by different procedures, the calling code and the Inputs routine respectively
Now what's weird is what the Inputs routine actually does. It takes a load of arguments, which normally would contain values you want to use in the routine. However they are all immediately overwritten. It's a bit like the following:
Option Explicit
Sub Test()
Dim x As Long
x = 5
Debug.Print x 'prints 5; the value initially stored in x
Foo x 'call Foo, passing the variable x which contains the value 5
Debug.Print x 'Actually prints 2! (see below)
End Sub
Sub Foo(arg1 As Long)
arg1 = 2
Debug.Print arg1 'prints 2, the 5 we passed is gone
End Sub
Why would you ever want to do that?
I suspect 2 possible reasons:
The routine declares a load of arguments, when the intention was actually just to declare a load of variables, in which case the code should in fact be:
Sub Inputs()
Dim zDOB As Date
Dim zRetAge As Double
Dim zRetDate As Date
...
zDOB = Range("B1")
zRetAge = Range("B7")
zRetDate = Range("B8")
...
End Sub
And can be called without any xBlah variables passed
The routine is exploiting the default ByRef passing of arguments, and is in fact designed to overwrite all those variables in place (the reason why x is assigned 2 even though it is declared outside the function)
In that case the signature should be made explicit:
Sub Inputs(ByRef zDOB As Date, ByRef zRetAge As Double, ByRef zRetDate As Date ...)

Related

Why can't I declare my Class Object as such?

I am currently creating a Class Object for a VBA file, its objective is to act as a range dictionary that can be passed single cells. If this cell is contained in one of the ranges, it returns the value associated to the corresponding range key. The class name is "rangeDic".
It is in the making so its functionalities are not implemented yet. Here's the code:
Private zone() As String
Private bounds() As String
Private link As Dictionary
Const ContextId = 33
'Init zone
Private Sub Class_Initialize()
Set link = New Dictionary
ReDim zone(0)
ReDim bounds(0)
End Sub
'properties
Property Get linkDico() As Dictionary
Set linkDico = link
End Property
Property Set linkDico(d As Dictionary)
Set link = d
End Property
Property Get pZone() As String()
pZone = zone
End Property
Property Let pZone(a() As String)
Let zone = a
End Property
'methods
Public Sub findBounds()
Dim elmt As String
Dim i As Integer
Dim temp() As String
i = 1
For Each elmt In zone
ReDim Preserve bounds(i)
temp = Split(elmt, ":")
bounds(i - 1) = temp(0)
bounds(i) = temp(1)
i = i + 2
Next elmt
End Sub
I was trying to instanciate it in a test sub in order to debug mid conception. Here's the code:
Sub test()
Dim rd As rangeDic
Dim ran() As String
Dim tabs() As Variant
Dim i As Integer
i = 1
With ThisWorkbook.Worksheets("DataRanges")
While .Cells(i, 1).Value <> none
ReDim Preserve ran(i - 1)
ReDim Preserve tabs(i - 1)
ran(i - 1) = .Cells(i, 1).Value
tabs(i - 1) = .Cells(i, 3).Value
i = i + 1
Wend
End With
Set rd = createRangeDic(ran, tabs)
End Sub
Public Function createRangeDic(zones() As String, vals() As Variant) As rangeDic
Dim obje As Object
Dim zonesL As Integer
Dim valsL As Integer
Dim i As Integer
zonesL = UBound(zones) - LBound(zones)
valsL = UBound(vals) - LBound(vals)
If zonesL <> valsL Then
Err.Raise vbObjectError + 5, "", "The key and value arrays are not the same length.", "", ContextId
End If
Set obje = New rangeDic
obje.pZone = zones()
For i = 0 To 5
obje.linkDico.add zones(i), vals(i)
Next i
Set createRangeDic = obje
End Function
Take a look at line 2 of Public Function createRangeDic. I have to declare my object as "Object", if I try declaring it as "rangeDic", Excel crashes at line obje.pZone = zones(). Upon looking in the Windows Event Log, I can see a "Error 1000" type of application unknown error resulting in the crash, with "VB7.DLL" being the faulty package.
Why so ? Am I doing something wrong ?
Thanks for your help
Edit: I work under Excel 2016
It looks like this is a bug. My Excel does not crash but I get an "Internal Error".
Let's clarify a few things first, since you're coming from a Java background.
Arrays can only be passed by reference
In VBA an array can only be passed by reference to another method (unless you wrap it in a Variant). So, this declaration:
Property Let pZone(a() As String) 'Implicit declaration
is the equivalent of this:
Property Let pZone(ByRef a() As String) 'Explicit declaration
and of course, this:
Public Function createRangeDic(zones() As String, vals() As Variant) As rangeDic
is the equivalent of this:
Public Function createRangeDic(ByRef zones() As String, ByRef vals() As Variant) As rangeDic
If you try to declare a method parameter like this: ByVal a() As String you will simply get a compile error.
Arrays are copied when assigned
Assuming two arrays called a and b, when doing something like a = b a copy of the b array is assigned to a. Let's test this. In a standard module drop this code:
Option Explicit
Sub ArrCopy()
Dim a() As String
Dim b() As String
ReDim b(0 To 0)
b(0) = 1
a = b
a(0) = 2
Debug.Print "a(0) = " & a(0)
Debug.Print "b(0) = " & b(0)
End Sub
After running ArrCopy my immediate window looks like this:
As shown, the contents of array b are not affected when changing array a.
A property Let always receives it's parameters ByVal regardless of whether you specify ByRef
Let's test this. Create a class called Class1 and add this code:
Option Explicit
Public Property Let SArray(ByRef arr() As String)
arr(0) = 1
End Property
Public Function SArray2(ByRef arr() As String)
arr(0) = 2
End Function
Now create a standard module and add this code:
Option Explicit
Sub Test()
Dim c As New Class1
Dim arr() As String: ReDim arr(0 To 0)
arr(0) = 0
Debug.Print arr(0) & " - value before passing to Let Property"
c.SArray = arr
Debug.Print arr(0) & " - value after passing to Let Property"
arr(0) = 1
Debug.Print arr(0) & " - value before passing to Function"
c.SArray2 arr
Debug.Print arr(0) & " - value after passing to Function"
End Sub
After running Test, my immediate window looks like this:
So, this simple test proves that the Property Let does a copy of the array even though arrays can only be passed ByRef.
The bug
Your original ran variable (Sub test) is passed ByRef to createRangeDic under a new name zones which is then passed ByRef again to pZone (the Let property). Under normal circumstances there should be no issue with passing an array ByRef as many times as you want but here it seems it is an issue because the Property Let is trying to make a copy.
Interestingly if we replace this (inside createRangeDic):
obje.pZone = zones()
with this:
Dim x() As String
x = zones
obje.pZone = x
the code runs with no issue even if obje is declared As rangeDic. This works because the x array is a copy of the zones array.
It looks that the Property Let cannot make a copy of an array that has been passed ByRef multiple times but it works perfectly fine if it was passed ByRef just once. Maybe because of the way stack frames are added in the call stack, there is a memory access issue but difficult to say. Regardless what the problem is, this seems to be a bug.
Unrelated to the question but I must add a few things:
Using ReDim Preserve in a loop is a bad idea because each time a new memory is allocated for a new (larger) array and each element is copied from the old array to the new array. This is very slow. Instead use a Collection as
#DanielDuĊĦek suggested in the comments or minimize the number of ReDim Preserve calls (for example if you know how many values you will have then just dimension the array once at the beginning).
Reading a Range cell by cell is super slow. Read the whole Range into an array by using the Range.Value or Range.Value2 property (I prefer the latter). Both methods returns an array as long as the range has more than 1 cell.
Never expose a private member object of a class if that object is responsible for the internal workings of the class. For example you should never expose the private collection inside a custom collection class because it breaks encapsulation. In your case the linkDico exposes the internal dictionary which can the be modified from outside the main class instance. Maybe it does not break anything in your particular example but just worth mentioning. On the other hand Property Get pZone() As String() is safe as this returns a copy of the internal array.
Add Option Explicit to the top of all your modules/classes to make sure you enforce proper variable declaration. Your code failed to compile for me because none does not exist in VBA unless you have it somewhere else in your project. There were a few other issues that I found once I turned the option on.

How to call a user defined function in vba code

I created a Public function in Module two called "t_value". I now want to use this function in the VBA code for a userform, which uses the input from the userform.
This is the function:
Public Function t_value(theta As Variant)
Dim theta_1 As Integer, theta_2 As Integer
Dim A As Variant, B As Variant, s As Variant
theta_1 = Application.WorksheetFunction.Floor(theta, 5)
theta_2 = Application.WorksheetFunction.Ceiling(theta, 5)
A = theta - theta_1
B = theta_2 - theta_1
s = A / B
t_value = s
End Function
Here is the code I would like to use the function above in:
Private Sub Submit_Click()
Dim theta As Variant, alpha As Variant, t As Variant, u As Variant
theta = UserForm1.theta_input.Value
alpha = UserForm1.alpha_input.Value
t = Application.WorksheetFunction.t_value(theta)
End Sub
Normally "Application.WorksheetFunction.[function]" works, but it wouldn't work for me in this situation - I thought it may be due to the fact I created the formula. Would it be easier to just put the formula into the Sub? I was worried about runtime. I'm rather new, so I'm not completely familiar with VBA syntax.
Application.WorksheetFunction is a class defined in the Excel library; you can find it in the Object Browser (F2):
A public Function in a standard module is just a function that can be invoked from a worksheet cell (provided it doesn't have side-effects), just as well as from anywhere in the workbook's VBA project: you can't write any VBA code that "becomes a member" of a class that's defined in a library you're referencing.
So if you have a function called MyFunction in a module called Module1, you can invoke it like this:
foo = MyFunction(args)
Or like this:
foo = Module1.MyFunction(args)
So in this case:
t = t_value(theta)
Would it be easier to just put the formula into the Sub?
Nope, because a Sub won't return a value (however, you can pass variables ByRef):
Sub t_value(theta as variant, ByRef t as Variant)
Dim theta_1 As Integer, theta_2 As Integer
Dim A As Variant, B As Variant, s As Variant
theta_1 = Application.WorksheetFunction.Floor(theta, 5)
theta_2 = Application.WorksheetFunction.Ceiling(theta, 5)
A = theta - theta_1
B = theta_2 - theta_1
s = A / B
t = s '## Assign the value to the ByRef 't' variable and it should retain its value in the calling procedure
End Sub
Whether you choose to put this function in a module (Public) or in the user form module is a design decision that depends on whether you want the function to be generally available outside of the form instance(s). Whether you choose to make this function a sub is a bit different -- I'd probably recommend against it following the general best practice that Functions should return values and Subroutines should just perform actions and/or manipulate objects.
Directly use
t = t_value(theta)
, instead of
t = Application.WorksheetFunction.t_value(theta)

Excel vba Compile error - Argument not optional,

I'm trying to figure this out and can't.
I keep getting an error: "Compile error - Argument not optional". I am supplying the arguments and they are set as Optional!
Trying to pass a string and an array to a function and count occurrences of the array strings within the string passed.
Code stops running at the line:
Public Function countTextInText(Optional text As String, Optional toCountARR As Variant) As Integer
with a "Compile error: Argument not optional" message highlighting the Val in the line:
For Each Val In toCountARR
Full code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim nameR As Range
Dim colR As Range
Dim TKRcnt As Integer
Dim TKRarr() As Variant
TKRarr = Array("TKR", "THR", "Bipolar")
Dim ORIFcnt As Integer
Dim ORIFarr() As Variant
TKRarr = Array("ORIF", "Ilizarov", "PFN")
Set nameR = Range("P2:P9")
Set colR = Range("B2:B50,G2:G50,L2:L50")
For Each namecell In nameR
For Each entrycell In colR
If entrycell.text = namecell.text Then
TKRcnt = countTextInText(entrycell.Offset(0, 2).text, TKRarr)
ORIFcnt = countTextInText(entrycell.Offset(0, 2).text, TKRarr)
End If
Next entrycell
MsgBox (namecell.text & " TKR count: " & TKRcnt & " ORIF count: " & ORIFcnt)
Next namecell
End Sub
Public Function countTextInText(Optional text As String, Optional toCountARR As Variant) As Integer
Dim cnt As Integer
Dim inStrLoc As Integer
For Each Val In toCountARR
inStrLoc = InStr(1, text, Val)
While inStrLoc <> 0
inStrLoc = InStr(inStrLoc, text, Val)
cnt = cnt + 1
Wend
Next Val
Set countTextInText = cnt
End Function
Val is a VBA function which requires a single, mandatory, argument - therefore the compiler generates the message saying "Argument not optional" if you don't provide that argument. (MSDN documentation of Val)
It is a bad idea to use VBA function names as variable names, so I would recommend you don't use Val as a variable name - use myVal or anything else that VBA hasn't already used.
If you really want to use Val (and you are sure that you won't be needing to access the Val function at all), you can use it as a variable name if you simply declare it as such, e.g.
Dim Val As Variant
You will also have problems with your line saying
Set countTextInText = cnt
as countTextInText has been declared to be an Integer, and Set should only be used when setting a variable to be a reference to an object. So that line should be
countTextInText = cnt
For those coming late to this question because of the question's title, as I did, having received this error while using the .Find method -
In my case, the problem was that the variable I was Seting was not Dimd at top of function.
My Example
Sub MyTest()
Dim tst, rngAll
rngAll = [a1].CurrentRegion
tst = fnFix1Plus1InValues(ByVal rngAll As Range)
End Sub
Public Function fnFix1Plus1InValues(ByVal rngAll As Range) As Boolean
Dim t1, t2, arr, Loc '<=== Needed Loc added here
Set Loc = rngAll.Find(What:="+", LookIn:=xlValues, LookAt:=xlPart, MatchCase:=False)
If Not Loc Is Nothing Then
Do Until Loc Is Nothing
t1 = Loc.Value
If fnContains(t1, "+") Then
'Do my stuff
End If
Set Loc = rngAll.FindNext(Loc)
Loop
End If
End Function 'fnFix1Plus1InValues

Can private sub pass newly created variable to main()?

Can private sub program being in the same module as main after receiving value from main, creates new variable and pass it back to main?
This is what I am trying to do, but I am having some difficulties.
For example, in testSUB below I altered the string. Can I pass extraSTRING and newSTRING back to main? Any examples would be helpful.
Module module1
Sub main()
Dim l As String
Dim I As Long = 1
Dim A As String
testsub(l, A, I)
End Sub
Private Sub testSub(l As String, A As String, I As Long)
Dim extraSTRING As String = "extraTEXT"
Dim newSTRING As String = l & extraSTRING
End Sub
End Module
To return a value you could turn your Sub into a Function:
Private Function testFunction (ByVal arg1 As String) As String
Return arg1 & " and some more text"
End Function
To call the above Function and assign the value returned use this code:
Dim a As String = testFunction("some text")
'Output:
'a = "some text and some more text"
Below is a screenshot of the code with the output:
Alternatively you can use ByRef:
Specifies that an argument is passed in such a way that the called procedure can change the value of a variable underlying the argument in the calling code.
ByRef differs slightly from ByVal:
Specifies that an argument is passed in such a way that the called procedure or property cannot change the value of a variable underlying the argument in the calling code.
Below is some sample code showing you the differences in action:
Module Module1
Sub Main()
Dim a As Integer = 0
Dim b As Integer = 0
Dim c As Integer = 0
testSub(a, b, c)
'Output:
'a = 0
'b = 0
'c = 3
End Sub
Private Sub testSub(arg1 As Integer, ByVal arg2 As Integer, ByRef arg3 As Integer)
arg1 = 1
arg2 = 2
arg3 = 3
End Sub
End Module
By not specifying a modifier in VB.NET (as shown with arg1 above) the compiler by default will use ByVal.
It would be good to note here that although VB.NET uses ByVal by default if not specified, VBA does not and instead by default uses ByRef. Beware of this should you ever port code from one to the other.
Below is a screenshot of the code with the output:
Using your code as an example:
Sub main()
Dim l As String
Dim A As String
Dim I As Long = 1
testSub(l, A, I)
End Sub
To pass the variables l, A and I and have their value changed you would change your method to use the modifier ByRef.
Private Sub testSub(ByRef l As String, ByRef A As String, ByRef I As Long)
l = "TEXT"
A = "extra" & l
I = 100
End Sub
Below is a screenshot of the code with the output:

Can I Evaluate An Excel VB Constant That Is In String Format?

Is it possible to Evaluate a String which contains a valid Excel VB Constant's Name
to return that Constant's Value?
eg
Dim ConstantName as String
Dim ConstantValue as Long
ConstantName="xlValues"
ConstantValue= UnknownFunction(ConstantName)
'would set ConstantValue=-4163
Fun!
Option Explicit
Function getConstantValue(constStr As String) As Variant
Dim oMod As VBIDE.CodeModule
Dim i As Long, _
num As Long
Set oMod = ThisWorkbook.VBProject.VBComponents("Module1").CodeModule
For i = 1 To oMod.CountOfLines
If oMod.Lines(i, 1) = "Function tempGetConstValue() As Variant" Then
num = i + 1
Exit For
End If
Next i
oMod.InsertLines num, "tempGetConstValue = " & constStr
getConstantValue = Application.Run("tempGetConstValue")
oMod.DeleteLines num
End Function
Function tempGetConstValue() As Variant
End Function
All code must be in a module called Module1. That can be changed pretty simply by changing the text "Module1" in the routine.
You'll need to add a reference to Microsoft Visual Basic for Applications Extensibility x.x
There are a number of ways this could fail. Let me know if you have any problems with it :)
Instead of using constants, you could use a dictionary
Dim dict As Object
Sub InitialiseDict()
Set dict = CreateObject(Scripting.Dictionary)
dict("xlValues") = -4163
dict("const1") = value1
...
dict("constN") = valueN
End Sub
ConstValue = dict("xlValues")
Is using the string value necessary?
Dim anyConstant as Long
anyConstant = xlValues
msgbox anyConstant
Set anyConstant to any xl constant you please, they are all enumerated Long values.
The first solution offered is indeed much more fun however.

Resources