Quantcast
Channel: VBForums - CodeBank - Visual Basic 6 and earlier
Viewing all articles
Browse latest Browse all 1449

Using a Collection for Sorting

$
0
0
I've discussed all this before, but it came up again in a recent post I made (although I don't think it was considered).

Here's some code that allows using a Collection for sorting purposes (designed for a BAS module):
Code:

Option Explicit
Option Compare Text            ' This takes care of UCase$() comparisons and also the codepage.
'
' This is Public so we can use collFirstKeyAndData, etc. outside of this module.
Public Type VbCollectionKeyAndData
    Key                As String
    Data                As Variant
End Type
'
Private Type VbCollectionHeader  ' Thanks go to The Trick for this.
    pInterface1        As Long  '  Ox00
    pInterface2        As Long  '  Ox04
    pInterface3        As Long  '  Ox08
    lRefCounter        As Long  '  Ox0C
    Count              As Long  '  Ox10
    pvUnk1              As Long  '  Ox14
    pFirstIndexedItem  As Long  '  Ox18
    pLastIndexedItem    As Long  '  Ox1C
    pvUnk4              As Long  '  Ox20
    pRootTreeItem      As Long  '  Ox24 ' This is actually a pointer to what's typically thought of as the root.
    pEndTreePtr        As Long  '  Ox28 ' This is effectively an EOF marker for the tree (bottom of it).  It points to the end of the VbCollectionHeader (HdrPtr + &h30)
    pvUnk5              As Long  '  Ox2C
End Type                          '  Ox30 ' Length.
'
Private Type VbCollectionItem      ' Thanks go to The Trick for this.
    Data                As Variant  '  Ox00
    KeyPtr              As Long    '  Ox10    ' This is actually a String, but it's not directly accessible.  See FetchKey.
    pPrevIndexedItem    As Long    '  Ox14
    pNextIndexedItem    As Long    '  Ox18
    pvUnknown          As Long    '  Ox1C
    pParentItem        As Long    '  Ox20
    pRightBranch        As Long    '  Ox24
    pLeftBranch        As Long    '  Ox28
    bFlag              As Boolean  '  Ox2C
End Type                            '  Ox30 ' Length. (boolean padded to 4)
'
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest As Any, pSource As Any, ByVal ByteLen As Long)
Private Declare Function GetMem4 Lib "msvbvm60.dll" (ByRef Source As Any, ByRef Dest As Any) As Long
'
Private uBlankItem      As VbCollectionItem
'

Public Function collFirstKey(c As Collection) As String
    collFirstKey = collFirst(c).Key
End Function

Public Function collLastKey(c As Collection) As String
    collLastKey = collFirst(c, True).Key
End Function

Public Function collNextKey(c As Collection) As String
    collNextKey = collNext(c).Key
End Function

Public Function collPrevKey(c As Collection) As String
    collPrevKey = collNext(c, True).Key
End Function

Public Function collFirstKeyAndData(c As Collection) As VbCollectionKeyAndData
    collFirstKeyAndData = collFirst(c)
End Function

Public Function collLastKeyAndData(c As Collection) As VbCollectionKeyAndData
    collLastKeyAndData = collFirst(c, True)
End Function

Public Function collNextKeyAndData(c As Collection) As VbCollectionKeyAndData
    collNextKeyAndData = collNext(c)
End Function

Public Function collPrevKeyAndData(c As Collection) As VbCollectionKeyAndData
    collPrevKeyAndData = collNext(c, True)
End Function

Public Sub collClearIterate(c As Collection)
    ' This does NOT remove any items.
    ' It is used to clear any previous iteration point.
    ' It is useful if we've started Adding/Deleting items.  We want to be cautious.
    ' If we start adding/deleting items, and then call collNext/collPrev, we may crash.
    ' However, if this Clear is called, we'll be safe.
    ' Alternatively, we can just call collFirstKey/collLastKey after adding/deleting, and all will be fine.
    StoreItemPtrInHeader c, 0&
End Sub

Public Function collKeyExists(c As Collection, ByVal sKey As String) As Boolean
    ' Dig our way to the bottom of the tree, and either find the key or not.
    ' This is an error-trapping free way to search.
    '
    Dim iItemPtr        As Long
    Dim iEofPtr        As Long
    Dim sKeyTemp        As String
    '
    If c Is Nothing Then Exit Function                                  ' No Collection.
    'If c.Count = 0 Then Exit Function                                  ' This isn't needed.  The "If iItemPtr = iEofPtr..." serves the same purpose.
    GetMem4 ByVal PtrAdd(ObjPtr(c), &H24&), iItemPtr                    ' VbCollectionHeader.pRootTreeItem
    GetMem4 ByVal PtrAdd(ObjPtr(c), &H28&), iEofPtr                    ' VbCollectionHeader.pEndTreePtr
    Do
        If iItemPtr = iEofPtr Then Exit Do                              ' Hit bottom of tree.  Could be true if there are no non-vbNullString keys.
        GetMem4 ByVal PtrAdd(iItemPtr, &H10&), ByVal VarPtr(sKeyTemp)  ' Alias the String/Key within the collection.
        Select Case StrComp(sKeyTemp, sKey, vbTextCompare)              ' Collection searches are NOT case sensitive (Option Compare Text).
        Case -1
            GetMem4 ByVal PtrAdd(iItemPtr, &H24&), iItemPtr            ' VbCollectionItem.pRightBranch
        Case 1
            GetMem4 ByVal PtrAdd(iItemPtr, &H28&), iItemPtr            ' VbCollectionItem.pLeftBranch
        Case Else
            collKeyExists = True                                        ' We found our key.
            Exit Do
        End Select
    Loop
    GetMem4 0&, ByVal VarPtr(sKeyTemp)                                  ' Clean-up our String aliasing.
