Since VBA/VB6 treats Enums
as Longs
under the hood, so I suggest a generic wrapper to which you can supply your Enums
and their string representations. This way you avoid having to have a separate class for each one.
Let's start with a generic class that serves a two-way or "bi-directional" map, which I have aptly named BiDirectionalMap
.
BiDirectionalMap.cls
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "BiDirectionalMap"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
'@Exposed
'@PredeclaredId
Option Explicit
Private Enum BiDirectionalMapErrors
MismatchedLength = vbObjectError + 1024
SetsNotIterable
KeyDoesNotExist
ValueDoesNotExist
KeyOrValueDoesNotExist = KeyDoesNotExist Or ValueDoesNotExist
End Enum
Private Const MismatchedLengthErrorDesc As String = "Keys and Values must have have the same number of values (i.e. one-to-one correspondence)."
Private Const SetsNotIterableErrorDesc As String = "Key(s), Value(s), or both are is not iterable. For single values, wrap in 'Array()' function."
Private Const DoesNotExistErrorDesc As String = "does not exist. Ensure that the data type is consistent with the original "
Private Type TBiDirectionalMap
KeyCompareMethod As VBA.VbCompareMethod
ValueCompareMethod As VBA.VbCompareMethod
KeysDict As Scripting.Dictionary
ValuesDict As Scripting.Dictionary
End Type
Private this As TBiDirectionalMap
Public Function Create(ByVal keys As Variant, ByVal values As Variant, _
Optional ByVal keyCompareMethod As VBA.VbCompareMethod = vbBinaryCompare, _
Optional ByVal valueCompareMethod As VBA.VbCompareMethod = vbBinaryCompare) As BiDirectionalMap
Errors.GuardNonDefaultInstance Me, BiDirectionalMap, VBA.TypeName(Me)
Dim result As BiDirectionalMap
Set result = New BiDirectionalMap
result.KeyCompareMethod = keyCompareMethod
result.ValueCompareMethod = valueCompareMethod
result.AddRange keys, values
Set Create = result
End Function
Public Property Get KeyCompareMethod() As VBA.VbCompareMethod
KeyCompareMethod = this.KeyCompareMethod
End Property
Friend Property Let KeyCompareMethod(ByVal value As VBA.VbCompareMethod)
Errors.GuardNullReference this.KeysDict, VBA.TypeName(Me) & "." & "KeyCompareMethod"
this.KeyCompareMethod = value
If this.KeysDict.Count = 0 Then
this.KeysDict.CompareMode = this.KeyCompareMethod
End If
End Property
Public Property Get ValueCompareMethod() As VBA.VbCompareMethod
ValueCompareMethod = this.ValueCompareMethod
End Property
Friend Property Let ValueCompareMethod(ByVal value As VBA.VbCompareMethod)
Errors.GuardNullReference this.ValuesDict, VBA.TypeName(Me) & "." & "ValueCompareMethod"
this.ValueCompareMethod = value
If this.ValuesDict.Count = 0 Then
this.ValuesDict.CompareMode = this.ValueCompareMethod
End If
End Property
Public Property Get Count() As Long
If this.KeysDict.Count = this.ValuesDict.Count Then
Count = this.KeysDict.Count
Else
Errors.ThrowError BiDirectionalMapErrors.MismatchedLength, _
VBA.TypeName(Me) & "." & "Count()", _
GetErrorMessage(BiDirectionalMapErrors.MismatchedLength)
End If
End Property
Public Property Get Keys() As Scripting.Dictionary
Set Keys = this.KeysDict
End Property
Public Property Get Values() As Scripting.Dictionary
Set Values = this.ValuesDict
End Property
Public Property Get Key(ByVal valueKey As Variant) As Variant
If this.ValuesDict.Exists(valueKey) Then
AssignProperty(Key) = this.ValuesDict(valueKey)
Else
Errors.ThrowError BiDirectionalMapErrors.KeyDoesNotExist, _
VBA.TypeName(Me) & "." & "Get Key()", _
GetErrorMessage(BiDirectionalMapErrors.KeyDoesNotExist)
End If
End Property
Public Property Let Key(ByVal valueKey As Variant, ByVal value As Variant)
this.KeysDict(valueKey) = value
this.ValuesDict(value) = valueKey
End Property
Public Property Set Key(ByVal valueKey As Variant, ByVal value As Variant)
Set this.KeysDict(valueKey) = value
Set this.ValuesDict(value) = valueKey
End Property
Public Property Get Value(ByVal keyValue As Variant) As Variant
If this.KeysDict.Exists(keyValue) Then
AssignProperty(Value) = keyValue
Else
Errors.ThrowError BiDirectionalMapErrors.ValueDoesNotExist, _
VBA.TypeName(Me) & "." & "Get Value()", _
GetErrorMessage(BiDirectionalMapErrors.ValueDoesNotExist)
End If
End Property
Public Property Let Value(ByVal keyValue As Variant, ByVal key As Variant)
this.ValuesDict(keyValue) = key
this.KeysDict(key) = keyValue
End Property
Public Property Set Value(ByVal keyValue As Variant, ByVal key As Variant)
Set this.ValuesDict(keyValue) = key
Set this.KeysDict(key) = keyValue
End Property
Public Function ContainsKey(ByVal key As Variant) As Boolean
ContainsKey = this.KeysDict.Exists(key)
End Function
Public Function ContainsValue(ByVal value As Variant) As Boolean
ContainsValue = this.ValuesDict.Exists(value)
End Function
Public Function ContainsPair(ByVal key As Variant, ByVal value As Variant) As Boolean
ContainsPair = (ContainsKey(key) And ContainsValue(value))
End Function
Public Sub AddRange(ByVal keys As Variant, ByVal values As Variant)
Errors.GuardNullReference this.KeysDict, VBA.TypeName(Me) & "." & "AddRange()"
Errors.GuardNullReference this.ValuesDict, VBA.TypeName(Me) & "." & "AddRange()"
Dim keysColl As Collection
Set keysColl = IterableToCollection(keys)
Dim valuesColl As Collection
Set valuesColl = IterableToCollection(values)
If keysColl.Count <> valuesColl.Count Then
Errors.ThrowError BiDirectionalMapErrors.MismatchedLength, _
VBA.TypeName(Me) & "." & "AddRange()", _
GetErrorMessage(BiDirectionalMapErrors.MismatchedLength)
End If
Dim i As Long
For i = 1 To keysColl.Count
Add keysColl(i), valuesColl(i)
Next i
End Sub
Public Sub Add(ByVal key As Variant, ByVal value As Variant)
On Error GoTo CleanFail
this.KeysDict.Add key, value
this.ValuesDict.Add value, key
CleanExit:
Exit Sub
CleanFail:
Errors.ThrowError Err.Number, VBA.TypeName(Me) & "." & "Add()", Err.Description
Resume CleanExit
End Sub
Public Sub RemoveRange(ByVal keysOrValues As Variant)
Dim keysOrValueColl As Collection
Set keysOrValueColl = IterableToCollection(keysOrValues)
Dim i As Long
For i = 1 To keysOrValueColl.Count
Remove keysOrValueColl(i)
Next i
End Sub
Public Sub Remove(ByVal keyOrValue As Variant)
If ContainsKey(keyOrValue) Then
Dim val As Variant
val = Value(keyOrValue)
this.KeysDict.Remove keyOrValue
this.ValuesDict.Remove val
ElseIf ContainsValue(keyOrValue) Then
Dim key As Variant
key = Key(keyOrValue)
this.ValuesDict.Remove key
this.KeysDict.Remove keyOrValue
Else
Errors.ThrowError BiDirectionalMapErrors.KeyOrValueDoesNotExist, _
VBA.TypeName(Me) & "." & "Remove()", _
GetErrorMessage(BiDirectionalMapErrors.KeyOrValueDoesNotExist)
End If
End Sub
Public Sub Clear()
If this.KeysDict.Count > 0 Then
this.KeysDict.RemoveAll
End If
If this.ValuesDict.Count > 0 Then
this.ValuesDict.RemoveAll
End If
End Sub
'*****************************************************************************************
'Private Methods / Properties
'*****************************************************************************************
Private Sub Class_Initialize()
Errors.GuardDoubleInitialization this.KeysDict, VBA.TypeName(Me) & "." & "Class_Initialize()"
Set this.KeysDict = New Scripting.Dictionary
Errors.GuardDoubleInitialization this.ValuesDict, VBA.TypeName(Me) & "." & "Class_Initialize()"
Set this.ValuesDict = New Scripting.Dictionary
End Sub
Private Function IterableToCollection(ByVal iterable As Variant) As Collection
Select Case VBA.VarType(iterable)
Case (vbArray + vbVariant) '8204; https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/vartype-function
Set IterableToCollection = ArrayToCollection(iterable)
Case vbObject
Set IterableToCollection = ObjectToCollection(iterable)
End Select
End Function
Private Function ArrayToCollection(ByVal variantArray As Variant) As Collection
ValidateArrayDimensions variantArray, "ArrayToCollection()"
Dim result As Collection
Set result = New Collection
Dim dimensions As Long
dimensions = GetArrayDimensions(variantArray)
Dim i As Long
On Error GoTo CleanFail
Select Case dimensions
Case 1
For i = LBound(variantArray) To UBound(variantArray)
result.Add variantArray(i)
Next i
Case 2
Dim secondDimLBound As Long
secondDimLBound = LBound(variantArray, 1)
For i = LBound(variantArray, 1) To UBound(variantArray, 1)
result.Add variantArray(i, secondDimLBound)
Next i
End Select
Set ArrayToCollection = result
CleanExit:
Exit Function
CleanFail:
ManageIterableError Err.Number, "ArrayToCollection()"
Resume CleanExit
End Function
Private Function ObjectToCollection(ByVal obj As Variant) As Collection
Dim result As Collection
Set result = New Collection
On Error GoTo CleanFail
Dim value As Variant
For Each value In obj
result.Add value
Next
Set ObjectToCollection = result
CleanExit:
Exit Function
CleanFail:
ManageIterableError Err.Number, "ObjectToCollection()"
Resume CleanExit
End Function
Private Property Let AssignProperty(ByRef returnValue As Variant, ByVal value As Variant)
If IsObject(value) Then
Set returnValue = value
Else
returnValue = value
End If
End Property
'*****************************************************************************************
'Error Handling
'*****************************************************************************************
Private Sub ValidateArrayDimensions(ByVal variantArray As Variant, ByVal methodName As String)
Dim dimensions As Long
dimensions = GetArrayDimensions(variantArray)
Select Case dimensions
Case Is > 2
Errors.ThrowError BiDirectionalMapErrors.SetsNotIterable, VBA.TypeName(Me) & "." & methodName
Case Is = 2
Errors.GuardExpression IsMultiColumnArray(variantArray), BiDirectionalMapErrors.SetsNotIterable, VBA.TypeName(Me) & "." & methodName
End Select
End Sub
Private Sub ManageIterableError(ByVal errorNumber As Long, ByVal methodName As String)
Select Case errorNumber
Case Errors.ObjectDoesNotSupportMethodRuntimeError, Errors.TypeMismatchRuntimeError
Errors.ThrowError BiDirectionalMapErrors.SetsNotIterable, _
VBA.TypeName(Me) & "." & methodName, _
GetErrorMessage(BiDirectionalMapErrors.SetsNotIterable)
Case Else
Errors.ThrowError errorNumber, _
VBA.TypeName(Me) & "." & methodName, _
Err.Description
End Select
End Sub
Private Function GetErrorMessage(ByVal errorNumber As BiDirectionalMapErrors) As String
Dim result As String
Select Case errorNumber
Case BiDirectionalMapErrors.MismatchedLength
result = MismatchedLengthErrorDesc
Case BiDirectionalMapErrors.SetsNotIterable
result = SetsNotIterableErrorDesc
Case BiDirectionalMapErrors.KeyDoesNotExist
result = "Key " & DoesNotExistErrorDesc & " keys."
Case BiDirectionalMapErrors.ValueDoesNotExist
result = "Value " & DoesNotExistErrorDesc & " values."
Case BiDirectionalMapErrors.KeyOrValueDoesNotExist
result = "Key or Value " & DoesNotExistErrorDesc & " keys or values."
End Select
GetErrorMessage = result
End Function
Private Function GetArrayDimensions(ByVal variantArray As Variant) As Long
Dim index As Long
On Error Resume Next
Err.Clear
Dim upperBound As Long
Do
index = index + 1
upperBound = UBound(variantArray, index)
Loop Until Err.Number <> 0
On Error GoTo 0
GetArrayDimensions = (index - 1)
End Function
Private Function IsMultiColumnArray(ByVal variantArray As Variant) As Boolean
On Error Resume Next
Err.Clear
Dim value As Variant
value = variantArray(LBound(variantArray), 2)
IsMultiColumnArray = (Err.Number = 0)
On Error GoTo 0
End Function
Essentially, all we are doing is tracking keys and values in 2 separate dictionaries. The underlying KeysDict
has key == key and value == value, while the ValuesDict
has key == value and value == key. We want to ensure that they are in sync at all times so as to maintain the bijection of the keys/values.
Now we can follow up with a wrapper to work specifically with Enums
.
EnumMap.cls
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "EnumMap"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
'@Exposed
'@PredeclaredId
Option Explicit
Private Mappings As BiDirectionalMap
Public Function Create(ByVal enumns As Variant, ByVal names As Variant, _
Optional ByVal nameCompareMethod As VBA.VbCompareMethod = vbBinaryCompare) As EnumMap
Errors.GuardNonDefaultInstance Me, EnumMap, VBA.TypeName(Me) & "." & "Create()"
Dim result As EnumMap
Set result = New EnumMap
result.SetMappings enumns, names, nameCompareMethod
Set Create = result
End Function
Public Property Get Count() As Long
Errors.GuardNullReference Mappings, VBA.TypeName(Me) & "." & "Count()"
Count = Mappings.Count
End Property
Public Property Get Enumns() As Scripting.Dictionary
Errors.GuardNullReference Mappings, VBA.TypeName(Me) & "." & "Enumns()"
Set Enumns = Mappings.Keys
End Property
Public Property Get Names() As Scripting.Dictionary
Errors.GuardNullReference Mappings, VBA.TypeName(Me) & "." & "Names()"
Set Names = Mappings.Values
End Property
Public Function ToEnum(ByVal name As String) As Long
Errors.GuardNullReference Mappings, VBA.TypeName(Me) & "." & "ToEnum()"
ToEnum = Mappings.Key(name)
End Function
Public Function ToName(ByVal enumValue As Long) As String
Errors.GuardNullReference Mappings, VBA.TypeName(Me) & "." & "ToName()"
ToName = Mappings.Value(enumValue)
End Function
Friend Sub SetMappings(ByVal enumns As Variant, ByVal names As Variant, _
ByVal nameCompareMethod As VBA.VbCompareMethod)
If Mappings Is Nothing Then
Set Mappings = BiDirectionalMap.Create(enumns, names, vbBinaryCompare, nameCompareMethod)
Exit Sub
End If
If Mappings.Count = 0 Then
Set Mappings = BiDirectionalMap.Create(enumns, names, vbBinaryCompare, nameCompareMethod)
End If
End Sub
The we can test like so:
Public Enum TestingEnum
Apples
Oranges
Cars
Lemons
Trees
Giraffes
End Enum
Private Sub Tester()
Dim enumValues() As Variant
enumValues() = Array(Apples, Oranges, Cars, Lemons, Trees, Giraffes)
Dim enumNames() As Variant
enumNames() = Array("Apples", "Oranges", "Cars", "Lemons", "Trees", "Giraffes")
Dim map As EnumMap
Set map = EnumMap.Create(enumValues, enumNames)
Debug.Print map.ToEnum("Cars")
Debug.Print map.ToName(Cars)
Debug.Print map.Count()
Debug.Assert map.names.Exists("Trucks")
Debug.Assert map.enumns.Exists(10)
End Sub
The only caveat with a generic wrapper is that does not support intellisense, but the reusability/portability that it affords outways this in my mind.
For reference, below is the Errors module referenced in the 2 classes above.
Errors.bas
Attribute VB_Name = "Errors"
Option Explicit
Public Const InvalidProcedureCallOrArgumentError As Long = 5
Public Const TypeMismatchRuntimeError As Long = 13
Public Const ObjectDoesNotSupportMethodRuntimeError As Long = 438
Public Const ObjectAlreadyInitializedError As Long = 1004 'Technically this is an Application-defined or object-defined error
Public Const InvalidProcedureCallOrArgumentErrorDesc As String = "Invalid procedure call or argument."
Public Const ObjectAlreadyInitializedErrorDesc As String = "Object is already initialized."
Public Const NonDefaultInstanceErrorDesc As String = "Method should be invoked from the default/predeclared instance of this class."
Public Const NullObjectErrorDesc As String = "Object reference cannot be Nothing."
Public Sub GuardNonDefaultInstance(ByVal instance As Object, ByVal defaultInstance As Object, _
Optional ByVal source As String = "Errors", _
Optional ByVal message As String = NonDefaultInstanceErrorDesc)
GuardExpression Not instance Is defaultInstance, errorNumber:=InvalidProcedureCallOrArgumentError, source:=source, message:=message
End Sub
Public Sub GuardDoubleInitialization(ByVal instance As Object, _
Optional ByVal source As String = "Errors", _
Optional ByVal message As String = ObjectAlreadyInitializedErrorDesc)
GuardExpression Not instance Is Nothing, errorNumber:=ObjectAlreadyInitializedError, source:=source, message:=message
End Sub
Public Sub GuardNullReference(ByVal instance As Object, _
Optional ByVal source As String = "Errors", _
Optional ByVal message As String = NullObjectErrorDesc)
GuardExpression instance Is Nothing, errorNumber:=InvalidProcedureCallOrArgumentError, source:=source, message:=message
End Sub
Public Sub GuardExpression(ByVal throw As Boolean, _
Optional ByVal errorNumber As Long = InvalidProcedureCallOrArgumentError, _
Optional ByVal source As String = "Errors", _
Optional ByVal message As String = InvalidProcedureCallOrArgumentErrorDesc)
If throw Then
ThrowError IIf(errorNumber = 0, InvalidProcedureCallOrArgumentError, errorNumber), source, message
End If
End Sub
Public Sub ThrowError(ByVal errorNumber As Long, Optional ByVal source As String = "Errors", _
Optional ByVal message As String = "Invalid procedure call or argument.")
VBA.Information.Err.Err.Raise errorNumber, source:=source, message:=message
End Sub
AsProperty
is not defined anywhere - is it supposed to be a public member of each enum, for example a member ofEnum Testing
?AsCount
is not defined or implemented in the select case statement. \$\endgroup\$