12
\$\begingroup\$

VBA'sCollection class is lacking so I created a basicList class using Python's as a template. This could make future derived classes easier to implement.

The biggest features are better setting and accessing of values. But one neat-o feature is Python-style indexing.

Dim myList As ListmyList.Extend(Array(1,2,3,4,5,6))Debug.Print myList(-2) ' 5Debug.print myList.Slice(-1, 1).ToString '"[6,5,4,3,2,1]"

Note: I kept VBA's 1 offset for collections. This means that there is a hole at index 0 and will always returnsubscript out of range. I don't like it but this way List's will play nice with VBA's collection objects.

Private members

Option ExplicitPrivate collec As Collection ' Sole datamember

TransformIndex: Enforces Zero Offset and Cylcing.

Private Sub TransformIndex(ByRef x As Variant)    If x < 0 Then x = x + collec.Count + 1End Sub

Replace isprivate; useItem andSlice to actually replace elements

Private Sub Replace(ByVal index As Long, ByVal element As Variant)    collec.Remove index    If index = collec.Count + 1 Then        collec.Add element    Else        collec.Add element, before:=index    End IfEnd Sub

Some boring stuff

Private Sub Class_Initialize()    Set collec = New CollectionEnd SubPrivate Sub Class_Terminate()    Set collec = NothingEnd SubPublic Property Get NewEnum() As IUnknownAttribute NewEnum.VB_UserMemId = -4    Set NewEnum = collec.[_NewEnum]End Property

Public Methods

The general pattern for these is and implementation of one action for a single element and another for a sequence of elements.e.g.Item and thenSlice orAppend andExtend. The only exception is removal that only implements single elements.

Accessers and Replacement

Item andSlice provide general access to members and allows replacement as they are not read only.

Public Property Let Item(ByVal index As Long, ByVal element As Variant)Attribute Item.VB_UserMemId = 0    TransformIndex index    Replace index, elementEnd PropertyPublic Property Set Item(ByVal index As Long, ByVal element As Variant)Attribute Item.VB_UserMemId = 0    TransformIndex index    Replace index, elementEnd Property

seq is an auxiliary module for general sequence functions. For purposes of this review assume functions from it does exactly what it should do.seq.Assign(x,y) assign's or setsx toy.

Public Property Get Item(ByVal index As Long) As VariantAttribute Item.VB_UserMemId = 0    TransformIndex index    seq.Assign Item, collec.Item(index)End Property

Slice: Allows for accessing sub sequences froma tob. Regardless of the order. You can specify a steps to skip elements but note thats must be a natural number. If you want to get a reversed sequence makeb less thana not a negatives.

AlsoSlice is read-write, so it allows replacement of sub-sequences.

Public Property Get Slice(ByVal a As Long, ByVal b As Long, Optional ByVal s As Long = 1) As List    TransformIndex a    TransformIndex b    Set Slice = New List    If s < 1 Then Err.Raise "List.Slice", "Step " & s & " is not a natural number."    s = IIF(a < b, s, -s)    Dim i As Long    For i = a To b Step s        Slice.Append collec.Item(i)    Next iEnd PropertyPublic Property Let Slice(ByVal a As Long, ByVal b As Long, Optional ByVal s As Long = 1, ByVal sequence As Variant)    TransformIndex a    TransformIndex b    If s < 1 Then Err.Raise "List.Slice", "Step " & s & " is not a natural number."    s = IIF(a < b, s, -s)    If Abs(a - b) + 1 <> seq.Length(sequence) Then        Err.Raise 9, "List.Slice", "Subscript out of Range."    End If    Dim i As Long: i = a    Dim element As Variant    For Each element In sequence        Replace i, element        i = i + s    Next element    Debug.Assert (i - s = b)End Property

Removal Methods

