292

Does VBA have dictionary structure? Like key<>value array?

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81

11 Answers11

364

Yes.

Set a reference to MS Scripting runtime ('Microsoft Scripting Runtime'). As per @regjo's comment, go to Tools->References and tick the box for 'Microsoft Scripting Runtime'.

References Window

Create a dictionary instance using the code below:

Set dict = CreateObject("Scripting.Dictionary")

or

Dim dict As New Scripting.Dictionary 

Example of use:

If Not dict.Exists(key) Then 
    dict.Add key, value
End If 

Don't forget to set the dictionary to Nothing when you have finished using it.

Set dict = Nothing 
danielcooperxyz
  • 960
  • 1
  • 13
  • 28
Mitch Wheat
  • 295,962
  • 43
  • 465
  • 541
  • 21
    This data structure type is provided by the scripting runtime, not by VBA. Basically, VBA can use practically any data structure type that is accessible to it via a COM interface. – David-W-Fenton May 29 '09 at 04:07
  • 172
    Just for the sake of completeness: you need to reference the "Microsoft Scripting Runtime" for this to work (go to Tools->References) and check its box. – regjo Dec 10 '09 at 12:32
  • 2
    Could someone explain to me how the Scripting Runtime's Dictionary object is different from a VBA collection? – David-W-Fenton Apr 29 '11 at 03:38
  • 3
    @ David-W-Fenton: a collection is not keyed – Mitch Wheat Apr 29 '11 at 03:41
  • 7
    Uh, VBA collections ARE keyed. But maybe we have a different definition of `keyed`. – David-W-Fenton Apr 30 '11 at 18:40
  • 3
    In Excel 2010 the reference to "Microsoft Scripting Runtime" is not necessary when using the CreateObject() method. – joweiser Jan 12 '12 at 23:55
  • 9
    I am using Excel 2010... but without the reference to "Microsoft Scripting Runtime" Tools - Ref.. Just doing CreateObject does NOT work. So, @masterjo I think your comment above is wrong. Unless I am missing something.. So, guys Tools -> references is required. – ihightower Jan 21 '12 at 12:26
  • ihightower: would it be possible for you to post a short pastebin of the code that's not working without the reference? I'm really curious! Tested this on a couple different environments and it runs without the reference. – joweiser Jan 22 '12 at 02:44
  • 4
    As an FYI, you can't use the `Dim dict As New Scripting.Dictionary` without the reference. Without the reference, you have to use the late binding `CreateObject` method of instantiating this object. – David Zemens Sep 25 '13 at 13:50
  • 1
    @David-W-Fenton (1) I don't think there is any way to iterate over a Collection's keys. (2) Setting an item (via the default property) with an existing key to a Dictionary will silently overwrite; this is sometimes very useful. (3) It is possible to change the CompareMode of the Dictionary, to be case-sensitive or other CompareMode options. – Zev Spitz Sep 16 '15 at 21:42
  • 1
    @joweiser This is true for previous versions of Office as well. – Zev Spitz Sep 16 '15 at 21:44
  • 2
    @MitchWheat You can also write `dict(key) = value` without explicitly checking if the key exists or not. – Zev Spitz Sep 16 '15 at 21:45
  • 3
    to create the reference to "Microsoft Scripting Runtime" you can also do it by adding this code : `ThisWorkbook.VBProject.References.AddFromFile "C:\windows\system32\scrrun.dll"` – Patrick Lepelletier Jun 12 '16 at 23:30
  • @Andreas Dietrich: the question doesn't mention Excel, so not sure why you are bringing it up. – Mitch Wheat Dec 17 '18 at 10:05
  • 1
    @MitchWheat you are right, but I found it out in the common Excel VBA context and think it may be also interesting to others – Andreas Covidiot Dec 17 '18 at 10:08
  • It's totally irrelevant to the question asked, – Mitch Wheat Dec 17 '18 at 10:15
206

VBA has the collection object:

    Dim c As Collection
    Set c = New Collection
    c.Add "Data1", "Key1"
    c.Add "Data2", "Key2"
    c.Add "Data3", "Key3"
    'Insert data via key into cell A1
    Range("A1").Value = c.Item("Key2")

The Collection object performs key-based lookups using a hash so it's quick.


You can use a Contains() function to check whether a particular collection contains a key:

Public Function Contains(col As Collection, key As Variant) As Boolean
    On Error Resume Next
    col(key) ' Just try it. If it fails, Err.Number will be nonzero.
    Contains = (Err.Number = 0)
    Err.Clear
