Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

VBA's Collection class is lacking so I created a basic List 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 List
myList.Extend(Array(1,2,3,4,5,6))
Debug.Print myList(-2) ' 5
Debug.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 return subscript out of range. I don't like it but this way List's will play nice with VBA's collection objects.

Private members

Option Explicit
Private 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 + 1
End Sub

Replace is private; use Item and Slice 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 If

End Sub

Some boring stuff

Private Sub Class_Initialize()
    Set collec = New Collection
End Sub
Private Sub Class_Terminate()
    Set collec = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Attribute 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 then Slice or Append and Extend. The only exception is removal that only implements single elements.

Accessers and Replacement

Item and Slice 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, element

End Property
Public Property Set Item(ByVal index As Long, ByVal element As Variant)
Attribute Item.VB_UserMemId = 0

    TransformIndex index
    Replace index, element

End 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 sets x to y.

Public Property Get Item(ByVal index As Long) As Variant
Attribute Item.VB_UserMemId = 0

    TransformIndex index
    seq.Assign Item, collec.Item(index)

End Property

Slice: Allows for accessing sub sequences from a to b. Regardless of the order. You can specify a step s to skip elements but note that s must be a natural number. If you want to get a reversed sequence make b less than a not a negative s.

Also Slice 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 i

End Property
Public 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 index

End 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 i

End Sub

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

Appending Methods

Public Sub Append(ByVal element As Variant)
    collec.Add element
End Sub
Public Sub Extend(ByVal sequence As Variant)

    Dim element As Variant
    For Each element In sequence
        collec.Add element
    Next element

End Sub

Insertion Methods

Public Sub Emplace(ByVal index As Long, ByVal element As Variant)

    TransformIndex index
    collec.Add element, before:=index

End Sub
Public 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 element

End Sub

Auxiliary Methods

Public Property Get Count() As Long
    Count = collec.Count
End Property
Public 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 = False

End Function
Public Property Get ToString() As String
    ToString = "[" & Join(seq.ToArray(collec), ", ") & "]"
End Property
share|improve this question

2 Answers 2

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 your Exists 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 = False
    
    End Function
    

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

share|improve this answer

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

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

  2. collec reads clumsy. I would rename it to something like underlying or underlyingCollection.

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

    Public Sub Clear(ByVal a As Long, ByVal b As Long)
    
  4. For Clear 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.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.