Variable not defined in VBA script - vba

I'm getting into the good habit of using Option Explicit at the top of all my MS Access VBA scripts. However, for this function it's giving me a Variable not defined error highlighting the line that begins with Set objSysInfo =
Public Function GetUser(Optional whatpart = "username")
Dim returnthis As String
If whatpart = "username" Then GetUser = Environ("USERNAME"): Exit Function
Set objSysInfo = CreateObject("ADSystemInfo")
Set objUser = GetObject("LDAP://" & objSysInfo.UserName)
Select Case whatpart
Case "fullname": returnthis = objUser.FullName
Case "firstname", "givenname": returnthis = objUser.givenName
Case "lastname": returnthis = objUser.LastName
Case Else: returnthis = Environ("USERNAME")
End Select
GetUser = returnthis
End Function
Might I be missing a specific reference?

Option Explicit forces you to declare all variables.
This means you must declare all variables you're using, otherwise your code won't compile - as you've noticed.
You declared local variable returnthis as a String, using the Dim keyword:
Dim returnthis As String
That's how you declare a local (procedure scope) variable.
You could declare variables at module scope using the Private keyword (Dim works as well, but better keep Dim for locals)
Option Explicit
Private foo As Object
How you scope variables depends on where you're using them - if a variable is only ever used in one single procedure, then it's better to scope it to that procedure and declare it as a local variable.
In this case you have objSysInfo and objUser that aren't declared anywhere.
Dim objSysInfo As Object
Dim objUser As Object
I know we're not on Code Review, but while we're here there are other issues with your code:
The return type of the function is implicitly Variant. Append As String to the end of the function's signature.
The optional parameter whatpart is implicitly Variant as well, and implicitly passed ByRef, but you only need to read it, not write and return to the caller - so declare it Optional ByVal whatpart As String = "username".
Prefer the Environ$ function over Environ - both do the same thing, but Environ returns a Variant, that you're implicitly converting to a String - best use Environ$ and work with a string from the start.
Instead of relying on magic, hard-coded strings, use an Enum value and Select Case over the possible legal values:
Public Enum NamePart
EnvironmentDefined
FullName
FirstName
LastName
End Enum
The full code could look like this:
Public Enum NamePart
EnvironmentDefined
FullName
FirstName
LastName
End Enum
Public Function GetUserName(Optional ByVal part As NamePart = EnvironmentDefined) As String
Dim result As String
Dim sysInfo As Object
Dim userInfo As Object
If Namepart = EnvironmentDefined Then
GetUser = Environ$("USERNAME")
Exit Function
End If
Set sysInfo = CreateObject("ADSystemInfo")
Set userInfo = GetObject("LDAP://" & sysInfo.UserName)
Select Case part
Case FullName
result = userInfo.FullName
Case FirstName
result = userInfo.GivenName
Case LastName
result = userInfo.LastName
Case Else
result = Environ$("USERNAME")
End Select
GetUserName = result
End Function
Note, I didn't include a GivenName enum member, since it's redundant; calling code can do this:
Dim value As String
value = GetUserName(FirstName)

Related

Replace a variable Environ in vba

Please, I had already create à variable Environ("MYXLSPATH")
Dim vr As String
vr = "SETX MYXLSPATH """ & ThisWorkbook.FullName & """"
Call Shell(vr)
And now, I want to replace the content of this variable by: "NAME"
Dim vr As String
Environ.RemoveItem ("MYXLSPATH")
vr = "SETX MYXLSPATH "" NAME """
Call Shell(vr)
But, It doesn't work, can you help me please ?
The second set of code should be:
Dim vr As String
vr = "SETX MYXLSPATH ""NAME"""
Call Shell(vr)
I've made two changes:
Removed the Environ.RemoveItem line. It's not needed and seems to be problematic.
Removed the space either side of NAME. So this means the environment variable is set to NAME and not  NAME 
End result:
Instead of running a CMD.EXE command you can call in to the Shell object to get to the environment variables directly. To replace, just call the set again.
Here are procedures to set and get:
Option Explicit
'#Description("Set environment variable value. Defaults to user space. System space requires elevated process to modify.")
Public Sub EnvironSetItem(ByVal environVariable As String, ByVal newValue As String, Optional ByVal strType As String = "User")
With CreateObject("WScript.Shell").Environment(strType)
.Item(environVariable) = newValue
End With
End Sub
'#Description("Get environment variable value. Defaults to userspace.")
Public Function EnvironGetItem(ByVal environVariable As String, Optional ByVal strType As String = "User") As String
With CreateObject("WScript.Shell").Environment(strType)
EnvironGetItem = .Item(environVariable)
End With
End Function