Public Sub Remove(ByVal index As Long)    TransformIndex index    collec.Remove indexEnd Sub''' List.Clear(x, y) \equiv List.Clear(y, x)Public Sub Clear(ByVal a As Long, ByVal b As Long)    TransformIndex a    TransformIndex b    ' Order of removal is irrelevant    If a > b Then seq.Swap a, b    Dim i As Long    For i = 0 To b - a        collec.Remove a    Next iEnd Sub

I have been trying to work out aRemoveRange function. I will update with them later. Added aClear function. NoteList.Clear(x, y) \$ \equiv \$List.Clear(y, x).

Appending Methods

Public Sub Append(ByVal element As Variant)    collec.Add elementEnd SubPublic Sub Extend(ByVal sequence As Variant)    Dim element As Variant    For Each element In sequence        collec.Add element    Next elementEnd Sub

Insertion Methods

Public Sub Emplace(ByVal index As Long, ByVal element As Variant)    TransformIndex index    collec.Add element, before:=indexEnd SubPublic Sub Insert(ByVal index As Long, ByVal sequence As Variant)    TransformIndex index    seq.Reverse sequence    Dim element As Variant    For Each element In sequence        collec.Add element, before:=index    Next elementEnd Sub

Auxiliary Methods

Public Property Get Count() As Long    Count = collec.CountEnd PropertyPublic Function Exists(ByVal sought As Variant) As Boolean    Exists = True    Dim element As Variant    For Each element In collec        If element = sought Then Exit Function    Next element    Exists = FalseEnd FunctionPublic Property Get ToString() As String    ToString = "[" & Join(seq.ToArray(collec), ", ") & "]"End Property
askedSep 15, 2014 at 17:13
cheezsteak's user avatar
\$\endgroup\$

3 Answers3

5
\$\begingroup\$

I'm no VBA coder so just a few minor things:

  1. Is there a particular reason to useByRef for the parameter toTransformIndex? It doesn't seem exactly necessary and I usually prefer methods without side effects.

  2. collec reads clumsy. I would rename it to something likeunderlying orunderlyingCollection.

  3. You should take a bit more care naming method parameters. Parameter names form an important part of the method documentation and should convey their intent clearly. For example here it is not exactly clear whata andb mean:

    Public Sub Clear(ByVal a As Long, ByVal b As Long)
  4. ForClear it is not entirely clear if for example the end index is included or not for the removal. C# usually follows the convention of specify the start index and the count of how many elements should be removed (i.e.Clear(start, count)). I found that approach more robust in the long run.

  5. I don't think dumping the entire list content inToString() is all that useful, especially if the list can grow larger it could be problematic.

answeredSep 15, 2014 at 21:52
ChrisWue's user avatar
\$\endgroup\$
1
  • \$\begingroup\$My rational for usingByRef forTransformIndex was that the alternative was to make it a function, but then it would look likeindex = TransformIndex(index), which seems redundant. Otherwise great points, especiallyClear(start, count).\$\endgroup\$CommentedSep 16, 2014 at 13:49
4
\$\begingroup\$

Only a a couple of minor notes. It looks pretty good (but admittedly, I've not run the code).

  1. Single letter argument names obfuscate their meaning.

    Public Property Get Slice(ByVal a As Long, ByVal b As Long, Optional ByVal s As Long = 1) As List
  2. I find it a little odd to initialize a bool to true, then set it to false if the code doesn't exit early. I would rewrite yourExists method just a little bit.

    Public Function Exists(ByVal sought As Variant) As Boolean    Dim element As Variant    For Each element In collec        If element = sought Then             Exists = True            Exit Function        End If    Next element    Exists = FalseEnd Function

    It's semantically the same, but the intent is a little clearer IMO.

answeredSep 15, 2014 at 21:42
RubberDuck's user avatar
\$\endgroup\$
3
\$\begingroup\$

In addition to what was already said, I'll add these points:

Public Sub Extend(ByVal sequence As Variant)    Dim element As Variant    For Each element In sequence        collec.Add element    Next elementEnd Sub

Here the only clue the calling code has about the expected type, is the parameter's name (sequence) - that reinforces what was already said about meaningful parameter names, but I find the way you've implemented this forces the client to add boilerplate code just to be able to pass values to this method.

A much more convenient (and self-documenting) way of taking in an arbitrary number of parameters, is to use aParamArray, and iterate its indices instead:

Public Sub Extend(ParamArray items())    Dim i As Integer    For i LBound(items) To UBound(items)        collec.Add items(i)    NextEnd Sub

That way the client code can do this:

Dim myList As New ListmyList.Extend 12, 23, 34, 45, 56, 67, "bob", 98.99

Notice a VBACollection doesn't enforce any kind of type safety, so the item at index 1 could be aString while the item at index 2 could be aDouble, or even anObject of any given type.

I know nothing of, so maybe that isn't an issue, but if your list is meant to only contain one type of items, you have a problem here.

Speaking of which:

Public Property Get ToString() As String    ToString = "[" & Join(seq.ToArray(collec), ", ") & "]"End Property

I don't seeseq.ToArray anywhere, but whatever it's doing, it's not accounting for the fact that yourList can takeObject items, and doing that would break yourToString implementation, while every other method would seemingly work as expected. You should verify whether an itemIsObject before attempting to do something with it that wouldn't work on an object.

You might have seenthis post, where I solved these issues in a .net-typeList<T> (although myToString doesn't list the contents).

answeredSep 16, 2014 at 0:11
Mathieu Guindon's user avatar
\$\endgroup\$
5
  • \$\begingroup\$I don't see howParamArray is a functional solution. It prevents the client from extending aList with anotherList or any other iterable. The only benefit is the syntactic sugar for initialization of not wrapping the args inArray(. -- Also aTypedList would be a subclass.\$\endgroup\$CommentedSep 16, 2014 at 14:05
  • \$\begingroup\$...and an unambiguous function signature: itdocuments that you're expecting more than one value. Taking in aVariant says nothing, you're relying on the user's knowledge thatExtend should take an array of values, and you're not validating whether the parameter's type is actually an array. This is VBA, aVariant could beanything. I've been using a similarList class for quite a while now, and for me at least, thesyntactic sugar has been much more valuable than thepossibility for hypothetical eventual extensibility.\$\endgroup\$CommentedSep 16, 2014 at 14:18
  • 1
    \$\begingroup\$I disagree that it's an unneeded functionality. Otherwise you have no way of dynamically initializing or concatenating Lists other than writing for each loops everywhere.\$\endgroup\$CommentedSep 16, 2014 at 14:56
  • \$\begingroup\$How about dealing with objects? Multiple types in wrapped collection? Let it blow up as well?\$\endgroup\$CommentedSep 16, 2014 at 14:58
  • \$\begingroup\$lets take it to second monitor. I agree thatsequence should be declared as something more descriptive thanVariant but I would want it to beIterable which doesn't exist in VBA. And I am willing to take that hit for the dynamic.\$\endgroup\$CommentedSep 16, 2014 at 16:14

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.