End Function

Edit 24 June 2015: Shorter Contains() thanks to @TWiStErRob.

Edit 25 September 2015: Added Err.Clear() thanks to @scipilot.

hmqcnoesy
  • 4,165
  • 3
  • 31
  • 47
Caleb Hattingh
  • 9,005
  • 2
  • 31
  • 44
  • 5
    Well done for pointing out the built in Collection object can be used as a dictionary, since the Add method has an optional "key" argument. – Simon Elms May 05 '13 at 12:37
  • 12
    The bad thing about the collection object is, that you cannot check if a key is already in the collection. It'll just throw an error. That's the big thing, i don't like about collections. (i know, that there are workarounds, but most of them are "ugly") – MiVoth Jun 18 '13 at 08:20
  • 5
    Note that the lookup of string keys (eg. c.Item("Key2") ) in the VBA Dictionary IS hashed, but lookup by integer index (eg. c.Item(20) )is not - it's a linear for/next style search and should be avoided. Best to use collections for only string key lookups or for each iteration. – Ben McIntyre Dec 20 '13 at 01:32
  • 1
    That `Contains` function is a beautiful hack on the error handler. – RubberDuck Jul 15 '14 at 04:02
  • 4
    I found a shorter [`Contains`](http://codevba.com/help/collection.htm#.VYVZAjCqopQ): `On Error Resume Next` _ `col(key)` _ `Contains = (Err.Number = 0)` – TWiStErRob Jun 20 '15 at 12:14
  • 1
    Shouldn't you clear `Err` at the end of `Contains()`? Else it would get caught by calling functions' error handling. (Note you cannot therefore use it during error handling without caching the current error being handled.) – scipilot Sep 24 '15 at 22:08
  • @scipilot: That's a good idea. Errors are automatically cleared when hitting `On Error` or `Try/Catch/Finally`, but a user might still have code somewhere that does error detection and sees the leftover stuff from here. – Caleb Hattingh Sep 24 '15 at 23:08
  • 1
    I believe in this case yes. On Error Resume will not raise the error up the stack. This will have the error still set at the call site, but will not 'raise' the error up the call stack. If you use On Error Goto in the function and do the test that way, this will trap the error and the error is local to the function and won't need reset. – PaulG Oct 18 '17 at 19:07
  • 7
    Perhaps the function should be named `ContainsKey`; someone reading only the invocation may confuse it for checking that it contains a particular value. – jpmc26 Feb 22 '18 at 11:57
  • Maybe you want to add On Error GoTo 0 after Err.Clear – Tarik Sep 06 '18 at 15:18
  • I was to have a comma separated key to one value. Can that be done with the a Collection? – Si8 Mar 13 '19 at 14:02
  • @Tarik Looks like that's not necessary. According to docs an error handler is active only "between the occurrence of the error and a Resume, Exit Sub, Exit Function, or Exit Property statement". https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/on-error-statement – Joshua Walsh Aug 04 '20 at 06:06
48

VBA does not have an internal implementation of a dictionary, but from VBA you can still use the dictionary object from MS Scripting Runtime Library.

Dim d
Set d = CreateObject("Scripting.Dictionary")
d.Add "a", "aaa"
d.Add "b", "bbb"
d.Add "c", "ccc"

If d.Exists("c") Then
    MsgBox d("c")
End If
Jarmo
  • 709
  • 5
  • 8
33

An additional dictionary example that is useful for containing frequency of occurence.

Outside of loop:

Dim dict As New Scripting.dictionary
Dim MyVar as String

Within a loop:

'dictionary
If dict.Exists(MyVar) Then
    dict.Item(MyVar) = dict.Item(MyVar) + 1 'increment
Else
    dict.Item(MyVar) = 1 'set as 1st occurence
End If

To check on frequency:

Dim i As Integer
For i = 0 To dict.Count - 1 ' lower index 0 (instead of 1)
    Debug.Print dict.Items(i) & " " & dict.Keys(i)
Next i
Community
  • 1
  • 1
John M
  • 14,338
  • 29
  • 91
  • 143
  • 1
    An additional tutorial link is: http://www.kamath.com/tutorials/tut009_dictionary.asp – John M Feb 15 '12 at 15:50
  • This was a very good answer and I used it. However, I found that I couldn't reference the dict.Items(i) or dict.Keys(i) in the loop as you do. I had to store those (item list and keys list) in separate vars before entering the loop and then use those vars to get to the values I needed. Like - allItems = companyList.Items allKeys = companyList.Keys allItems(i) If not, I would get the error: "Property let procedure not defined and property get procedure did not return an object" when attempting to access Keys(i) or Items(i) in the loop. – raddevus Oct 30 '17 at 19:55
11

Building off cjrh's answer, we can build a Contains function requiring no labels (I don't like using labels).

