12
\$\begingroup\$

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
asked Sep 15, 2014 at 17:13
\$\endgroup\$

3 Answers 3

5
\$\begingroup\$

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 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 what a and b mean:

    Public Sub Clear(ByVal a As Long, ByVal b As Long)
    
  4. For Clear 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 in ToString() is all that useful, especially if the list can grow larger it could be problematic.

answered Sep 15, 2014 at 21:52
\$\endgroup\$
1
  • \$\begingroup\$ My rational for using ByRef for TransformIndex was that the alternative was to make it a function, but then it would look like index = TransformIndex(index), which seems redundant. Otherwise great points, especially Clear(start, count). \$\endgroup\$ Commented Sep 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 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.

answered Sep 15, 2014 at 21:42
\$\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 element
End 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 a ParamArray, 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)
 Next
End Sub

That way the client code can do this:

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

Notice a VBA Collection doesn't enforce any kind of type safety, so the item at index 1 could be a String while the item at index 2 could be a Double, or even an Object 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 see seq.ToArray anywhere, but whatever it's doing, it's not accounting for the fact that your List can take Object items, and doing that would break your ToString implementation, while every other method would seemingly work as expected. You should verify whether an item IsObject before attempting to do something with it that wouldn't work on an object.

You might have seen this post, where I solved these issues in a .net-type List<T> (although my ToString doesn't list the contents).

answered Sep 16, 2014 at 0:11
\$\endgroup\$
5
  • \$\begingroup\$ I don't see how ParamArray is a functional solution. It prevents the client from extending a List with another List or any other iterable. The only benefit is the syntactic sugar for initialization of not wrapping the args in Array(. -- Also a TypedList would be a subclass. \$\endgroup\$ Commented Sep 16, 2014 at 14:05
  • \$\begingroup\$ ...and an unambiguous function signature: it documents that you're expecting more than one value. Taking in a Variant says nothing, you're relying on the user's knowledge that Extend should take an array of values, and you're not validating whether the parameter's type is actually an array. This is VBA, a Variant could be anything. I've been using a similar List class for quite a while now, and for me at least, the syntactic sugar has been much more valuable than the possibility for hypothetical eventual extensibility. \$\endgroup\$ Commented Sep 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\$ Commented Sep 16, 2014 at 14:56
  • \$\begingroup\$ How about dealing with objects? Multiple types in wrapped collection? Let it blow up as well? \$\endgroup\$ Commented Sep 16, 2014 at 14:58
  • \$\begingroup\$ lets take it to second monitor. I agree that sequence should be declared as something more descriptive than Variant but I would want it to be Iterable which doesn't exist in VBA. And I am willing to take that hit for the dynamic. \$\endgroup\$ Commented Sep 16, 2014 at 16:14

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.