Results 1 to 3 of 3

Thread: Collection extended: find out keys of a Collection

  1. #1

    Thread Starter
    VB6, XHTML & CSS hobbyist Merri's Avatar
    Join Date
    Oct 2002
    Location
    Finland
    Posts
    6,654

    Collection extended: find out keys of a Collection

    For quite a while there has been code for finding out Collection keys posted over at Planet Source Code. The problem with this code is that it is slow: having a loop with API calls is not the fastest thing to do, so there obviously was room for improvement.

    Here I introduce two separate solutions: a module with three methods to work with existing Collections, and a class module that gives a slightly more OOP feel and possibly easier access, but is slightly slower to access as it is one additional layer more (that one could consider unnecessary).

    Here is the module code:
    Code:
    ' Collection extension module by Merri
    ' Based on information found at http://www.planet-source-code.com/vb/scripts/ShowCode.asp?txtCodeId=68075&lngWId=1
    Option Explicit
    
    Private Declare Function ArrayPtr Lib "msvbvm60" Alias "VarPtr" (Arr() As Any) As Long
    Private Declare Function lstrcmpiW Lib "kernel32" (ByVal Str1 As Long, ByVal Str2 As Long) As Long
    Private Declare Sub PutMem4 Lib "msvbvm60" (ByVal Ptr As Long, ByVal Value As Long)
    Private Declare Function SysAllocStringByteLen Lib "oleaut32" (ByVal Ptr As Long, ByVal Length As Long) As Long
    
    Public Function GetColIndex(Collection As Collection, Key As String) As Long
        Dim lngCurPtr As Long, lngHeader(5) As Long, lngKeyPtr As Long, lngLenB As Long, lngPtr() As Long, lngStrPtr As Long
        If Not Collection Is Nothing Then
            If LenB(Key) Then
                ' remember the StrPtr for fast access
                lngKeyPtr = StrPtr(Key)
                ' we do not want to have API calls in a loop because they're slow
                ' we can avoid that by faking a safe array to our liking!
                lngHeader(0) = 1 ' dimensions
                lngHeader(1) = 4 ' bytes per item
                lngHeader(4) = 1 ' number of items
                ' put lngPtr array into our control
                PutMem4 ArrayPtr(lngPtr), VarPtr(lngHeader(0))
                ' starting pointer: we trick to begin from the last item
                lngCurPtr = ObjPtr(Collection) + 8
                ' and then we loop...
                For GetColIndex = Collection.Count To 1 Step -1
                    ' pointer change: get previous item
                    lngHeader(3) = lngCurPtr + 20
                    ' remember current pointer
                    lngCurPtr = lngPtr(0)
                    ' pointer change: get string pointer
                    lngHeader(3) = lngCurPtr + 16
                    ' compare for equality
                    If lngPtr(0) Then If lstrcmpiW(lngPtr(0), lngKeyPtr) = 0 Then Exit For
                Next GetColIndex
                ' we are done with that trick, reset lngPtr to null
                PutMem4 ArrayPtr(lngPtr), 0
            End If
        Else
            ' No Object
            Err.Raise 91
        End If
    End Function
    
    Public Function GetColKey(Collection As Collection, ByVal Index As Long) As String
        Dim lngA As Long, lngHeader(5) As Long, lngLenB As Long, lngPtr() As Long, lngStrPtr As Long
        If Not Collection Is Nothing Then
            If Index >= 1 And Index <= Collection.Count Then
                ' we do not want to have API calls in a loop because they're slow
                ' we can avoid that by faking a safe array to our liking!
                lngHeader(0) = 1 ' dimensions
                lngHeader(1) = 4 ' bytes per item
                lngHeader(4) = 1 ' number of items
                ' put lngPtr array into our control
                PutMem4 ArrayPtr(lngPtr), VarPtr(lngHeader(0))
                ' we have to loop...
                If Index < Collection.Count \ 2 Then
                    ' pointer change: get first item
                    lngHeader(3) = ObjPtr(Collection) + 24
                    ' loop from beginning to end
                    For lngA = 2 To Index
                        ' pointer change: get next item
                        lngHeader(3) = lngPtr(0) + 24
                    Next lngA
                Else
                    ' pointer change: get last item
                    lngHeader(3) = ObjPtr(Collection) + 28
                    ' loop from end to beginning
                    For lngA = Collection.Count - 1 To Index Step -1
                        ' pointer change: get previous item
                        lngHeader(3) = lngPtr(0) + 20
                    Next lngA
                End If
                ' now figure out the StrPtr
                lngHeader(3) = lngPtr(0) + 16
                lngStrPtr = lngPtr(0)
                ' now figure out the string length IF we have a string
                If lngStrPtr Then
                    lngHeader(3) = lngStrPtr - 4
                    lngLenB = lngPtr(0)
                    ' and now create a new string and place it to output
                    PutMem4 VarPtr(GetColKey), SysAllocStringByteLen(lngStrPtr, lngLenB)
                End If
                ' we are done with that trick, reset lngPtr to null
                PutMem4 ArrayPtr(lngPtr), 0
            Else
                ' Index Out Of Range
                Err.Raise 9
            End If
        Else
            ' No Object
            Err.Raise 91
        End If
    End Function
    
    Public Sub GetColKeys(Collection As Collection, StringArray() As String)
        Dim lngA As Long, lngCount As Long, lngCurPtr As Long, lngKeyPtr As Long, lngHeader(5) As Long, lngLenB As Long, lngPtr() As Long, lngStrPtr As Long
        ' ensure the array is uninitialized
        If Not Not StringArray Then Erase StringArray
        Debug.Assert App.hInstance
        ' shortest way to see whether Erase worked or not (can't Erase fixed size arrays)
        If Not Not StringArray Then Exit Sub
        Debug.Assert App.hInstance
        If Not Collection Is Nothing Then
            If Collection.Count Then
                ' reserve maximum amount of results
                ReDim StringArray(0 To Collection.Count - 1)
                ' remember the pointer of first item for fast access
                lngKeyPtr = VarPtr(StringArray(0))
                ' we do not want to have API calls in a loop because they're slow
                ' we can avoid that by faking a safe array to our liking!
                lngHeader(0) = 1 ' dimensions
                lngHeader(1) = 4 ' bytes per item
                lngHeader(4) = 1 ' number of items
                ' put lngPtr array into our control
                PutMem4 ArrayPtr(lngPtr), VarPtr(lngHeader(0))
                ' starting pointer
                lngCurPtr = ObjPtr(Collection)
                ' and then we loop...
                For lngA = 1 To Collection.Count
                    ' pointer change: get next item
                    lngHeader(3) = lngCurPtr + 24
                    ' remember current pointer
                    lngCurPtr = lngPtr(0)
                    ' pointer change: get string pointer
                    lngHeader(3) = lngCurPtr + 16
                    ' see if we add it in
                    lngStrPtr = lngPtr(0)
                    If lngStrPtr Then
                        ' get string length
                        lngHeader(3) = lngStrPtr - 4
                        lngLenB = lngPtr(0)
                        ' store a new string to output string array
                        PutMem4 lngKeyPtr, SysAllocStringByteLen(lngStrPtr, lngLenB)
                        ' jump to next output string array item and increase counter
                        lngKeyPtr = lngKeyPtr + 4
                        lngCount = lngCount + 1
                    End If
                Next lngA
                ' we are done with that trick, reset lngPtr to null
                PutMem4 ArrayPtr(lngPtr), 0
            End If
            If lngCount = 0 Then
                ' return an empty initialized string array
                StringArray = Split(vbNullString)
            ElseIf lngCount < Collection.Count Then
                ' remove unused items
                ReDim Preserve StringArray(lngCount - 1)
            End If
        Else
            ' No Object
            Err.Raise 91
        End If
    End Sub
    The class module requires other changes to be done than just pasting the code, so you can find that one in the attachment.


    Usage of module
    Three new methods are introduced:

    GetColIndex(Collection As Collection, Key As String) As Long
    Returns index for given Key, or 0 if Key was not found.

    GetColKey(Collection As Collection, ByVal Index As Long) As String
    Returns Key for given Index, or a null string if no Key is assigned to the item.

    GetColKeys(Collection As Collection, StringArray() As String)
    Returns an array of Keys in the given string array. Only Keys that exist are returned. If no Key was found the array is initialized, but empty (read: you can use LBound & UBound safely).


    Usage of class module
    Four new methods are introduced: Clear, Index, Key, Keys
    • Clear cleans up the collection
    • Index returns the Index of given Key, or 0 if Key was not found
    • Key returns the Key of given Index, or null string if Key has not been assigned to the item
    • Keys fills the given string array with Keys that exist in the collection
    Attached Files Attached Files
    Last edited by Merri; May 31st, 2009 at 12:58 PM. Reason: Small bug fix on key string comparison: wrong API call!

  2. #2

    Thread Starter
    VB6, XHTML & CSS hobbyist Merri's Avatar
    Join Date
    Oct 2002
    Location
    Finland
    Posts
    6,654

    Re: Collection extended: find out keys of a Collection

    Here is a sample:
    Code:
    Option Explicit
    
    Private Sub Form_Load()
        Dim Moron As New Collection, strKeys() As String
        Moron.Add "Why", "I"
        Moron.Add "Would", "Know"
        Moron.Add "That", "My"
        Moron.Add "Advanced", "Stuff"
        Moron.Add "Rocks?"
        GetColKeys Moron, strKeys
        MsgBox Join(strKeys)
        MsgBox Moron(GetColIndex(Moron, "know")) & " " & GetColKey(Moron, 1) & " " & GetColKey(Moron, 4) & " " & Moron(GetColIndex(Moron, "my"))
    End Sub
    Last edited by Merri; May 31st, 2009 at 12:57 PM.

  3. #3

    Thread Starter
    VB6, XHTML & CSS hobbyist Merri's Avatar
    Join Date
    Oct 2002
    Location
    Finland
    Posts
    6,654

    Re: Collection extended: find out keys of a Collection

    Here is an extension to the module: SetColItem. It lets you change a Collection item without removing it first.

    Code:
    Public Sub SetColItem(Collection As Collection, NewItem, Optional ByVal Index As Long, Optional Key As String)
        Dim lngA As Long, lngCurPtr As Long, lngHeader(5) As Long, lngKeyPtr As Long, lngPtr() As Long, lngVarHead(5) As Long, varArray()
        If Not Collection Is Nothing Then
            ' now have a go at this...
            If (LenB(Key) > 0) Or (Index >= 1 And Index <= Collection.Count) Then
                ' we do not want to have API calls in a loop because they're slow
                ' we can avoid that by faking a safe array to our liking!
                lngHeader(0) = 1 ' dimensions
                lngHeader(1) = 4 ' bytes per item
                lngHeader(4) = 1 ' number of items
                ' put lngPtr array into our control
                PutMem4 ArrayPtr(lngPtr), VarPtr(lngHeader(0))
                ' we have to loop...
                If LenB(Key) Then
                    ' work on key
                    lngKeyPtr = StrPtr(Key)
                    ' starting pointer: we trick to begin from the last item
                    lngCurPtr = ObjPtr(Collection) + 8
                    ' and then we loop...
                    For Index = Collection.Count To 1 Step -1
                        ' pointer change: get previous item
                        lngHeader(3) = lngCurPtr + 20
                        ' remember current pointer
                        lngCurPtr = lngPtr(0)
                        ' pointer change: get string pointer
                        lngHeader(3) = lngCurPtr + 16
                        ' compare for equality
                        If lngPtr(0) Then If lstrcmpiW(lngPtr(0), lngKeyPtr) = 0 Then Exit For
                    Next Index
                Else
                    ' work on index
                    If Index < Collection.Count \ 2 Then
                        ' pointer change: get first item
                        lngHeader(3) = ObjPtr(Collection) + 24
                        ' loop from beginning to end
                        For lngA = 2 To Index
                            ' pointer change: get next item
                            lngHeader(3) = lngPtr(0) + 24
                        Next lngA
                    Else
                        ' pointer change: get last item
                        lngHeader(3) = ObjPtr(Collection) + 28
                        ' loop from end to beginning
                        For lngA = Collection.Count - 1 To Index Step -1
                            ' pointer change: get previous item
                            lngHeader(3) = lngPtr(0) + 20
                        Next lngA
                    End If
                    lngCurPtr = lngPtr(0)
                End If
                ' if Index is set then we can go on
                If Index Then
                    ' the cool stuff continues: now we create a Variant array
                    lngVarHead(0) = 1
                    lngVarHead(1) = 16
                    lngVarHead(3) = lngCurPtr
                    lngVarHead(4) = 1
                    PutMem4 ArrayPtr(varArray), VarPtr(lngVarHead(0))
                    ' then we do just the simplest thing of replacing a Variant with another...
                    If VarType(NewItem) <> vbObject Then
                        varArray(0) = NewItem
                    Else
                        Set varArray(0) = NewItem
                    End If
                    PutMem4 ArrayPtr(varArray), 0
                End If
                ' we are done with that trick, reset lngPtr to null
                PutMem4 ArrayPtr(lngPtr), 0
            Else
                ' Index Out Of Range
                Err.Raise 9
            End If
        Else
            ' No Object
            Err.Raise 91
        End If
    End Sub
    Probably not one of the most useful things out there, but this brings out a new possible idea: a CollectionToArray converter method that could return a Variant array with the items and a String array with keys, and which efficiently clears up the Collection as it goes. Although I don't know what a Collection would like if it had just null items before being destroyed, but I guess that is something I'll just see if I bother with this.


    ... and by going further, by having such arrays it would be easily possible to make a sorter that would sort Collection items by their respective key! Sorting the actual items makes no sense since a Collection item can be anything, and that makes it unsortable. The keys are sortable as they're always strings.
    Last edited by Merri; May 31st, 2009 at 05:45 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width