Public Function Contains(Col As Collection, Key As String) As Boolean
    Contains = True
    On Error Resume Next
        err.Clear
        Col (Key)
        If err.Number <> 0 Then
            Contains = False
            err.Clear
        End If
    On Error GoTo 0
End Function

For a project of mine, I wrote a set of helper functions to make a Collection behave more like a Dictionary. It still allows recursive collections. You'll notice Key always comes first because it was mandatory and made more sense in my implementation. I also used only String keys. You can change it back if you like.

Set

I renamed this to set because it will overwrite old values.

Private Sub cSet(ByRef Col As Collection, Key As String, Item As Variant)
    If (cHas(Col, Key)) Then Col.Remove Key
    Col.Add Array(Key, Item), Key
End Sub

Get

The err stuff is for objects since you would pass objects using set and variables without. I think you can just check if it's an object, but I was pressed for time.

Private Function cGet(ByRef Col As Collection, Key As String) As Variant
    If Not cHas(Col, Key) Then Exit Function
    On Error Resume Next
        err.Clear
        Set cGet = Col(Key)(1)
        If err.Number = 13 Then
            err.Clear
            cGet = Col(Key)(1)
        End If
    On Error GoTo 0
    If err.Number <> 0 Then Call err.raise(err.Number, err.Source, err.Description, err.HelpFile, err.HelpContext)
End Function

Has

The reason for this post...

Public Function cHas(Col As Collection, Key As String) As Boolean
    cHas = True
    On Error Resume Next
        err.Clear
        Col (Key)
        If err.Number <> 0 Then
            cHas = False
            err.Clear
        End If
    On Error GoTo 0
End Function

Remove

Doesn't throw if it doesn't exist. Just makes sure it's removed.

Private Sub cRemove(ByRef Col As Collection, Key As String)
    If cHas(Col, Key) Then Col.Remove Key
End Sub

Keys

Get an array of keys.

Private Function cKeys(ByRef Col As Collection) As String()
    Dim Initialized As Boolean
    Dim Keys() As String

    For Each Item In Col
        If Not Initialized Then
            ReDim Preserve Keys(0)
            Keys(UBound(Keys)) = Item(0)
            Initialized = True
        Else
            ReDim Preserve Keys(UBound(Keys) + 1)
            Keys(UBound(Keys)) = Item(0)
        End If
    Next Item

    cKeys = Keys
End Function
Community
  • 1
  • 1
Evan Kennedy
  • 3,975
  • 1
  • 25
  • 31
8

All the others have already mentioned the use of the scripting.runtime version of the Dictionary class. If you are unable to use this DLL you can also use this version, simply add it to your code.

https://github.com/VBA-tools/VBA-Dictionary/blob/master/Dictionary.cls

It is identical to Microsoft's version.

8

The scripting runtime dictionary seems to have a bug that can ruin your design at advanced stages.

If the dictionary value is an array, you cannot update values of elements contained in the array through a reference to the dictionary.

Kalidas
  • 89
  • 1
  • 1
6

Yes. For VB6, VBA (Excel), and VB.NET

