Created
September 13, 2019 14:51
-
-
Save florentbr/0488726f5bc22657319d52f49dbb0c62 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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