When cycling through a list of items in VBA, I use this code pattern:
For Each Item In ListOfItems
' Do something with Item
Next Item
However, in practical applications, I often need access to an iteration counter, the previous or next item, or I have to perform special steps for the first or last item in the list. For instance:
For Each Item In ListOfItems
If Item.IsFirst Then
...
End If
Debug.Print Item.Index, Item.SomeProperty
Next Item
However, .Index and .IsFirst are not properties of the class of Item. Therefore, this does not work. So, my question to the community is: How can I get as close as possible to my desired code pattern?
I know that I could implement a counter with some additional code. Checking for the first item could as well be performed with some additional code. But that's not what I want. I have seen many bugs where the developer forgot to increment the counter. I want to have this out of the hands of the developer.
Likewise, I also know that I could use other types of loops, such as Do While or For i=1 To .... That's also not what I want. I want to use THE straight-forward code pattern, which is made for cycling through a list, which in my opinion is For Each ....
My own attempts to solve this led to this pattern:
For Each IterationItem In toIterationItems(ListOfItems)
If IterationItem.IsFirst Then
...
End If
Debug.Print IterationItem.Index, IterationItem.Item.SomeProperty
Next IterationItem
This works. The function toIterationItems takes a Collection or Dictionary and for each element, it puts an IterationItem into an output Collection, which is handed back to the For Each. So, For Each cycles through a Collection of IterationItems instead of the original ListOfItems.
While this works (I can post the code here, if desired), I am unsure, if there aren't any better approaches. My approach has some disadvantages...
Your approach has the merit of abstracting away the need for counter management, which indeed can be a source of bugs. However, as you mention, it comes with some downsides. It introduces another level of indirection and complexity, which might confuse less experienced developers or make the code harder to maintain. Additionally, it involves creating a whole new collection just to iterate over an existing one, which is inefficient especially for large collections.
The primary alternatives in VBA would be:
As you mentioned, managing the counter manually. Yes, it can lead to bugs if not done carefully, but if the loop is simple and well-documented, the risks are minimal. For complex loops, an abstraction like your IterationItem might indeed be useful.
Using a different language feature or construct. You mentioned you want to use For Each, but it's worth noting that the more traditional For loop inherently supports accessing the index, the previous item, and the next item, and it also makes it easy to perform specific actions on the first or last item.
Here's an example of how you could use a For loop to do all these things:
Dim ListCount As Long
ListCount = ListOfItems.Count
For i = 1 To ListCount
Dim CurrentItem As Variant
CurrentItem = ListOfItems(i)
If i = 1 Then
' First item
ElseIf i = ListCount Then
' Last item
Else
' Middle items
Dim PreviousItem As Variant
Dim NextItem As Variant
PreviousItem = ListOfItems(i - 1)
NextItem = ListOfItems(i + 1)
End If
Debug.Print i, CurrentItem.SomeProperty
Next i
If you prefer the readability of For Each and don't mind the added complexity and inefficiency, then your IterationItem approach might be the best for you. But if you want something simpler and more efficient, a For loop is the most natural fit for the requirements you described. It's just a matter of trade-offs between readability, simplicity, and efficiency.
As a response to @VBasic2008, I post my intermediate solution, here. This is not meant as THE solution, but only as an add-on to my question:
IterationItem:Option Explicit
Private Type TThis
dicParent As Dictionary
lngIndex As Long
End Type
Private This As TThis
Public Sub Init(ByVal adicParent As Dictionary, ByVal alngIndex As Long)
Set This.dicParent = adicParent
This.lngIndex = alngIndex
End Sub
Public Property Get Item() As Variant
VariantLet Item, This.dicParent.Items(This.lngIndex - 1)
End Property
Public Property Get Key() As String
Key = This.dicParent.Keys(This.lngIndex - 1)
End Property
Public Property Get Index() As Long
Index = This.lngIndex
End Property
Public Property Get IsFirstItem() As Boolean
IsFirstItem = CBool(This.lngIndex = 1)
End Property
Public Property Get IsLastItem() As Boolean
IsLastItem = CBool(This.lngIndex = This.dicParent.Count)
End Property
Public Property Get NextItem() As Variant
VariantLet NextItem, This.dicParent.Items(This.lngIndex)
End Property
Public Property Get PrevItem() As Variant
VariantLet PrevItem, This.dicParent.Items(This.lngIndex - 2)
End Property
Private Sub VariantLet(ByRef Destination As Variant, ByVal Source As Variant)
If IsObject(Source) Then
Set Destination = Source
Else
Destination = Source
End If
End Sub
toIterationItems:Public Function toIterationItems(ByVal InputData As Variant) As Collection
Dim varValue As Variant, IterItem As IterationItem, Counter&
Set toIterationItems = New Collection
For Each varValue In InputData
Set IterItem = New IterationItem
Counter = Counter + 1
IterItem.Init InputData, Counter
toIterationItems.Add IterItem
Next varValue
End Function
I don't think that is the optimal solution. I post this here only to demonstrate my direction of thinking. One disadvantage is that my solution deviates from the goal, which I described above. It also creates additional computational load.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With