VBA Function Passing Multi Variables back to Sub

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.

In VBA how do you compare 2 instances of a User Defined Type (UDT)? I'm getting a Type mismatch

I have a module that defines a UDT as follows:
Private Type UserData
uName As String 'user name
uDate As Date 'date/time of last interaction
End Type
I have a simple test function that I'm trying to use to compare two different instances of the UDT as follows:
Sub TestCheck()
Dim testRec(1) As UserData
testRec(0).uName = "a"
testRec(0).uDate = Date
testRec(1) = testRec(0)
If testRec(1) = testRec(0) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
I get Compile error: Type mismatch on testRec(1) = testRec(0)
I really would rather not have to loop through each member of each instance in order to check for equivalency. Aren't UDTs supposed to act as variables? If I have to loop through each member of each instance to compare them, then it really doesn't save anything for me to use the UDTs. Is there a way to do the comparison without looping through the members?
For anyone who has the same question, based on Brian M Stafford's comments, the simple answer is no.
However, here's a simple function to get the job done:
Private Function UserDataEqual(ByRef varA As UserData, ByRef varB As UserData) As Boolean
If varA.uName = varB.uName _
And varA.uDate = varB.uDate Then
UserDataEqual = True
Else
UserDataEqual = False
End If
End Function
It would be used as follows:
Sub TestCheck()
Dim testRec(1) As UserData
testRec(0).uName = "a"
testRec(0).uDate = Date
testRec(1) = testRec(0)
If UserDataEqual(testRec(1), testRec(0)) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
Thanks for answering my questions Brian.
This type of activity is what Classes are for. Instead of a user defined type create a class with appropriate methods. Here we have defined a Class called UserData which has a predeclared Id so that we can use the class as a UserData Factory. In this example we have
UserData Class
' User Rubberduck annotations to set the predecalredId to True
'#PredeclaredId
Option Explicit
Public Enum UserDataType
udDate
udName
End Enum
Private Type Properties
UserData As Object
End Type
Private p As Properties
Public Function Make(ByVal ipName As String, ByVal ipDateAndTime As Date) As UserData
With New UserData
Set Make = .Self(ipName, ipDateAndTime)
End With
End Function
Public Function Self(ByVal ipName As String, ByVal ipDateAndTime As Date) As UserData
' Use late bound crreation of a scripting dictionary to avoid reference issues
Set p.UserData = CreateObject("Scripting.Dictionary")
With p.UserData
.Add udName, ipName
.Add udDate, ipDateAndTime
End With
Set Self = Me
End Function
Public Property Get Item(ByVal ipEnum As Long) As Variant
Item = p.UserData.Item(ipEnum)
End Property
Public Property Let Item(ByVal ipEnum As Long, ByVal ipValue As Variant)
p.UserData.Item(ipEnum) = ipValue
End Property
Public Function SameAs(ByVal ipUserData As UserData) As Boolean
SameAs = False
Dim myIndex As Long
For myIndex = 0 To p.UserData.Count - 1
If Me.Item(myIndex) <> ipUserData.Item(myIndex) Then Exit Function
Next
SameAs = True
End Function
This class makes the creation of user data types a bit easier as we can now just say UserData,Make( ,
So the text sub can become
Option Explicit
Public Sub TestCheck()
Dim testRec(1) As UserData
Set testRec(0) = UserData.Make("a", Date)
Set testRec(1) = UserData.Make("b", Date)
If testRec(1).SameAs(testRec(0)) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
As you can see. You can see. To change the UserData class for a different set of members you only have to change the enumeration (provided you keep to simple variables).
Simple answer probably what you were looking for (although won't work in your situation I'm afraid, I'll explain):
'#Description("Returns the count of bytes which match over length")
Public Declare PtrSafe Function RtlCompareMemory Lib "ntdll" ( _
ByRef a As Any, _
ByRef b As Any, _
ByVal Length As LongPtr _
) As LongPtr
Called like:
Debug.Assert RtlCompareMemory(a, b, LenB(a)) = LenB(a) 'checks every byte matches
'Or as a function ?UserDataMatch(a,b)
Public Function UserDataMatch(ByRef a As UserData, ByRef b As UserData) As Boolean
UserDataMatch = RtlCompareMemory(a, b, LenB(a)) = LenB(a)
End Function
The catch is this won't work for you because Strings are stored in the UDT as pointers to some variable length block of memory containing their actual values (and these pointers will never match in VBA). So my approach only works for fixed sized UDTs, e.g.:
Public Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
Public Type UDT
IID As GUID 'nested is fine
ClsID As GUID
RefCount As Long
MyClass As Object 'even classes and objects work - if the UDTs point to the same instance
End Type
But not for a UDT which contains a string or some other immutable reference type where the pointers can never be shared between two variables without low level hackery.

Checking the passed variable

Is it possible to check if the passed variable in a function is a specific text? For example, I have a function:
Function dateCheck2(dateValue As String) As Boolean
If IIf(IsDate(frmResponse.txtEndDate), Format(CDate(frmResponse.txtEndDate), _
"mm\/dd\/yyyy"), "") = dateValue Then
dateCheck2 = True
Exit Function
End If
End Function
Then I have to call this in a UForm, lets say :
dateCheck2(sample)
What I want is to check if the passed variable is sample. Is there any way to do that?
Or you can create your own class (in VBA its called data type) which will hold variable name and values. Something like this
Public Type myFluffyType
fluffyName as string
fluffyValue as string
end Type
Then you can dim your variable like this
Dim fluffyVariable as myFluffyType
and initialize
fluffyVariable.fluffyName = "fluffyVariable"
fluffyVariable.fluffyValue = "notSoMuchFluffyValue"
and then when you passed it into your function you have all informations you need
Edit: on your problem you can do something like this. But im not sure what you exactly wana do next
Public type myType
varName as string
varValue as string
end type
Sub callDateCheck2
Dim sample as myType
sample.varName = "sample"
sample.varValue = "whateverValues"
Call dateCheck2(sample)
end sub
Function dateCheck2(dateValue As myType) As Boolean
if dateValue.varName = "sample" then
msgbox "everything ok"
else
msgbox "bad variable name"
end function
end if
If IIf(IsDate(frmResponse.txtEndDate), Format(CDate(frmResponse.txtEndDate), _
"mm\/dd\/yyyy"), "") = dateValue Then
dateCheck2 = True
Exit Function
End If
End Function

Declaring constant in VBA from a INI file

I have some const in a VBA module but I would like to put them in a INI file because I have other applications that use the same constants. This works:
Public Const PATH_DB As String = "\\server\folder\bdd.mdb"
This however doesn't work:
Public Const PATH_DB As String = getFromIni("path","db","C:\config.ini")
Public Function getFromIni(ByVal strSectionName As String, ByVal strEntry As String, ByVal strIniPath As String) As String
Dim x As Long
Dim sSection As String, sEntry As String, sDefault As String
Dim sRetBuf As String, iLenBuf As Integer, sFileName As String
Dim sValue As String
sSection = strSectionName
sEntry = strEntry
sDefault = ""
sRetBuf = Strings.String$(256, 0) '256 null characters
iLenBuf = Len(sRetBuf$)
sFileName = strIniPath
x = GetPrivateProfileString(sSection, sEntry, "", sRetBuf, iLenBuf, sFileName)
sValue = Strings.Trim(Strings.Left$(sRetBuf, x))
If sValue <> "" Then
getFromIni = sValue
Else
getFromIni = vbNullChar
End If
End Function
# C:\config.ini
[path]
db=\\server\folder\bdd.mdb
My getFromIni function actually works pretty well but not when I want to declare constants (it doesn't compile at all). I tried a global variable instead but for some reason, it doesn't work either, the variable cannot be found when used from another form in the same project (it only works when it's declared as a const, but I can't declare it as a const when getting the value from a function).
What's wrong?
You cannot assign a function call as the value of a CONST string. I would expect you to receive the error "Constant expression required" when running this.
Change
Public Const PATH_DB As String = getFromIni("path","db","C:\config.ini")
to
Public PATH_DB As String
Place the following call that gets the value from INI file in a initialize method (say Database Open event)
PATH_DB = getFromIni("path","db","C:\config.ini")