1
\$\begingroup\$

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 Type

This 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!

  1. Currently the "retrieval" functions return aVariant whichIsEmpty() when the item doesn't exist:Clx_Get() andClx_Set() andClx_Cut(). They record its existence (True) or nonexistence (False) in aBoolean argument of the formhas*, which is passed and modifiedby reference.

    Should we instead omit allhas* arguments, and simply return anError value viaCVErr()? This declutters usage and issometimes recommended for functions that may otherwise crash, but it comes with disadvantages:

    • Such values are nowambiguous. DidClx_*() actually encounter an error in execution, or did theCollection simplycontain thisError value as one of itsitems?
    • CanCVErr() evenprocess error codes, beyond thexlErr enumeration for cell errors in Excel? I have found conflicting information on this.
    • Are we reallyinterested in the actual error codes? TheClx_*() 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 useIsError() on the output, rather than having this information already recorded withinhas*.
  2. For consistency with other "safe" functions, shouldClx_Array() andClx_Clone() trap errors when extracting items? In other words, should they populate the targets (arr andclone) with only thesubset ofkeys (and their items) found inclx?

    Currently they use.Item() for efficiency, which throws an error when anykeys are 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 extraIf-statement to each iteration, along with a more complex call toClx_Get().

  3. How much effort shouldClx_Array() devote to resizingarr?

    When the user omits thearr argument, 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 supplycount to save computation time, rather than runningclx.Count which is really an iterative function.

    Note: If we omit missingkeys inClx_Array() andClx_Get(), we mustresize the result once more, and shortenarr to address missing items overlooked bycount.

  4. Does theFor Each properly maximize efficiency for iterating overCollections in 𝒪(𝑛) time?

    To my knowledge, it isprohibitively slow to access largeCollections by index, because they are essentially linked lists: so we degenerate into 𝒪(𝑛²) time by looping numerically onclx.Item(i) fromi = 1 To n.

  5. TheClx_*() 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
askedSep 30 at 20:13
Greg's user avatar
\$\endgroup\$
7
  • \$\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\$CommentedOct 1 at 15:11
  • \$\begingroup\$@PeterT I am not developing a classfor a Dictionary. Rather, I am developing a (standalone) class which has (something like) a Dictionary as one of itsproperties. I haveedited my question to clarify.\$\endgroup\$CommentedOct 1 at 21:14
  • \$\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\$CommentedOct 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\$CommentedOct 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\$CommentedOct 2 at 18:23

0

You mustlog in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.