Background
I am building a complex, standalone class. It has aproperty called.Mapping, which should behave like aDictionary.
This must work onMac as well as Windows, so I cannot employ the nativeCreateObject("Scripting.Dictionary"). Yet it must workwithout dependencies, so I cannot use (say) the excellentDictionary class by@CristianBuse.
So I am designing aUDT called "Dix",within my class module, toemulate aDictionary.
Private Type Dix Count As Long Keys As Collection Items As Collection ' CompareMode As VBA.VbCompareMethodEnd TypeThis requires safe and consistent operations onCollections like.Keys and.Items. So I have written customClx_*() utilities, which should be useful here anduniversally.
Questions
Here are some questions that come to mind, but feel free to pose (and address) your own!
Currently the "retrieval" functions return a
VariantwhichIsEmpty()when the item doesn't exist:Clx_Get()andClx_Set()andClx_Cut(). They record its existence (True) or nonexistence (False) in aBooleanargument of the formhas*, which is passed and modifiedby reference.Should we instead omit all
has*arguments, and simply return anErrorvalue viaCVErr()? This declutters usage and issometimes recommended for functions that may otherwise crash, but it comes with disadvantages:- Such values are nowambiguous. Did
Clx_*()actually encounter an error in execution, or did theCollectionsimplycontain thisErrorvalue as one of itsitems? - Can
CVErr()evenprocess error codes, beyond thexlErrenumeration for cell errors in Excel? I have found conflicting information on this. - Are we reallyinterested in the actual error codes? The
Clx_*()functions "safely" trap relevant errors while propagating all others: soClx_Get()traps only a nonexistent key (error5) or position (error9). - It adds an extra step to use
IsError()on the output, rather than having this information already recorded withinhas*.
- Such values are nowambiguous. Did
For consistency with other "safe" functions, should
Clx_Array()andClx_Clone()trap errors when extracting items? In other words, should they populate the targets (arrandclone) with only thesubset ofkeys(and their items) found inclx?Currently they use
.Item()for efficiency, which throws an error when anykeysare absent fromclx.But theycould useClx_Get()instead, and skip population whenhas*shows aFalse. Among other things, this would avoid leaving thearr"half-baked", with values overwritten up to the first error.However, would thisimpede performance at scale? It introduces an extra
If-statement to each iteration, along with a more complex call toClx_Get().How much effort should
Clx_Array()devote to resizingarr?When the user omits the
arrargument, thenClx_Array()simply returns aVariant()array that it creates from scratch. But when the user suppliesarr, thenClx_Array()copies the values intoarr, so the user can enforce stricter types like aString()array. The user may also supplycountto save computation time, rather than runningclx.Countwhich is really an iterative function.Note: If we omit missing
keysinClx_Array()andClx_Get(), we mustresize the result once more, and shortenarrto address missing items overlooked bycount.Does the
For Eachproperly maximize efficiency for iterating overCollections in 𝒪(𝑛) time?To my knowledge, it isprohibitively slow to access large
Collections by index, because they are essentially linked lists: so we degenerate into 𝒪(𝑛²) time by looping numerically onclx.Item(i)fromi = 1 To n.The
Clx_*()functions are intended as "safe" wrappers for comparableclx.*()methods. While the use of (say)Clx_Cut()is nifty withinClx_Set(), it also slows performance and deepens the call stack by delegating to anotherClx_*()function, rather than simply using the correspondingclx.*()method(s).Which approach better balances performance with (conceptual) consistency?
Code
' ###############' ## Utilities ##' ###############' Assign any value (objective or scalar) to a variable.Private Sub Assign(ByRef var As Variant, ByVal val As Variant) If VBA.IsObject(val) Then Set var = val Else If Let var = val End IfEnd Sub' Throw the latest error object.Private Sub Err_Raise() VBA.Err.Raise number := VBA.Err.Number, _ source := VBA.Err.Source, _ description := VBA.Err.Description, _ helpFile := VBA.Err.HelpFile, _ helpContext := VBA.Err.HelpContextEnd Sub' ########################' ## Utilities | Arrays ##' ########################' Get the length (along a dimension) of an array.Private Function Arr_Length(ByRef arr As Variant, _ Optional ByVal dimension As Long = 1 _) As Long Const EMPTY_ERR_NUMBER As Long = 9 ' Subscript out of range. On Error GoTo BOUND_ERROR Arr_Length = UBound(arr, dimension) - LBound(arr, dimension) + 1 Exit Function BOUND_ERROR: Select Case VBA.Err.Number Case EMPTY_ERR_NUMBER Arr_Length = 0 Case Else Err_Raise End SelectEnd Function' #############################' ## Utilities | Collections ##' #############################' Get an item (safely) from a Collection.Private Function Clx_Get(ByRef clx As Collection, _ ByVal index As Variant, _ Optional ByRef has As Boolean _) As Variant Const POS_ERR_NUMBER As Long = 9 ' Subscript out of range. Const KEY_ERR_NUMBER As Long = 5 ' Invalid procedure call or argument. On Error GoTo ITEM_ERROR Assign Clx_Get, clx.Item(index) has = True Exit Function ITEM_ERROR: Select Case VBA.Err.Number Case POS_ERR_NUMBER, KEY_ERR_NUMBER has = False Case Else Err_Raise End SelectEnd Function' Test if an item exists.Private Function Clx_Has(ByRef clx As Collection, _ ByRef index As Variant _) As Boolean Clx_Get clx, index := index, has := Clx_HasEnd Function' Remove (and record) an item.Private Function Clx_Cut(ByRef clx As Collection, _ ByRef index As Variant, _ Optional ByRef has As Boolean _) As Variant ' Record any item... Assign Clx_Cut, Clx_Get(clx, index := index, has := has) ' ...and remove it. If has Then clx.Remove indexEnd Function' Set (and record) an item.Private Function Clx_Set(ByRef clx As Collection, _ ByRef key As String, _ ByRef item As Variant, _ Optional ByRef has As Boolean _) As Variant ' Remove (and record) any item... Assign Clx_Set, Clx_Cut(clx, index := key, has := has) ' ...and add the new value under its key. clx.Add item, key := keyEnd Function' Rekey an item.Private Sub Clx_Key(ByRef clx As Collection, _ ByRef key As String, _ ByRef newKey As String, _ Optional ByRef has As Boolean, _ Optional ByRef hasNew As Boolean _) ' Record existence and value of old key. Dim item As Variant Assign item, Clx_Get(clx, index := key, has := has) ' Short-circuit for unaltered key. If key = newKey Then hasNew = has Exit Sub End If ' Record existence of new key. Dim hasNew As Boolean hasNew = Clx_Has(clx, index := newKey) ' Rename existing key as unused key. If has And Not hasNew Then clx.Add item, key := newKey, after := key clx.Remove key End IfEnd Sub' Copy a Collection into an array: by position or by key.Private Function Clx_Array(ByRef clx As Collection, _ Optional ByRef keys As Collection, _ Optional ByRef arr As Variant, _ Optional ByVal base As Long = 0, _ Optional ByVal count As Long = -1 _) As Variant ' Create the array if it is not supplied. If VBA.IsMissing(arr) Then Dim a() As Variant arr = a() End If ' Count the items by default. If count < 0 Then If keys Is Nothing Then count = clx.Count Else count = keys.Count End If End If ' Empty the array in the absence of items... If count = 0 Then Erase arr ' ...and otherwise copy the items into the array. Else ' Measure the array. Dim lng As Long: lng = Arr_Len(arr, dimension := 1) Dim resize As Boolean Dim low2 As Long: low2 = base Dim up2 As Long: up2 = low + count - 1 ' Determine if resizing is needed... If lng = 0 Then resize = True Else Dim low1 As Long: low1 = LBound(arr, 1) Dim up1 As Long: up1 = UBound(arr, 1) resize = low1 <> low2 Or up1 <> up2 End If ' ...and resize accordingly. If resize Then ReDim arr(low To up) ' Copy values into array: by position... Dim i As Long: i = low If keys Is Nothing Then Dim item As Variant For Each item In clx Assign arr(i), item i = i + 1 Next item ' ...or by key. Else Dim key As String For Each key in keys Assign arr(i), clx.Item(key) i = i + 1 Next key End If End If ' Return the result. Clx_Array = arrEnd Function' Clone a Collection: its items and (optionally) its keys.Private Function Clx_Clone(ByRef clx As Collection, _ Optional ByRef keys As Collection _) As Collection Dim clone As Collection ' Short circuit if original is uninitialized. If clx Is Nothing Then Exit Function ' Copy any items into the clone: without keys... Set clone = New Collection If keys Is Nothing Then Dim item As Variant For Each item In clx clone.Add item Next item ' ...or with their respective keys. Else Dim key As String For Each key In keys clone.Add clx.Item(key), key := key Next key End If Set Clx_Clone = cloneEnd Function- \$\begingroup\$Though I haven't reviewed your code, I believe the exercise and path you're taking to build your own Dictionary class is certainly worthy. You may want to look atCristian Buse's Dictionary implementation for an example of how some of your questions were addressed.\$\endgroup\$PeterT– PeterT2025-10-01 15:11:40 +00:00CommentedOct 1 at 15:11
- \$\begingroup\$When you say no dependencies you mean your entire codebase must be a single file? For context why is that? And why could you not copy the code from an existing implementation into your module, if the license is very permissive?\$\endgroup\$Greedo– Greedo2025-10-02 08:06:50 +00:00CommentedOct 2 at 8:06
- 1\$\begingroup\$@Greg I see nothing wrong in simply using a Dictionary. Your class will still be standalone and you can just ask your users to use either the Scripting.Dictinary, Tim Hall's dict, my dict, or any other of the available options that you deem fit. It will save you a lot of headache. Who knows, maybe Microsoft will roll out a native dict like they recently did with RegExp.\$\endgroup\$Cristian Buse– Cristian Buse2025-10-02 10:15:40 +00:00CommentedOct 2 at 10:15
- \$\begingroup\$@Greedo My project has two beneficiaries in mind:(1) a developer ("dev") who builds UDFs in VBA; and(2) theirlay user ("user") who enjoys these UDFs in Excel, and need not know of VBA. My project helps the dev greatly and painlessly enhance the experience for their user. The dev pastes a snippet of my code into their module. If my class is already installed, then everything works as intended; but if my class is absent, then the user sees a pop-up in Excel, which prompts them to install my class.To keep installation simple for lay users, my class should be independent.\$\endgroup\$Greg– Greg2025-10-02 18:23:28 +00:00CommentedOct 2 at 18:23
You mustlog in to answer this question.
Explore related questions
See similar questions with these tags.