Skip to content

Instantly share code, notes, and snippets.

@florentbr
Created September 13, 2019 14:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save florentbr/0488726f5bc22657319d52f49dbb0c62 to your computer and use it in GitHub Desktop.
Save florentbr/0488726f5bc22657319d52f49dbb0c62 to your computer and use it in GitHub Desktop.
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Dictionary"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = True
Attribute VB_Description = "Collection of keys and items which maps keys to items with a minimum cost O(1)."
'
' Version: 2017/7/04
'
' Collection of keys and items. Maps keys to items with a minimum cost, O(1).
'
' Features :
' * Cross platform, no dependencies.
' * Performs better than Scripting.Dictionary or Collection on large sets.
' * New methods: TryGet, TryAdd, IndexOf and Clone
' * Provides introspection on each key/item in the debug view.
' * Preserves the insertion order and provides access to keys and items by index.
' * Unlike Scripting.Dictionary, the getter raises an error if the key is missing, unless a default value is provided.
' * Unlike Scripting.Dictionary, the getter doesn't create an entry if a key is not present.
'
' Usage:
'
' Dim dict As New Dictionary
'
' ' Add a key/item and raise an error if the key is already present '
' dict.Add "a", 1
'
' ' Assign a key/item. Overwrites the item if the key is already present '
' dict("a") = 2
' Set dict("b") = New Collection
'
' ' Get an item or raise an error if the key is not present '
' Debug.Print dict("a")
'
' ' Get an item or a default item if the key is not present '
' Debug.Print dict("b", Default:=3)
'
' ' Get an item by reference if key is present '
' Dim value
' If dict.TryGet("a", value) Then Debug.Print value
'
' ' Remove an item if key is present '
' Dim value
' If dict.Remove("a", value) Then Debug.Print "Removed " & value
'
' ' Add an item only if the key is not already present '
' If dict.TryAdd("a", 5) Then Debug.Print "Successfuly added"
'
' ' Iterate the Keys/Items (Base 1 index) '
' For i = 1 To dict.Count
' Debug.Print dict.Keys(i), dict.Items(i)
' Next
'
' ' Get the Keys/Items '
' Debug.Print Join(dict.Keys, ", ")
' Debug.Print Join(dict.Items, ", ")
'
Option Explicit
Option Base 1
Public Enum VbCompareMethod
vbBinaryCompare
vbTextCompare
End Enum
Private Type TThis
Compare As VbCompareMethod
Count As Long ' Count of entries '
Deleted As Long ' Count of deleted entries '
Keys() As Variant ' Ordered keys (base 1) '
Items() As Variant ' Ordered items (base 1) '
Hashes() As Long ' Ordered keys hash '
Slots() As Long ' Indexes of the next entry / buckets '
End Type
Private this As TThis
Public Property Get CompareMode() As VbCompareMethod
Attribute CompareMode.VB_Description = "Specifies the type of key comparison. Either vbBinaryCompare or vbTextCompare"
CompareMode = this.Compare
End Property
Public Property Let CompareMode(ByVal Compare As VbCompareMethod)
If Count And this.Compare <> Compare Then Err.Raise 9, , "Dictionary not empty"
this.Compare = Compare
End Property
Public Property Get Count() As Long
Attribute Count.VB_Description = "Number of entries"
Count = this.Count - this.Deleted
End Property
Private Property Get Entries() As Variant()
If this.Count - this.Deleted Then x_enum Entries
End Property
Public Function Clone() As Dictionary
Set Clone = New Dictionary
Clone.x_load this
End Function
Public Sub Add(Key, Optional Item)
Dim h&, s&, i& ' hash, slot, index '
If x_try_add(Key, h, s, i) Then Else Err.Raise 457, , "Key already associated: " & Key
If IsObject(Item) Then Set this.Items(i) = Item Else this.Items(i) = Item
End Sub
Public Function Exists(Key) As Boolean
Attribute Exists.VB_Description = "Returns True if the key is present, False otherwise."
Exists = x_try_get(Key, 0&, 0&, 0&)
End Function
Public Function IndexOf(Key) As Long
If this.Deleted Then x_resize ' collapse entries if some were removed '
x_try_get Key, 0&, 0&, IndexOf
End Function
Public Property Get Item(Key, Optional Default)
Dim h&, s&, i& ' hash, slot, index '
If x_try_get(Key, h, s, i) Then
If IsObject(this.Items(i)) Then Set Item = this.Items(i) Else Item = this.Items(i)
Else
If VBA.IsMissing(Default) Then Err.Raise 9, , "Key not found: " & Key
If IsObject(Default) Then Set Item = Default Else Item = Default
End If
End Property
Public Property Let Item(Key, Optional Default, Item)
Attribute Item.VB_Description = "Gets or sets an item. When the key is missing, the getter returns the Default value if provided or raises error 422"
Attribute Item.VB_UserMemId = 0
Dim h&, s&, i& ' hash, slot, index '
x_try_add Key, h, s, i
this.Items(i) = Item
End Property
Public Property Set Item(Key, Optional Default, Item)
Dim h&, s&, i& ' hash, slot, index '
x_try_add Key, h, s, i
Set this.Items(i) = Item
End Property
Public Function Keys(Optional ByVal Index As Long)
Attribute Keys.VB_Description = "Returns all the keys (base 1 array) or a key at Index (base 1) if provided"
If this.Deleted Then x_resize ' collapse entries if some were removed '
If Index Then ' return the key at index '
If Index > this.Count Then Err.Raise 9
Keys = this.Keys(Index)
Else ' return all the keys in a base1 array '
If this.Count Then x_copy Keys, this.Keys Else Keys = Array()
End If
End Function
Public Function Items(Optional ByVal Index As Long)
Attribute Items.VB_Description = "Returns all the items (base 1 array) or an item at Index (base 1) if provided"
If this.Deleted Then x_resize ' collapse entries if some were removed '
If Index Then ' return the value at index '
If Index > this.Count Then Err.Raise 9
If IsObject(this.Items(Index)) Then Set Items = this.Items(Index) Else Items = this.Items(Index)
Else ' return all the values in a base1 array '
If this.Count Then x_copy Items, this.Items Else Items = Array()
End If
End Function
Public Function TryGet(Key, out) As Boolean
Dim h&, s&, i& ' hash, slot, index '
If x_try_get(Key, h, s, i) Then TryGet = True Else Exit Function
If IsObject(this.Items(i)) Then Set out = this.Items(i) Else out = this.Items(i)
End Function
Public Function TryAdd(Key, Item) As Boolean
Dim h&, s&, i& ' hash, slot, index '
If x_try_add(Key, h, s, i) Then TryAdd = True Else Exit Function
If IsObject(Item) Then Set this.Items(i) = Item Else this.Items(i) = Item
End Function
Public Function Remove(Key, Optional out) As Boolean
Attribute Remove.VB_Description = "Tries to removes a key/item pair. Returns True if the key was present, false otherwise."
Dim h&, s&, i& ' hash, slot, index '
If x_try_get(Key, h, s, i) Then Remove = True Else Exit Function
If VBA.IsMissing(out) Then Else If IsObject(this.Items(i)) Then Set out = this.Items(i) Else out = this.Items(i)
this.Deleted = this.Deleted + 1&
this.Slots(s) = this.Slots(i)
this.Slots(i) = 0&
this.Hashes(i) = 0&
this.Keys(i) = Empty
this.Items(i) = Empty
End Function
Public Sub RemoveAll()
Attribute RemoveAll.VB_Description = "Removes all the key/item."
this.Count = 0&
this.Deleted = 0&
Erase this.Keys, this.Items, this.Hashes, this.Slots
End Sub
Private Function x_try_get(Key, h As Long, s As Long, i As Long) As Boolean
If this.Count Then Else Exit Function
h = x_hash(LCase$(Key)) Xor -1& ' get negative hash '
s = UBound(this.Slots) + (h Mod UBound(this.Hashes)) ' get slot '
Do
i = this.Slots(s) ' get index '
If i Then Else Exit Function ' return if no entry '
If this.Hashes(i) = h Then If x_equal(Key, this.Keys(i)) Then Exit Do ' break if match '
s = i ' next slot '
Loop
x_try_get = True
End Function
Private Function x_try_add(Key, h As Long, s As Long, i As Long) As Boolean
If this.Count Then Else x_resize
If this.Count >= UBound(this.Keys) Then x_resize
h = x_hash(LCase$(Key)) Xor -1& ' get negative hash '
s = UBound(this.Slots) + (h Mod UBound(this.Hashes)) ' get slot '
Do
i = this.Slots(s) ' get index '
If i Then Else Exit Do ' break if no entry '
If this.Hashes(i) = h Then If x_equal(Key, this.Keys(i)) Then Exit Function ' exit if match '
s = i ' next slot '
Loop
this.Count = this.Count + 1
this.Keys(this.Count) = Key
this.Hashes(this.Count) = h
this.Slots(s) = this.Count
i = this.Count
x_try_add = True
End Function
Private Sub x_resize()
Dim i&, s&, n&
If this.Deleted Then ' collapse entries '
For i = 1 To this.Count
If this.Hashes(i) Then ' if entry '
n = n + 1&
this.Hashes(n) = this.Hashes(i)
this.Keys(n) = this.Keys(i)
If IsObject(this.Items(i)) Then Set this.Items(n) = this.Items(i) Else this.Items(n) = this.Items(i)
End If
Next
this.Count = n
this.Deleted = 0
If n Then ReDim Preserve this.Keys(n), this.Items(n), this.Hashes(n) ' truncate / GC objects '
End If
n = 5 + this.Count * 1.973737421
ReDim Preserve this.Keys(n), this.Items(n), this.Hashes(n)
ReDim this.Slots(n * 2)
For i = 1 To this.Count
s = UBound(this.Slots) + (this.Hashes(i) Mod n) ' get slot '
Do While this.Slots(s) ' until empty slot '
s = this.Slots(s)
Loop
this.Slots(s) = i ' empty slot gets the index '
Next
End Sub
Private Function x_hash(buffer() As Byte) As Long
Dim i&
For i = 1 To UBound(buffer) Step 2
x_hash = ((x_hash Mod 69208103) + buffer(i - 1)) * 31& + buffer(i)
Next
End Function
Private Function x_equal(a, b) As Boolean
x_equal = (VarType(a) = vbString) = (VarType(b) = vbString) And StrComp(a, b, this.Compare) = 0
End Function
Friend Sub x_load(data As TThis)
this = data
End Sub
Private Sub x_copy(dest, src)
dest = src
ReDim Preserve dest(this.Count)
End Sub
Private Sub x_enum(dest())
Dim i&, n&
ReDim dest(1 To this.Count - this.Deleted, 1 To 2)
For i = 1 To this.Count
If this.Hashes(i) Then
n = n + 1
dest(n, 1) = this.Keys(i)
If IsObject(this.Items(i)) Then Set dest(n, 2) = this.Items(i) Else dest(n, 2) = this.Items(i)
End If
Next
End Sub
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment