Data Dictionaries

Data Dictionaries Vs Arrays
Dictionaries are like arrays with two major advantages.

  1. Dictionaries don't require declaring size. Before using an array we have to 'dimension' it, which can be a problem when we don't know how big the array needs to be.

  2. Dictionaries have keys so we can retrieve items from a dictionary by a name, or an index. Arrays can only be accessed by index.


Data Dictionaries vs Collections
Data Dictionaries are like enhanced Collections but with these differences:
1

  1. CompareMode property specifies case sensitivity for Keys.

  2. A method for determining if an object exists in a Dictionary.

  3. A method for extracting all of the Keys into an Array.

  4. A method for extracting all of the Items into an Array.

  5. A method for changing a Key value.

  6. A method for removing all items from the Dictionary.

  7. Dictionary Keys are not limited to String datatype.

NOTE: An important difference between the Collection object and the Dictionary object is the Item property's behavior. If we use the Item property to reference a nonexistent Key in a Collection, we receive an error. If we use the Item property to reference a nonexistent Key in a Dictionary, that Key will be added to the Dictionary. This is known as a 'silent add'. To prevent silent adds, use the Exists method to determine whether or not a Key is present in a Dictionary.


Syntax

Create a Dictionary Object on any version of Excel on any PC
This uses a late binding example which I prefer. You may hear that late binding is slow. It's not.

Dim MyDictionary as Object

Set MyDictionary = CreateObject("Scripting.Dictionary")


Create a Dictionary Object on any version of XL
This uses a early binding which requires adding the "Microsoft Scripting Runtime" reference to the project (from the VBE menu use Tools > References). This can run into problems when the PC running the workbook does not have the exact reference.

Dim MyDictionary as Scripting.Dictionary

Set MyDictionary = New Scripting.Dictionary


Set Key Case Sensitivity

MyDictionary.CompareMode = vbBinaryCompare 'Case Sensitive

MyDictionary.CompareMode = vbTextCompare 'Case Insensitive

Add Item

MyDictionary.Add ItemKey, ItemValue

Add or Change Item
This is sometimes called the "Silent Add" method. If a key doesn't exist, it gets added.

MyDictionary(ItemKey) = ItemValue

-or-

Set MyDictionary(ItemKey) = myObject

Determine if an Item Exists
If you don't want the "Silent Add" feature, use this before attempting to change items.

If MyDictionary.Exists(ItemKey) Then ...

Get Number of Items

Debug.Print = MyDictionary.Count

Get an Item by Key

MyItem = MyDictionary(ItemKey)

- or -

Set MyObject = MyDictionary(ItemKey)

Get an Item by Number
Note! Dictionary Items start at 0

MyItem = MyDictionary.Items()(ItemNumber)

- or -

Set MyObject = MyDictionary.Items()(ItemNumber)

Get a Key by Number

MyKey = MyDictionary.Keys()(ItemNumber)


Load all Items into an Array

Dim vArray as Variant

vArray = MyDictionary.Items()

Load all Keys into an Array

Dim vArray as Variant

vArray = MyDictionary.Keys()

Remove an Item

MyDictionary.Remove(ItemKey)

Remove All Items

MyDictionary.RemoveAll


Coding Specifics
Loop through all items and keys using index values: NOTE! Early binding only!

Dim n As Long

For n = 0 to MyDictionary.Count -1

Debug.Print MyDictionary.Keys(n), MyDictionary.Items(n)

Next n

Loop through all items and keys using index values. NOTE! Compatible with early and late binding

Dim n As Long

For n = 0 to MyDictionary.Count -1

Debug.Print MyDictionary.Keys()(n), MyDictionary.Items()(n)

Next n

Loop through all items using For Each. NOTE! Compatible with early and late binding

Dim ItemKey As Variant

For Each ItemKey in MyDictionary.Keys

Debug.Print ItemKey, MyDictionary(ItemKey)

Next ItemKey


Usages

Eliminate Duplicates
Dictionaries are perfect for creating unique lists in VBA using the dictionary's "silent add" behavior.

Function GetDistinct(ByVal oTarget as Range) as Variant

Dim vArray as Variant 'Array to hold a range's cells

Dim oDictionary as Object 'Dictionary to hold unique values

Dim v as Variant 'An element of vArray

Set oDictionary = CreateObject("Scripting.Dictionary")


vArray = oTarget

For Each v in vArray

oDictionary(v) = v

Next

GetDistinct = oDictionary.Items()

End Function


To test the above function, select a range of cells with duplicate values then type this into the immediate windows and hit Enter:

For Each v in GetDistinct(Selection): Debug.Print v: Next

Code Table Lookup
If we plan to reference a table repeatedly and "randomly" (as opposed to "sequentially"), putting the table into a dictionary and referencing the table's values is quick and easy. Below is a sample table.

Whatever is in the first column will be the keys, which in this case, are country names. Tied to each key are the values. We can use this routine to index this table so we can retrieve values in a very similar fashion to VLOOKUP().

Public Function Tbl2Dic(ByVal oLo As ListObject) As Object

Dim oDictionary as Object 'Dictionary to hold table rows by key

Dim olr As ListRow 'Table row

Set oDictionary = CreateObject("Scripting.Dictionary")


For each olr In oLo.ListRows

oDictionary(olr.Range(1).Text) = Application.Transpose(Application.Transpose(olr.Range))

Next


Set Tbl2Dic = oDictionary

End Function

The routine places the values in a one dimensional array so if we click inside this table and call this routine like so:

Set v = Tbl2Dic(Selection.ListObject)

v becomes a data dictionary that can be accessed key, and then by column index. Thus, to get Egypt's Net Change (4th column) we can do so like this:

Debug.Print v("Egypt")(4)


Returning Ranges instead of ValuesThe above returns the cell's value, but not it's format. Nor can it be updated. If we want the value and format or to be able to update the cell from VBA we can change the line within the loop to store the ListRow with the key instead of just the values.

oDictionary(olr.Range(1).Text) = olr.Range

Now when we access Egypt's Yearly Change (3rd column) we can see its value in its proper format:

Debug.Print v("Egypt").Range(3).Text

And if we want to change the value, we can with this:

v("Egypt").Range(3) = 0.0200


Using Multiple Keys and Column Names
The prior functions work like VLOOKUP(). VLOOKUP() is designed for just one key field. But sometimes our data has more than one key. In the data sample below are employees. It is possible that two employees could have the same last name. But it is highly unlikely, in a small company, that two employees will have the same first and last name.

We can lookup values using multiple key fields by storing data dictionaries in data dictionaries. For example, we can have a data dictionary of Last Names. We can then store within each Last Name entry a data dictionary of First Names of people having the same last name. Likewise, we can store each row's values in a data dictionary keyed by column name and store that in the previous data dictionary. So when we want to retrieve Steven Buchanan's title we could execute this:

Debug.Print v("Buchanan")("Steven")("Title")

NOTE! v("Buchanan") is a data dictionary of last names. The data dictionary item keyed by "Buchanan" contains a data dictionary of first names associated with people having "Buchanan" as their last name. One of those people have the first name: "Steven". In that data dictionary item is a data dictionary keyed by column names containing all values from that row.

This sounds complicated but it is somewhat simplified using recursion (a program that calls itself). Once we create the routines we can use them very simply. For example, to key the employee table by the first two columns we call the first routine: CrtIDX (Create Index) like so:

Set v = CrtIDX(Selection.ListObject, 2)

CrtIDX sets up the next routine IdxRec (Index Record) which creates a data dictionary for the first key, they calls itself to create a data dictionary for the first key item containing the second key. Once all keys are built, it calls itself again to index all values.

Below are routines that are very similar to what I use in my work. The major difference is the error handling has been removed to make this easy for readers to copy and pasted into their own project. I highly recommend adding error handling.


Public Function CrtIDX(ByVal oLo As ListObject, _

Optional ByVal lKeyCols As Long = 1) As Object


' Description:Create Property Table Dictionary (Recursive)

' Inputs: oLo ListObject (Excel Table)

' lKeyCols Number of columns to use as keys

' Outputs: Me Success: Dictionary

' Failure: Nothing

' Requisites: Function: IdxRec

' Notes: See LstIDX for how to access IDX

' PDD ex: Set oIDX = CrtIDX([PDD],3)

' Example: CrtIDX(Selection.ListObject).Keys()(1)


' Date Ini Modification

' 11/29/21 CWH Initial Development


' Declarations

Const cRoutine As String = "CrtIDX"

Dim oRecords As Object 'Records to be processed in a dictionary

Dim vHdrs As Variant 'Record Field Names/Column Headers

Dim lRow As Long 'Records Row

Dim lCol As Long 'Records Column

Dim v As Variant


' Error Handling Initialization

Set CrtIDX = Nothing

' Initialize Variables

Set oRecords = CreateObject("Scripting.Dictionary")

vHdrs = Application.Transpose( _

Application.Transpose(oLo.HeaderRowRange))

' Procedure

If Not oLo.DataBodyRange Is Nothing Then

' Convert records to dictionary

ReDim v(1 To oLo.ListColumns.Count)

For lRow = 1 To oLo.ListRows.Count

For lCol = 1 To oLo.ListColumns.Count

v(lCol) = oLo.ListRows(lRow).Range(lCol)

Next

oRecords(lRow) = v

Next

' Index Records

Set CrtIDX = IdxRec(oRecords:=oRecords, _

lKeyColumns:=lKeyCols, _

vHdrs:=vHdrs, _

lCol:=1)

End If


End Function

Private Function IdxRec(ByVal oRecords As Object, _

ByVal lKeyColumns As Long, _

ByVal vHdrs As Variant, _

ByVal lCol As Long) As Variant


' Description:Index Records in a Dictionary Object (Recursive)

' Inputs: oRecords Records to index

' lKeyColumns Number of columns to use as keys

' vHdrs Record Field Names/Column Headers

' lCol Table Column to filter

' Outputs: Me Success: Dictionary

' Failure: Nothing

' Requisites: Function: *None!

' Notes: See LstIDX for how to access IDX

' PDD ex: Set oPivot = IdxRec([PDD])

' ?

' Example: See IdxRecUI()


' Date Ini Modification

' 09/30/15 CWH Initial Development


' Declarations

Const cRoutine As String = "IdxRec"

Dim oResults As Object 'Results Dictionary

Dim oChildren As Object 'Filtered Records in a dictionary

Dim lRowChild As Long 'Children Row

Dim oValues As Object 'Unique Values in column lKey + 1

Dim vValue As Variant 'A single value

Dim lCols As Long 'Record Column Count

Dim lRow As Long 'Records Row

Dim n As Long 'Generic Counter


' Error Handling Initialization

Set IdxRec = Nothing


' Initialize Variables

Set oResults = CreateObject("Scripting.Dictionary")

lCols = UBound(oRecords(1))

' Procedure

If lCol = lKeyColumns Then

' Last key column. Return data

For lRow = 1 To oRecords.Count

If lCols = lKeyColumns + 1 Then

' Return value at right using value in column as key

oResults(oRecords(lRow)(lCol)) = oRecords(lRow)(lCols)

Else

' Return values at right using column headings as keys

Set oValues = CreateObject("Scripting.Dictionary")

For n = lKeyColumns + 1 To lCols

oValues(vHdrs(n)) = oRecords(lRow)(n)

Next

Set oResults(oRecords(lRow)(lCol)) = oValues

Set oValues = Nothing

End If

Next

Else

' Not last key column. Determining key values in column

Set oValues = CreateObject("Scripting.Dictionary")

For lRow = 1 To oRecords.Count

oValues(oRecords(lRow)(lCol)) = oRecords(lRow)(lCol)

Next

' Process each unique key value

For Each vValue In oValues

' Filter records on unique key value

Set oChildren = CreateObject("Scripting.Dictionary")

lRowChild = 0

For lRow = 1 To oRecords.Count

If oRecords(lRow)(lCol) = vValue Then

lRowChild = lRowChild + 1

oChildren(lRowChild) = oRecords(lRow)

End If

Next

' Index on key value

Set oResults(vValue) = IdxRec(oChildren, lKeyColumns, vHdrs, lCol + 1)

Set oChildren = Nothing

Next

End If

Set IdxRec = oResults


End Function