diff --git a/Dictionary.cls b/Dictionary.cls index 1a7e8a3..0a6ab55 100644 --- a/Dictionary.cls +++ b/Dictionary.cls @@ -23,8 +23,10 @@ Option Explicit ' Constants and Private Variables ' --------------------------------------------- ' -' Store 0: Index, 1: Key, 2: Value +' KeyValue 0: Key, 1: Value Private pKeyValues As Collection +Private pKeys() As Variant +Private pItems() As Variant Private pCompareMode As CompareMethod ' --------------------------------------------- ' @@ -132,23 +134,7 @@ End Function ' @return {Variant} ' --------------------------------------------- ' Public Function Items() As Variant - Dim Arr As Variant - - If pKeyValues.Count > 0 Then - Dim i As Long - ReDim Arr(pKeyValues.Count - 1) - For i = 1 To pKeyValues.Count - If IsObject(pKeyValues(i)(2)) Then - Set Arr(i - 1) = pKeyValues(i)(2) - Else - Arr(i - 1) = pKeyValues(i)(2) - End If - Next i - Else - Arr = Array() - End If - - Items = Arr + Items = pItems End Function '' @@ -157,19 +143,7 @@ End Function ' @return {Variant} ' --------------------------------------------- ' Public Function Keys() As Variant - Dim Arr As Variant - - If pKeyValues.Count > 0 Then - Dim i As Long - ReDim Arr(pKeyValues.Count - 1) - For i = 1 To pKeyValues.Count - Arr(i - 1) = pKeyValues(i)(1) - Next i - Else - Arr = Array() - End If - - Keys = Arr + Keys = pKeys End Function '' @@ -194,6 +168,8 @@ End Sub ' --------------------------------------------- ' Public Sub RemoveAll() Set pKeyValues = New Collection + Erase pKeys + Erase pItems End Sub ' ============================================= ' @@ -205,45 +181,111 @@ Private Function GetKeyValue(Key As Variant) As Variant GetKeyValue = pKeyValues(GetFormattedKey(Key)) End Function -Private Sub AddKeyValue(Key As Variant, Value As Variant) +Private Sub AddKeyValue(Key As Variant, Value As Variant, Optional Index As Long = -1) + If Me.Count = 0 Then + ReDim Preserve pKeys(0 To 0) + ReDim Preserve pItems(0 To 0) + Else + ReDim Preserve pKeys(0 To UBound(pKeys) + 1) + ReDim Preserve pItems(0 To UBound(pItems) + 1) + End If + Dim FormattedKey As String FormattedKey = GetFormattedKey(Key) - pKeyValues.Add Array(pKeyValues.Count + 1, FormattedKey, Value), FormattedKey + + If Index > 0 And Index <= pKeyValues.Count Then + Dim i As Long + For i = UBound(pKeys) To Index Step -1 + pKeys(i) = pKeys(i - 1) + If IsObject(pItems(i - 1)) Then + Set pItems(i) = pItems(i - 1) + Else + pItems(i) = pItems(i - 1) + End If + Next i + + pKeys(Index - 1) = Key + If IsObject(Value) Then + Set pItems(Index - 1) = Value + Else + pItems(Index - 1) = Value + End If + + pKeyValues.Add Array(FormattedKey, Key, Value), FormattedKey, Before:=Index + Else + pKeys(UBound(pKeys)) = Key + If IsObject(Value) Then + Set pItems(UBound(pItems)) = Value + Else + pItems(UBound(pItems)) = Value + End If + + pKeyValues.Add Array(FormattedKey, Key, Value), FormattedKey + End If End Sub Private Sub ReplaceKeyValue(KeyValue As Variant, Key As Variant, Value As Variant) - ' Remove previous KeyValue - RemoveKeyValue KeyValue - - ' Add new key and value Dim Index As Long - Dim NewKeyValue As Variant - Index = KeyValue(0) - NewKeyValue = Array(Index, GetFormattedKey(Key), Value) + Dim i As Integer - ' Add updated key value back to KeyValues - If pKeyValues.Count = 0 Then - pKeyValues.Add NewKeyValue, CStr(Key) - ElseIf Index > pKeyValues.Count Then - pKeyValues.Add NewKeyValue, CStr(Key), After:=Index - 1 - Else - pKeyValues.Add NewKeyValue, CStr(Key), Before:=Index - End If + For i = 0 To UBound(pKeys) + If pKeys(i) = KeyValue(1) Then + Index = i + 1 + Exit For + End If + Next i + + ' Remove existing value + RemoveKeyValue KeyValue, Index + + ' Add new key value back + AddKeyValue Key, Value, Index End Sub -Private Sub RemoveKeyValue(KeyValue As Variant) +Private Sub RemoveKeyValue(KeyValue As Variant, Optional ByVal Index As Long = -1) + Dim i As Long + If Index = -1 Then + For i = 0 To UBound(pKeys) + If pKeys(i) = KeyValue(1) Then + Index = i + End If + Next i + Else + Index = Index - 1 + End If + + If Index >= 0 And Index <= UBound(pKeys) Then + For i = Index To UBound(pKeys) - 1 + pKeys(i) = pKeys(i + 1) + + If IsObject(pItems(i + 1)) Then + Set pItems(i) = pItems(i + 1) + Else + pItems(i) = pItems(i + 1) + End If + Next i + + If UBound(pKeys) = 0 Then + Erase pKeys + Erase pItems + Else + ReDim Preserve pKeys(0 To UBound(pKeys) - 1) + ReDim Preserve pItems(0 To UBound(pItems) - 1) + End If + End If + pKeyValues.Remove KeyValue(0) End Sub Private Function GetFormattedKey(Key As Variant) As String GetFormattedKey = CStr(Key) - If Me.CompareMode = CompareMethod.TextCompare Then - GetFormattedKey = UCase(GetFormattedKey) - ElseIf Me.CompareMode = CompareMethod.BinaryCompare Then + If Me.CompareMode = CompareMethod.BinaryCompare Then ' Collection does not have method of setting key comparison ' So case-sensitive keys aren't supported by default ' -> Approach: Append lowercase characters to original key ' AbC -> AbC__b, abc -> abc__abc, ABC -> ABC + ' Won't work in very strange cases, but should work for now + ' AbBb -> AbBb__bb matches AbbB -> AbbB__bb Dim Lowercase As String Lowercase = "" @@ -251,7 +293,7 @@ Private Function GetFormattedKey(Key As Variant) As String Dim Ascii As Integer Dim Char As String For i = 1 To Len(GetFormattedKey) - Char = Mid$(GetFormattedKey, i, 1) + Char = VBA.Mid$(GetFormattedKey, i, 1) Ascii = Asc(Char) If Ascii >= 97 And Ascii <= 122 Then Lowercase = Lowercase & Char @@ -264,90 +306,10 @@ Private Function GetFormattedKey(Key As Variant) As String End If End Function -'Private Function GetKey(KeyValue As Variant) As String -' If Not IsEmpty(KeyValue) Then -' GetKey = KeyValue(0) -' End If -'End Function -'Private Function GetValue(KeyValue As Variant) As Variant -' If Not IsEmpty(KeyValue) Then -' If IsObject(KeyValue(1)) Then -' Set GetValue = KeyValue(1) -' Else -' GetValue = KeyValue(1) -' End If -' End If -'End Function -'Private Function GetIndex(KeyValue As Variant) As Long -' If Not IsEmpty(KeyValue) Then -' GetIndex = KeyValue(2) -' End If -'End Function - -'Private Function IndexOfKey(Key As Variant) As Long -' Dim i As Long -' Key = CStr(Key) -' -' For i = 1 To pKeys.Count -' If VBA.StrComp(pKeys(i), Key, Me.CompareMode) = 0 Then -' IndexOfKey = i -' Exit Function -' End If -' Next i -' -' ' Not found -' IndexOfKey = -1 -'End Function -' -'Private Sub AddItem(Key As Variant, Item As Variant) -' Dim Index As Long -' Index = IndexOfKey(Key) -' -' If Index >= 0 Then -' ReplaceItemInCollection pItems, Item, Index -' Else -' pKeys.Add CStr(Key) -' pItems.Add Item -' End If -'End Sub -' -'Private Sub ReplaceItemInCollection(ByRef Coll As Collection, Item As Variant, Index As Long) -' If Index >= 1 And Index <= Coll.Count Then -' Coll.Remove Index -' -' If Coll.Count = 0 Then -' Coll.Add Item -' ElseIf Index > Coll.Count Then -' Coll.Add Item, After:=Index - 1 -' Else -' Coll.Add Item, Before:=Index -' End If -' End If -'End Sub -' -'Private Function CollectionToArray(Coll As Collection) As Variant -' Dim Arr As Variant -' Dim i As Long -' -' ' Collection is 1-based / Variant is 0-based -' If Coll.Count > 0 Then -' ReDim Arr(Coll.Count - 1) -' For i = 1 To Coll.Count -' If IsObject(Coll(i)) Then -' Set Arr(i - 1) = Coll(i) -' Else -' Arr(i - 1) = Coll(i) -' End If -' Next i -' Else -' Arr = Array() -' End If -' -' CollectionToArray = Arr -'End Function - Private Sub Class_Initialize() Set pKeyValues = New Collection + Erase pKeys + Erase pItems End Sub Private Sub Class_Terminate() diff --git a/specs/Specs.bas b/specs/Specs.bas index 8125a64..fca8994 100644 --- a/specs/Specs.bas +++ b/specs/Specs.bas @@ -1,9 +1,15 @@ Attribute VB_Name = "Specs" Public Function Specs() As SpecSuite - InlineRunner.RunSuite RunSpecs(True) - InlineRunner.RunSuite RunSpecs(False) - - SpeedTest + #If Mac Then + ' Mac + InlineRunner.RunSuite RunSpecs(UseNative:=False) + SpeedTest CompareToNative:=False + #Else + ' Windows + InlineRunner.RunSuite RunSpecs(UseNative:=True) + InlineRunner.RunSuite RunSpecs(UseNative:=False) + SpeedTest CompareToNative:=True, UsePreciseTimer:=True + #End If End Function Public Function RunSpecs(Optional UseNative As Boolean = False) As SpecSuite @@ -148,7 +154,7 @@ Public Function RunSpecs(Optional UseNative As Boolean = False) As SpecSuite With Specs.It("should get an array of all items") Set Dict = CreateDictionary(UseNative) - .Expect(UBound(Dict.Items)).ToEqual -1 + .Expect(Dict.Items).RunMatcher "Specs.ToBeAnEmptyArray", "to be an empty array" Dict.Add "A", 123 Dict.Add "B", 3.14 @@ -159,12 +165,18 @@ Public Function RunSpecs(Optional UseNative As Boolean = False) As SpecSuite .Expect(UBound(Items)).ToEqual 3 .Expect(Items(0)).ToEqual 123 .Expect(Items(3)).ToEqual True + + Dict.Remove "A" + Dict.Remove "B" + Dict.Remove "C" + Dict.Remove "D" + .Expect(Dict.Items).RunMatcher "Specs.ToBeAnEmptyArray", "to be an empty array" End With With Specs.It("should get an array of all keys") Set Dict = CreateDictionary(UseNative) - .Expect(UBound(Dict.Keys)).ToEqual -1 + .Expect(Dict.Keys).RunMatcher "Specs.ToBeAnEmptyArray", "to be an empty array" Dict.Add "A", 123 Dict.Add "B", 3.14 @@ -175,6 +187,9 @@ Public Function RunSpecs(Optional UseNative As Boolean = False) As SpecSuite .Expect(UBound(Keys)).ToEqual 3 .Expect(Keys(0)).ToEqual "A" .Expect(Keys(3)).ToEqual "D" + + Dict.RemoveAll + .Expect(Dict.Keys).RunMatcher "Specs.ToBeAnEmptyArray", "to be an empty array" End With With Specs.It("should remove item") @@ -248,86 +263,171 @@ Public Function RunSpecs(Optional UseNative As Boolean = False) As SpecSuite Set RunSpecs = Specs End Function -Public Sub SpeedTest() - Dim NativeResults(3) As Variant - Dim NonNativeResults(3) As Variant +Public Sub SpeedTest(Optional CompareToNative As Boolean = False, Optional UsePreciseTimer As Boolean = False) + Dim Counts As Variant + Counts = Array(5000, 5000, 5000, 5000, 7500, 7500, 7500, 7500) - NativeResults(0) = RunSpeedTest(100, True) - NativeResults(1) = RunSpeedTest(250, True) - NativeResults(2) = RunSpeedTest(500, True) - NativeResults(3) = RunSpeedTest(1000, True) + Dim Baseline As Collection + If CompareToNative Then + Set Baseline = RunSpeedTest(Counts, True, UsePreciseTimer) + End If - NonNativeResults(0) = RunSpeedTest(100, False) - NonNativeResults(1) = RunSpeedTest(250, False) - NonNativeResults(2) = RunSpeedTest(500, False) - NonNativeResults(3) = RunSpeedTest(1000, False) + Dim Results As Collection + Set Results = RunSpeedTest(Counts, False, UsePreciseTimer) - Debug.Print vbNewLine & "SpeedTest Results - Scripting.Dictionary vs. VBA-Dictionary" & vbNewLine - PrintResults "Add", NativeResults, NonNativeResults, 1 - PrintResults "Iterate", NativeResults, NonNativeResults, 2 + Debug.Print vbNewLine & "SpeedTest Results:" & vbNewLine + PrintResults "Add", Baseline, Results, 0 + PrintResults "Iterate", Baseline, Results, 1 End Sub -Public Function PrintResults(Test As String, Before As Variant, After As Variant, Index As Integer) As String - Dim BeforeAvg As Double - Dim AfterAvg As Double +Public Sub PrintResults(Test As String, Baseline As Collection, Results As Collection, Index As Integer) + Dim BaselineAvg As Single + Dim ResultsAvg As Single Dim i As Integer - For i = LBound(Before) To UBound(Before) - BeforeAvg = BeforeAvg + Before(i)(0) / (CDbl(Before(i)(Index)) / 1000) - AfterAvg = AfterAvg + After(i)(0) / (CDbl(After(i)(Index)) / 1000) - Next i - - BeforeAvg = BeforeAvg / (UBound(Before) + 1) - AfterAvg = AfterAvg / (UBound(After) + 1) - - Dim Results As String - Results = Test & ": " & Format(BeforeAvg, "#,##0") & " ops./s vs. " & Format(AfterAvg, "#,##0") & " ops./s, " - If AfterAvg <= BeforeAvg Then - Results = Results & Format(BeforeAvg / AfterAvg, "#,##0") & "x slower" - Else - Results = Results & Format(AfterAvg / BeforeAvg, "#,##0") & "x faster" + If Not Baseline Is Nothing Then + For i = 1 To Baseline.Count + BaselineAvg = BaselineAvg + Baseline(i)(Index) + Next i + BaselineAvg = BaselineAvg / Baseline.Count End If - Debug.Print Results - For i = LBound(Before) To UBound(Before) - Debug.Print " " & Format(Before(i)(0) / (Before(i)(Index) / 1000), "#,##0") & " vs. " & Format(After(i)(0) / (After(i)(Index) / 1000), "#,##0") + For i = 1 To Results.Count + ResultsAvg = ResultsAvg + Results(i)(Index) Next i - Debug.Print "" -End Function - -Public Function RunSpeedTest(Optional Count As Long = 1000, Optional UseNative As Boolean = False) As Variant - Dim Dict As Object - Set Dict = CreateDictionary(UseNative) + ResultsAvg = ResultsAvg / Results.Count - Dim Timer As New PreciseTimer - Timer.StartCounter + Dim Result As String + Result = Test & ": " & Format(Round(ResultsAvg, 0), "#,##0") & " ops./s" - Dim i As Long - For i = 1 To Count - Dict.Add CStr(i), i - Next i + If Not Baseline Is Nothing Then + Result = Result & " vs. " & Format(Round(BaselineAvg, 0), "#,##0") & " ops./s " - Dim AddResult As Double - AddResult = Timer.TimeElapsed + If ResultsAvg < BaselineAvg Then + Result = Result & Format(Round(BaselineAvg / ResultsAvg, 0), "#,##0") & "x slower" + ElseIf BaselineAvg > ResultsAvg Then + Result = Result & Format(Round(ResultsAvg / BaselineAvg, 0), "#,##0") & "x faster" + End If + End If + Result = Result - Timer.StartCounter + If Results.Count > 1 Then + Result = Result & vbNewLine + For i = 1 To Results.Count + Result = Result & " " & Format(Round(Results(i)(Index), 0), "#,##0") + + If Not Baseline Is Nothing Then + Result = Result & " vs. " & Format(Round(Baseline(i)(Index), 0), "#,##0") + End If + + Result = Result & vbNewLine + Next i + End If + Debug.Print Result +End Sub + +Public Function RunSpeedTest(Counts As Variant, Optional UseNative As Boolean = False, Optional UsePreciseTimer As Boolean = False) As Collection + Dim Results As New Collection + Dim CountIndex As Integer + Dim Dict As Object + Dim StartTime As Single + Dim i As Long + Dim AddResult As Single Dim Key As Variant Dim Value As Variant - For Each Key In Dict.Keys - Value = Dict.Item(Key) - Next Key + Dim IterateResult As Single - Dim IterateResult As Double - IterateResult = Timer.TimeElapsed + For CountIndex = LBound(Counts) To UBound(Counts) + Set Dict = CreateDictionary(UseNative) + + If UsePreciseTimer Then + Dim Timer As New PreciseTimer + Timer.StartTimer + Else + StartTime = VBA.Timer + End If + + For i = 1 To Counts(CountIndex) + Dict.Add CStr(i), i + Next i + + If UsePreciseTimer Then + AddResult = CSng(Timer.TimeElapsed / 1000) + Else + AddResult = VBA.Timer - StartTime + End If + + ' Convert to ops./s + If AddResult > 0 Then + AddResult = Counts(CountIndex) / AddResult + Else + ' Due to single precision, timer resolution is 0.01 ms, set to 0.005 ms + AddResult = Counts(CountIndex) / 0.005 + End If + + If UsePreciseTimer Then + Timer.StartTimer + Else + StartTime = VBA.Timer + End If + + For Each Key In Dict.Keys + Value = Dict.Item(Key) + Next Key + + If UsePreciseTimer Then + IterateResult = CSng(Timer.TimeElapsed / 1000) + Else + IterateResult = VBA.Timer - StartTime + End If + + ' Convert to ops./s + If IterateResult > 0 Then + IterateResult = Counts(CountIndex) / IterateResult + Else + ' Due to single precision, timer resolution is 0.01 ms, set to 0.005 ms + IterateResult = Counts(CountIndex) / 0.005 + End If + + Results.Add Array(AddResult, IterateResult) + Next CountIndex - RunSpeedTest = Array(Count, AddResult, IterateResult) + Set RunSpeedTest = Results End Function Public Function CreateDictionary(Optional UseNative As Boolean = False) As Object If UseNative Then - Set CreateDictionary = New Scripting.Dictionary + Set CreateDictionary = CreateObject("Scripting.Dictionary") + Else + Set CreateDictionary = New Dictionary + End If +End Function + +Public Function ToBeAnEmptyArray(Actual As Variant) As Variant + Dim UpperBound As Long + + Err.Clear + On Error Resume Next + + ' First, make sure it's an array + If IsArray(Actual) = False Then + ' we weren't passed an array, return True + ToBeAnEmptyArray = True Else - Set CreateDictionary = New VBAProject.Dictionary + ' Attempt to get the UBound of the array. If the array is + ' unallocated, an error will occur. + UpperBound = UBound(Actual, 1) + If (Err.Number <> 0) Then + ToBeAnEmptyArray = True + Else + ' Check for case of -1 UpperBound (Scripting.Dictionary.Keys/Items) + Err.Clear + If LBound(Actual) > UpperBound Then + ToBeAnEmptyArray = True + Else + ToBeAnEmptyArray = False + End If + End If End If End Function diff --git a/specs/VBA-Dictionary - Specs.xlsm b/specs/VBA-Dictionary - Specs.xlsm index e8e1e11..bcaacbf 100644 Binary files a/specs/VBA-Dictionary - Specs.xlsm and b/specs/VBA-Dictionary - Specs.xlsm differ