Matthew Flaschen
  • 278,309
  • 50
  • 514
  • 539
  • 3
    You can read question ones more: I've asked about VBA: Visual Basic for Application, not for VB, not for VB.Net, not for any other language. –  May 27 '09 at 12:15
  • 1
    fessGUID: then again, you should read answers more! This answer can also be used for VBA (in particular, the first link). – Konrad Rudolph May 27 '09 at 12:22
  • 5
    I admit. I read the question too fast. But I did tell him what he needed to know. – Matthew Flaschen May 28 '09 at 03:42
  • 1
    The relevance of .Net is that sooner or later VBA (which is currently a subset of VB6) will end up moving to being a subset of .Net. Besides you should never complain if people give you more than you ask for. It's just ungrateful. – Oorang May 29 '09 at 20:30
  • 5
    @Oorang, there's absolutely no evidence of VBA becoming a subset of VB.NET, backcompat rules in Office - imagine trying to convert every Excel macro ever written. – Richard Gadsden Aug 27 '10 at 14:26
  • 2
    VBA is actually a SUPERSET of VB6. It uses the same core DLL as VB6, but then adds on all sorts of functionality for the specific applications in Office. – David-W-Fenton May 06 '11 at 01:47
  • @David-W-Fenton: If I recall correctly, VBA is actually the language engine that VB6 and its predecessors use. There is a core VBA language and then additional libraries for Office applications like Word, Excel, Access. – Simon Elms May 05 '13 at 12:33
  • The "VB6" link above has suffered link rot. The Wayback Machine has a mostly readable version [here](https://web.archive.org/web/20070216062650/http://support.microsoft.com/kb/187234). For now, VBA documentation for `Scripting.Dictionary` is found at: https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/dictionary-object. – jcb May 14 '19 at 17:03
5

If by any reason, you can't install additional features to your Excel or don't want to, you can use arrays as well, at least for simple problems. As WhatIsCapital you put name of the country and the function returns you its capital.

Sub arrays()
Dim WhatIsCapital As String, Country As Array, Capital As Array, Answer As String

WhatIsCapital = "Sweden"

Country = Array("UK", "Sweden", "Germany", "France")
Capital = Array("London", "Stockholm", "Berlin", "Paris")

For i = 0 To 10
    If WhatIsCapital = Country(i) Then Answer = Capital(i)
Next i

Debug.Print Answer

End Sub
user2604899
  • 99
  • 1
  • 3
  • 2
    The concept of this answer is sound, but the sample code won't run as written. Each variable needs its own `Dim` keyword, `Country` and `Capital` need to be declared as Variants due to the use of `Array()`, `i` ought to be declared (and must be if `Option Explicit` is set), and the loop counter is going to throw an out of bound error -- safer to use `UBound(Country)` for the `To` value. Also maybe worth noting that while the `Array()` function is a useful shortcut, it's not the standard way to declare arrays in VBA. – jcb Feb 27 '18 at 01:11
3

VBA can use the dictionary structure of Scripting.Runtime.

And its implementation is actually a fancy one - just by doing myDict(x) = y, it checks whether there is a key x in the dictionary and if there is not such, it even creates it. If it is there, it uses it.

And it does not "yell" or "complain" about this extra step, performed "under the hood". Of course, you may check explicitly, whether a key exists with Dictionary.Exists(key). Thus, these 5 lines:

If myDict.exists("B") Then
    myDict("B") = myDict("B") + i * 3
Else
    myDict.Add "B", i * 3
End If

are the same as this 1 liner - myDict("B") = myDict("B") + i * 3. Check it out:

Sub TestMe()

    Dim myDict As Object, i As Long, myKey As Variant
    Set myDict = CreateObject("Scripting.Dictionary")
    
    For i = 1 To 3
        Debug.Print myDict.Exists("A")
        myDict("A") = myDict("A") + i
        myDict("B") = myDict("B") + 5
    Next i
    
    For Each myKey In myDict.keys
        Debug.Print myKey; myDict(myKey)
    Next myKey

End Sub

enter image description here

Vityata
  • 42,633
  • 8
  • 55
  • 100
3

You can access a non-Native HashTable through System.Collections.HashTable.

HashTable

Represents a collection of key/value pairs that are organized based on the hash code of the key.

Not sure you would ever want to use this over Scripting.Dictionary but adding here for the sake of completeness. You can review the methods in case there are some of interest e.g. Clone, CopyTo

Example:

Option Explicit

Public Sub UsingHashTable()

    Dim h As Object
    Set h = CreateObject("System.Collections.HashTable")
   
    h.Add "A", 1
    ' h.Add "A", 1  ''<< Will throw duplicate key error
    h.Add "B", 2
    h("B") = 2
      
    Dim keys As mscorlib.IEnumerable 'Need to cast in order to enumerate  'https://stackoverflow.com/a/56705428/6241235
    
    Set keys = h.keys
    
    Dim k As Variant
    
    For Each k In keys
        Debug.Print k, h(k)                      'outputs the key and its associated value
    Next
    
End Sub

This answer by @MathieuGuindon gives plenty of detail about HashTable and also why it is necessary to use mscorlib.IEnumerable (early bound reference to mscorlib) in order to enumerate the key:value pairs.


QHarr
  • 83,427
  • 12
  • 54
  • 101