End Function


' ********************************************************************
' ********************************************************************
' ********************************************************************
'
' Just private procedures for the above from here down.
'
' ********************************************************************
' ********************************************************************
' ********************************************************************


Private Function collFirst(c As Collection, Optional bGetLast As Boolean) As VbCollectionKeyAndData
    ' Ascii EOF (Chr$(26)) used as an EOF/BOF indicator.
    ' Does NOT include any items with no key.
    '
    If c Is Nothing Then
        collFirst.Key = Chr$(26&)
        collFirst.Data = collFirst.Key
        Exit Function
    End If
    '
    Dim uHdr        As VbCollectionHeader
    Dim uItem      As VbCollectionItem
    Dim iItemPtr    As Long
    '
    ' Get header and top item pointer.
    CopyMemory uHdr, ByVal ObjPtr(c), LenB(uHdr)
    iItemPtr = uHdr.pRootTreeItem
    '
    ' Check if all items have vbNullString key and also the count.
    If iItemPtr = EndPointer(c) Or c.Count = 0 Then
        collFirst.Key = Chr$(26&)
        collFirst.Data = collFirst.Key
        Exit Function
    End If
    '
    ' Get top item.
    CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
    '
    ' Dig to bottom of left side (or right side if we want last).
    If Not bGetLast Then
        Do While HasLeftBranch(c, uItem)
            iItemPtr = uItem.pLeftBranch
            CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Loop
    Else
        Do While HasRightBranch(c, uItem)
            iItemPtr = uItem.pRightBranch
            CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Loop
    End If
    '
    ' Return results.
    StoreItemPtrInHeader c, iItemPtr            ' Store item pointer in header so collNext, collPrev can work.
    collFirst.Key = FetchKey(uItem)            ' Return key found.
    LetSetData uItem.Data, collFirst.Data      ' Return data found.
    '
    ' Mandatory cleanup.  We must do this so that things in the uItem.Data don't get deallocated.
    CopyMemory uItem, uBlankItem, LenB(uItem)
End Function

Private Function collNext(c As Collection, Optional bGetPrev As Boolean) As VbCollectionKeyAndData
    ' Ascii EOF (Chr$(26)) used as an EOF/BOF indicator.
    ' Does NOT include any items with no key.
    '
    Dim sCurrentKey    As String
    Dim uItem          As VbCollectionItem
    Dim iItemPtr        As Long
    '
    If c Is Nothing Then
        collNext.Key = Chr$(26&)
        collNext.Data = collNext.Key
        Exit Function
    End If
    '
    ' Fetch our current item pointer that we previously stored.
    iItemPtr = GetItemPtrFromHeader(c)
    If iItemPtr = 0& Then
        collNext.Key = Chr$(26&)
        collNext.Data = collNext.Key
        Exit Function
    End If
    '
    ' Get the current item.
    CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
    '
    ' Figure out which direction we're going.
    Select Case True                                                                                    ' Comments are for collNext.  collPrev is just reversed.
    '
    Case (HasRightBranch(c, uItem) And Not bGetPrev) Or _
        (HasLeftBranch(c, uItem) And bGetPrev)                                                        ' Go right, and then as far left as we can.
        If Not bGetPrev Then iItemPtr = uItem.pRightBranch _
                        Else iItemPtr = uItem.pLeftBranch
        CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Do While (HasLeftBranch(c, uItem) And Not bGetPrev) Or _
                (HasRightBranch(c, uItem) And bGetPrev)
            If Not bGetPrev Then iItemPtr = uItem.pLeftBranch Else iItemPtr = uItem.pRightBranch
            CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Loop
        StoreItemPtrInHeader c, iItemPtr                                                                ' Save what we found for next collNext.
        collNext.Key = FetchKey(uItem)                                                                  ' Return key found.
        LetSetData uItem.Data, collNext.Data                                                            ' Return data found.
    '
    Case HasUpBranch(c, uItem)                                                                          ' Go up, but make sure up is greater.
        sCurrentKey = FetchKey(uItem)                                                                  ' We must save this before we lose it for our comparisons.
        iItemPtr = uItem.pParentItem                                                                    ' If it's not greater, keep going up until we find one that is.
        CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Do
            collNext.Key = FetchKey(uItem)                                                              ' We test and possibly return this.
            If (collNext.Key >= sCurrentKey And Not bGetPrev) Or _
              (collNext.Key <= sCurrentKey And bGetPrev) Then                                          ' We use >= because they could be vbNullString if no key was specified.
                StoreItemPtrInHeader c, iItemPtr                                                        ' Save what we found for next collNext.
                LetSetData uItem.Data, collNext.Data                                                    ' Return data found.
                Exit Do
            End If
            If Not HasUpBranch(c, uItem) Then                                                          ' We hit the top without finding one greater.
                StoreItemPtrInHeader c, 0&                                                              ' Clear item pointer in header.
                collNext.Key = Chr$(26&)                                                                ' Return EOF/BOF indicator in key.
                collNext.Data = collNext.Key                                                            ' Return EOF/BOF indicator in data.
                Exit Do
            End If
            iItemPtr = uItem.pParentItem                                                                ' We're getting the next parent to see if it's greater.
            CopyMemory uItem, ByVal iItemPtr, LenB(uItem)
        Loop
    '
    Case Else                                                                                          ' We hit the end.  This only happens with one or two items in Collection.
        StoreItemPtrInHeader c, 0&                                                                      ' Clear item pointer in header.
        collNext.Key = Chr$(26&)                                                                        ' Return EOF/BOF indicator in key.
        collNext.Data = collNext.Key                                                                    ' Return EOF/BOF indicator in data.
    '
    End Select
    '
    ' Mandatory cleanup.  We must do this so that things in the uItem.Data don't get deallocated.
    CopyMemory uItem, uBlankItem, LenB(uItem)
