-
Notifications
You must be signed in to change notification settings - Fork 90
/
Dictionary.cls
456 lines (404 loc) · 14 KB
/
Dictionary.cls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
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 = "Drop-in replacement for Scripting.Dictionary on Mac\r\n(c) Tim Hall - https://github.com/vba-tools/vba-dictionary"
''
' # Dictionary
'
' Drop-in replacement for Scripting.Dictionary on Mac
'
' @class Dictionary
' @author tim.hall.engr@gmail.com
' @repository https://github.com/vba-tools/vba-dictionary
' @license MIT
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ '
Option Explicit
#Const UseScriptingDictionaryIfAvailable = True
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
' dict_KeyValue 0: FormattedKey, 1: OriginalKey, 2: Value
Private dict_pKeyValues As Collection
Private dict_pKeys() As Variant
Private dict_pItems() As Variant
Private dict_pObjectKeys As Collection
Private dict_pCompareMode As CompareMethod
#Else
Private dict_pDictionary As Object
#End If
Public Enum CompareMethod
BinaryCompare = VBA.vbBinaryCompare
TextCompare = VBA.vbTextCompare
DatabaseCompare = VBA.vbDatabaseCompare
End Enum
Public Property Get CompareMode() As CompareMethod
Attribute CompareMode.VB_Description = "Set or get the string comparison method."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
CompareMode = dict_pCompareMode
#Else
CompareMode = dict_pDictionary.CompareMode
#End If
End Property
Public Property Let CompareMode(Value As CompareMethod)
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Me.Count > 0 Then
' Can't change CompareMode for Dictionary that contains data
' http://msdn.microsoft.com/en-us/library/office/gg278481(v=office.15).aspx
Err.Raise 5 ' Invalid procedure call or argument
End If
dict_pCompareMode = Value
#Else
dict_pDictionary.CompareMode = Value
#End If
End Property
Public Property Get Count() As Long
Attribute Count.VB_Description = "Get the number of items in the dictionary.\n"
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Count = dict_pKeyValues.Count
#Else
Count = dict_pDictionary.Count
#End If
End Property
Public Property Get Item(Key As Variant) As Variant
Attribute Item.VB_Description = "Set or get the item for a given key."
Attribute Item.VB_UserMemId = 0
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Dim dict_KeyValue As Variant
dict_KeyValue = dict_GetKeyValue(Key)
If Not IsEmpty(dict_KeyValue) Then
If VBA.IsObject(dict_KeyValue(2)) Then
Set Item = dict_KeyValue(2)
Else
Item = dict_KeyValue(2)
End If
Else
' Not found -> Returns Empty
End If
#Else
If VBA.IsObject(dict_pDictionary.Item(Key)) Then
Set Item = dict_pDictionary.Item(Key)
Else
Item = dict_pDictionary.Item(Key)
End If
#End If
End Property
Public Property Let Item(Key As Variant, Value As Variant)
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Me.Exists(Key) Then
dict_ReplaceKeyValue dict_GetKeyValue(Key), Key, Value
Else
dict_AddKeyValue Key, Value
End If
#Else
dict_pDictionary.Item(Key) = Value
#End If
End Property
Public Property Set Item(Key As Variant, Value As Variant)
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Me.Exists(Key) Then
dict_ReplaceKeyValue dict_GetKeyValue(Key), Key, Value
Else
dict_AddKeyValue Key, Value
End If
#Else
Set dict_pDictionary.Item(Key) = Value
#End If
End Property
Public Property Let Key(Previous As Variant, Updated As Variant)
Attribute Key.VB_Description = "Change a key to a different key."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Dim dict_KeyValue As Variant
dict_KeyValue = dict_GetKeyValue(Previous)
If Not VBA.IsEmpty(dict_KeyValue) Then
dict_ReplaceKeyValue dict_KeyValue, Updated, dict_KeyValue(2)
End If
#Else
dict_pDictionary.Key(Previous) = Updated
#End If
End Property
''
' Add an item with the given key
'
' @param {Variant} Key
' @param {Variant} Item
' --------------------------------------------- '
Public Sub Add(Key As Variant, Item As Variant)
Attribute Add.VB_Description = "Add a new key and item to the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Not Me.Exists(Key) Then
dict_AddKeyValue Key, Item
Else
' This key is already associated with an element of this collection
Err.Raise 457
End If
#Else
dict_pDictionary.Add Key, Item
#End If
End Sub
''
' Check if an item exists for the given key
'
' @param {Variant} Key
' @return {Boolean}
' --------------------------------------------- '
Public Function Exists(Key As Variant) As Boolean
Attribute Exists.VB_Description = "Determine if a given key is in the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Exists = Not IsEmpty(dict_GetKeyValue(Key))
#Else
Exists = dict_pDictionary.Exists(Key)
#End If
End Function
''
' Get an array of all items
'
' @return {Variant}
' --------------------------------------------- '
Public Function Items() As Variant
Attribute Items.VB_Description = "Get an array containing all items in the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Me.Count > 0 Then
Items = dict_pItems
Else
' Split("") creates initialized empty array that matches Dictionary Keys and Items
Items = VBA.Split("")
End If
#Else
Items = dict_pDictionary.Items
#End If
End Function
''
' Get an array of all keys
'
' @return {Variant}
' --------------------------------------------- '
Public Function Keys() As Variant
Attribute Keys.VB_Description = "Get an array containing all keys in the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
If Me.Count > 0 Then
Keys = dict_pKeys
Else
' Split("") creates initialized empty array that matches Dictionary Keys and Items
Keys = VBA.Split("")
End If
#Else
Keys = dict_pDictionary.Keys
#End If
End Function
''
' Remove an item for the given key
'
' @param {Variant} Key
' --------------------------------------------- '
Public Sub Remove(Key As Variant)
Attribute Remove.VB_Description = "Remove a given key from the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Dim dict_KeyValue As Variant
dict_KeyValue = dict_GetKeyValue(Key)
If Not VBA.IsEmpty(dict_KeyValue) Then
dict_RemoveKeyValue dict_KeyValue
Else
' Application-defined or object-defined error
Err.Raise 32811
End If
#Else
dict_pDictionary.Remove Key
#End If
End Sub
''
' Remove all items
' --------------------------------------------- '
Public Sub RemoveAll()
Attribute RemoveAll.VB_Description = "Remove all information from the dictionary."
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Set dict_pKeyValues = New Collection
Erase dict_pKeys
Erase dict_pItems
#Else
dict_pDictionary.RemoveAll
#End If
End Sub
' ============================================= '
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Private Function dict_GetKeyValue(dict_Key As Variant) As Variant
On Error Resume Next
dict_GetKeyValue = dict_pKeyValues(dict_GetFormattedKey(dict_Key))
Err.Clear
End Function
Private Sub dict_AddKeyValue(dict_Key As Variant, dict_Value As Variant, Optional dict_Index As Long = -1)
If Me.Count = 0 Then
ReDim dict_pKeys(0 To 0)
ReDim dict_pItems(0 To 0)
Else
ReDim Preserve dict_pKeys(0 To UBound(dict_pKeys) + 1)
ReDim Preserve dict_pItems(0 To UBound(dict_pItems) + 1)
End If
Dim dict_FormattedKey As String
dict_FormattedKey = dict_GetFormattedKey(dict_Key)
If dict_Index >= 0 And dict_Index < dict_pKeyValues.Count Then
' Shift keys/items after + including index into empty last slot
Dim dict_i As Long
For dict_i = UBound(dict_pKeys) To dict_Index + 1 Step -1
dict_pKeys(dict_i) = dict_pKeys(dict_i - 1)
If VBA.IsObject(dict_pItems(dict_i - 1)) Then
Set dict_pItems(dict_i) = dict_pItems(dict_i - 1)
Else
dict_pItems(dict_i) = dict_pItems(dict_i - 1)
End If
Next dict_i
' Add key/item at index
dict_pKeys(dict_Index) = dict_Key
If VBA.IsObject(dict_Value) Then
Set dict_pItems(dict_Index) = dict_Value
Else
dict_pItems(dict_Index) = dict_Value
End If
' Add key-value at proper index
dict_pKeyValues.Add Array(dict_FormattedKey, dict_Key, dict_Value), dict_FormattedKey, Before:=dict_Index + 1
Else
' Add key-value as last item
If VBA.IsObject(dict_Key) Then
Set dict_pKeys(UBound(dict_pKeys)) = dict_Key
Else
dict_pKeys(UBound(dict_pKeys)) = dict_Key
End If
If VBA.IsObject(dict_Value) Then
Set dict_pItems(UBound(dict_pItems)) = dict_Value
Else
dict_pItems(UBound(dict_pItems)) = dict_Value
End If
dict_pKeyValues.Add Array(dict_FormattedKey, dict_Key, dict_Value), dict_FormattedKey
End If
End Sub
Private Sub dict_ReplaceKeyValue(dict_KeyValue As Variant, dict_Key As Variant, dict_Value As Variant)
Dim dict_Index As Long
Dim dict_i As Integer
dict_Index = dict_GetKeyIndex(dict_KeyValue(1))
' Remove existing dict_Value
dict_RemoveKeyValue dict_KeyValue, dict_Index
' Add new dict_Key dict_Value back
dict_AddKeyValue dict_Key, dict_Value, dict_Index
End Sub
Private Sub dict_RemoveKeyValue(dict_KeyValue As Variant, Optional ByVal dict_Index As Long = -1)
Dim dict_i As Long
If dict_Index = -1 Then
dict_Index = dict_GetKeyIndex(dict_KeyValue(1))
End If
If dict_Index >= 0 And dict_Index <= UBound(dict_pKeys) Then
' Shift keys/items after index down
For dict_i = dict_Index To UBound(dict_pKeys) - 1
dict_pKeys(dict_i) = dict_pKeys(dict_i + 1)
If VBA.IsObject(dict_pItems(dict_i + 1)) Then
Set dict_pItems(dict_i) = dict_pItems(dict_i + 1)
Else
dict_pItems(dict_i) = dict_pItems(dict_i + 1)
End If
Next dict_i
' Resize keys/items to remove empty slot
If UBound(dict_pKeys) = 0 Then
Erase dict_pKeys
Erase dict_pItems
Else
ReDim Preserve dict_pKeys(0 To UBound(dict_pKeys) - 1)
ReDim Preserve dict_pItems(0 To UBound(dict_pItems) - 1)
End If
End If
dict_pKeyValues.Remove dict_KeyValue(0)
dict_RemoveObjectKey dict_KeyValue(1)
End Sub
Private Function dict_GetFormattedKey(dict_Key As Variant) As String
If VBA.IsObject(dict_Key) Then
dict_GetFormattedKey = dict_GetObjectKey(dict_Key)
ElseIf VarType(dict_Key) = VBA.vbBoolean Then
dict_GetFormattedKey = IIf(dict_Key, "-1__-1", "0__0")
ElseIf VarType(dict_Key) = VBA.vbString Then
dict_GetFormattedKey = dict_Key
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_____
Dim dict_Lowercase As String
dict_Lowercase = ""
Dim dict_i As Integer
Dim dict_Char As String
Dim dict_Ascii As Integer
For dict_i = 1 To VBA.Len(dict_GetFormattedKey)
dict_Char = VBA.Mid$(dict_GetFormattedKey, dict_i, 1)
dict_Ascii = VBA.Asc(dict_Char)
If dict_Ascii >= 97 And dict_Ascii <= 122 Then
dict_Lowercase = dict_Lowercase & dict_Char
Else
dict_Lowercase = dict_Lowercase & "_"
End If
Next dict_i
If dict_Lowercase <> "" Then
dict_GetFormattedKey = dict_GetFormattedKey & "__" & dict_Lowercase
End If
End If
Else
' For numbers, add duplicate to distinguish from strings
' -> 123 -> "123__123"
' "123" -> "123"
dict_GetFormattedKey = VBA.CStr(dict_Key) & "__" & CStr(dict_Key)
End If
End Function
Private Function dict_GetObjectKey(dict_ObjKey As Variant) As String
Dim dict_i As Integer
For dict_i = 1 To dict_pObjectKeys.Count
If dict_pObjectKeys.Item(dict_i) Is dict_ObjKey Then
dict_GetObjectKey = "__object__" & dict_i
Exit Function
End If
Next dict_i
dict_pObjectKeys.Add dict_ObjKey
dict_GetObjectKey = "__object__" & dict_pObjectKeys.Count
End Function
Private Sub dict_RemoveObjectKey(dict_ObjKey As Variant)
Dim dict_i As Integer
For dict_i = 1 To dict_pObjectKeys.Count
If dict_pObjectKeys.Item(dict_i) Is dict_ObjKey Then
dict_pObjectKeys.Remove dict_i
Exit Sub
End If
Next dict_i
End Sub
Private Function dict_GetKeyIndex(dict_Key As Variant) As Long
Dim dict_i As Long
For dict_i = 0 To UBound(dict_pKeys)
If VBA.IsObject(dict_pKeys(dict_i)) And VBA.IsObject(dict_Key) Then
If dict_pKeys(dict_i) Is dict_Key Then
dict_GetKeyIndex = dict_i
Exit For
End If
ElseIf VBA.IsObject(dict_pKeys(dict_i)) Or VBA.IsObject(dict_Key) Then
' Both need to be objects to check equality, skip
ElseIf dict_pKeys(dict_i) = dict_Key Then
dict_GetKeyIndex = dict_i
Exit For
End If
Next dict_i
End Function
#End If
Private Sub Class_Initialize()
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Set dict_pKeyValues = New Collection
Erase dict_pKeys
Erase dict_pItems
Set dict_pObjectKeys = New Collection
#Else
Set dict_pDictionary = CreateObject("Scripting.Dictionary")
#End If
End Sub
Private Sub Class_Terminate()
#If Mac Or Not UseScriptingDictionaryIfAvailable Then
Set dict_pKeyValues = Nothing
Set dict_pObjectKeys = Nothing
#Else
Set dict_pDictionary = Nothing
#End If
End Sub