End Function

Private Function HasLeftBranch(c As Collection, uItem As VbCollectionItem) As Boolean
    HasLeftBranch = uItem.pLeftBranch <> EndPointer(c)
End Function

Private Function HasRightBranch(c As Collection, uItem As VbCollectionItem) As Boolean
    HasRightBranch = uItem.pRightBranch <> EndPointer(c)
End Function

Private Function HasUpBranch(c As Collection, uItem As VbCollectionItem) As Boolean
    HasUpBranch = uItem.pParentItem <> EndPointer(c)
End Function

Private Function PtrAdd(iPtr As Long, iOffset As Long) As Long
    PtrAdd = (iPtr Xor &H80000000) + iOffset Xor &H80000000
End Function

Private Sub LetSetData(DataIn As Variant, DataOut As Variant)
    ' This is necessary because we don't know if the item is an object or not.
    ' If the item is an object, just using Let will cause an error.
    ' We must also use DataOut passed ByRef, so we don't have to Let/Set it again from a Function.
    If IsObject(DataIn) Then Set DataOut = DataIn Else DataOut = DataIn
End Sub

Private Sub LetStringArray(sa() As String, idx As Long, sVal As String)
    ' Necessary because, if the array is the function name, you can't assign from within the function.
    sa(idx) = sVal
End Sub

Private Function EndPointer(c As Collection) As Long
    ' This is effectively an EOF (or end-of-branch) marker that's used by VB6's Collections.
    ' They DON'T use zero for this, and each Collection will have a different value.
    ' It's basically a pointer back to the bottom of the Collection header.
    GetMem4 ByVal PtrAdd(ObjPtr(c), &H28&), EndPointer      ' VbCollectionHeader.pEndTreePtr
End Function

Private Function StoreItemPtrInHeader(c As Collection, iItemPtr As Long)
    Dim uHdr As VbCollectionHeader
    CopyMemory uHdr, ByVal ObjPtr(c), LenB(uHdr)
    uHdr.pvUnk4 = iItemPtr
    CopyMemory ByVal ObjPtr(c), uHdr, LenB(uHdr)
End Function

Private Function GetItemPtrFromHeader(c As Collection) As Long
    Dim uHdr As VbCollectionHeader
    CopyMemory uHdr, ByVal ObjPtr(c), LenB(uHdr)
    GetItemPtrFromHeader = uHdr.pvUnk4
End Function

Private Function FetchKey(uItem As VbCollectionItem) As String
    Dim sKeyTemp As String
    GetMem4 uItem.KeyPtr, ByVal VarPtr(sKeyTemp)            ' Key string of collection item.
    FetchKey = sKeyTemp                                    ' Copy key into FetchKey string.
    GetMem4 0&, ByVal VarPtr(sKeyTemp)                      ' Make string empty again to keep memory straight.
End Function

And here's some code for a quick test for a Form1:
Code:

Option Explicit
'

Private Sub Form_Load()
    Dim c As New Collection
    Dim sKey As String
   
    c.Add "qwer", "qqq"
    c.Add "asdf", "aaa"
    c.Add "zxcv", "zzz"
   
   
    If c.Count Then
   
        Debug.Print collFirstKey(c)
        Do
            sKey = collNextKey(c)
            If sKey = Chr$(26) Then Exit Do
            Debug.Print sKey
        Loop
   
    End If
   
   
End Sub

As just a bit of background, keys are added into a collection using an internal binary tree. Sadly, they don't give you a way to easily traverse that binary tree, but that is rectified with the above code.

I've got no idea how this would speed compare with various quick sorts, or other sorting approaches. But I thought I'd post it here anyway. It could be particularly useful if you need to sort things stored in a Variant, based on some sort-key.

Viewing all articles
Browse latest Browse all 1449

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>