I am trying to now learn how Dictionaries work within VBA, so I created a simple Class Module, A Function, and then two subs, but for reasons beyond me the For loop is completely skipped within the function. Below is the Code for all of the items mentioned above. I do have the Microsoft Scripting Runtime checked in Tools > References. Im not really familiar with how Late and Early Binding are utilized, so I'm wondering if that's one of the issues.
Currently the Set rg = LoanData.Range("AH2") is in a table, I have tried the data in that range as both a table and also as just a range, but the For Loop in the function is skipped if the data is in a Table or not.
Class Module called clsCounty
Public CountyID As Long
Public County As String
Function called ReadCounty
Private Function ReadCounty() As Dictionary
Dim dict As New Dictionary
Dim rg As Range
Set rg = LoanData.Range("AH2")
Dim oCounty As clsCounty, i As Long
For i = 2 To rg.Rows.Count
Set oCounty = New clsCounty
oCounty.CountyID = rg.Cells(i, 1).Value
oCounty.County = rg.Cells(i, 2).Value
dict.Add oCounty.CountyID, oCounty
Next i
Set ReadCounty = dict
End Function
The two subs to write to the immediate window
Private Sub WriteToImmediate(dict As Dictionary)
Dim key As Variant, oCounty As clsCounty
For Each key In dict.Keys
Set oCounty = dict(key)
With oCounty
Debug.Print .CountyID, .County
End With
Next key
End Sub
Sub Main()
Dim dict As Dictionary
Set dict = ReadCounty
WriteToImmediate dict
End Sub
You've declared your range as Set rg = LoanData.Range("AH2") and then use For i = 2 To rg.Rows.Count in your loop. the rg.Rows.Count will be 1 as there is only 1 cell in your range. This is before the starting value for your For loop (2) so it won't do anything.
i.e. For i = 2 to 1
Declare your rg variable with the full range. I'm going to guess something like
With LoanData
Set rg = .Range(.Cells(1,"AH"), .Cells(.Cells(.Rows.Count, "AH").End(xlUp).Row, "AH"))
End With
The problem is indeed in the usage of Set rg = LoanData.Range("AH2"), as mentioned in the other answer.
However, to be a bit more elegant, you may consider using LastRow, function, which takes as arguments columnToCheck and wsName:
Public Function LastRow(wsName As String, Optional columnToCheck As Long = 1) As Long
Dim ws As Worksheet
Set ws = Worksheets(wsName)
LastRow = ws.Cells(ws.Rows.Count, columnToCheck).End(xlUp).Row
End Function
In the code, it would look like this:
Private Function ReadCounty() As Dictionary
Dim dict As New Dictionary
Dim oCounty As clsCounty, i As Long
'For i = 2 To LastRow("LoanData", 34)
For i = 2 To LastRow(LoanData.Name, Range("AH1").Column)
Set oCounty = New clsCounty
oCounty.CountyID = LoanData.Cells(i, "AH").Value
oCounty.County = LoanData.Cells(i, "AI").Value
dict.Add oCounty.CountyID, oCounty
Next i
Set ReadCounty = dict
End Function
Related
I have this table (just few rows) that is loaded to a dictionary.
I have a this class module 'clsTicket'
Public Field As String
Public row As Long
Public cell As String
Public Tabs As String
Public Required As String
This function shows all the items for the key, ind this case 'Ticket_Field3'
Call getDictKeyValuesAll(oDict, "Ticket_Field3")
Function getDictKeyValuesAll(oDict As Object, key As Variant) As Object
' Checks for the key exists
If Not oDict.Exists(key) Then Exit Function
Dim rgOut As Range
Set rgOut = Sheet1.Range("A2").CurrentRegion
Dim oTicket As clsTicket
Set oTicket = oDict(key)
With oTicket
' Write out the values
rgOut.Cells(1, 1).Value = key
rgOut.Cells(1, 2).Value = .row
rgOut.Cells(1, 3).Value = .cell
rgOut.Cells(1, 4).Value = .Tabs
rgOut.Cells(1, 5).Value = .Required
End With
Set getDictKeyValuesAll = oDict
End Function
So far so good and it works as expected.
Now I would like to make a new function to return just a few items, not all items. I thought of using a ParamArray and try to get the values for "Row" and "Cell" for the key "Ticket_Field3". I tried to create the function below with a For loop, but I don't know how to get to each item specifically.
Any suggestion?
Call ShowDictKeyValues(oDict, "Ticket_Field3", "Row", "Cell")
Function getDictKeyValues(oDict As Object, key As Variant, ParamArray arrValues() As Variant) As Object
' Checks for the key exists
If Not oDict.Exists(key) Then Exit Function
Dim rgOut As Range
Set rgOut = wsTicket.Range("A2")
Dim oTicket As clsTicket
Set oTicket = oDict(key)
Dim vArg As Variant, icount As Integer
For icount = 0 To UBound(arrValues)
vArg = arrValues(icount)
rgOut.Cells(1, icount + 1).Value = oDict.Item(vArg)
Next icount
End Function
First, as #Scott Holtzman has already mentioned, you can pass Array("Row", "Cell") to getDictKeyValues instead. So you can replace ParamArray arrValues() As Variant with arrValues As Variant.
Function getDictKeyValues(oDict As Object, key As Variant, arrValues As Variant) As Object
Then you can use CallByName to return the values for each of the specified property names for the class object.
CallByName(oTicket, arrValues(iCount), VbGet)
By the way, since you're not actually returning anything, you can change your function to a sub.
Called Procedure
Sub getDictKeyValues(oDict As Object, key As Variant, arrValues As Variant)
' Checks for the key exists
If Not oDict.Exists(key) Then Exit Sub
Dim rgOut As Range
Set rgOut = wsTicket.Range("A2")
Dim oTicket As clsTicket
Set oTicket = oDict(key)
Dim iCount As Integer
For iCount = LBound(arrValues) To UBound(arrValues)
rgOut.Cells(1, iCount + 1).Value = CallByName(oTicket, arrValues(iCount), VbGet)
Next iCount
End Sub
Call to Procedure
Call getDictKeyValues(oDict, "Ticket_Field3", Array("Row", "Cell"))
Already looked in the forum but was not able to find anything similar.
I would like to create a string array where the index “value” is a string.
For instance:
MyArray(“abc”)=1
MyArray(“def”)=2
MyArray(“ghi”)=3
Is there a way to do this in VBA or can I achieve the same in a different way?
A Dictionary Introduction
A Simple Example
Sub DictIntroduction()
Dim InitText As Variant, InitNumbers As Variant
InitText = Array("abc", "def", "ghi")
InitNumbers = Array(1, 2, 3)
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
Dim i As Long
For i = 0 To UBound(InitText)
dict.Add InitText(i), InitNumbers(i)
Next
Dim Key As Variant
For Each Key In dict.Keys
Debug.Print Key, dict(Key)
Next
Debug.Print dict("abc")
Debug.Print dict("def")
Debug.Print dict("ghi")
End Sub
To find out more about the dictionary visit the following links:
Excel VBA Dictionary - A Complete Guide (Article)
Excel VBA Dictionary (YouTube Playlist)
Sub main()
Set myArray = CreateObject("Scripting.Dictionary")
myArray("abc") = 1
myArray("def") = 2
myArray("ghi") = 3
Debug.Print myArray("abc")
For Each tempKey In myArray
Debug.Print tempKey, myArray(tempKey)
Next
End Sub
I am trying to add a column as the key and the column to the right of it as the value.
Can I do this without a loop?
I tried:
analystDict.Add Key:=refWS.Range("A2:A21"), Item:=refWS.Range("B2:B21")
When I try to Debug.Print I get a Type mismatch error:
For Each x In analystDict.Keys
Debug.Print x, analystDict(x)
Next x
You can't do this in VBA without writing a helper function.
Option Explicit
Public Sub AddTest()
Dim analystDict As Scripting.Dictionary
Set analystDict = New Scripting.Dictionary
Dim refWS As Worksheet
Set refWS = ActiveSheet
AddToDictionary _
analystDict, _
Application.WorksheetFunction.Transpose(refWS.Range("A2:A21").Value), _
Application.WorksheetFunction.Transpose(refWS.Range("B2:B21").Value)
End Sub
Public Sub AddToDictionary(ByRef ipDict As Scripting.Dictionary, ByVal ipKeys As Variant, ByVal ipValues As Variant)
If UBound(ipKeys) <> UBound(ipValues) Then
MsgBox "Arrays are not the same size"
Exit Function
End If
Dim myIndex As Long
For myIndex = LBound(ipKeys) To UBound(ipKeys)
ipDict.Add ipKeys(myIndex), ipValues(myIndex)
Next
End Function
You're taking a shortcut that's not allowed; Dictionary.Add is implemented such that it expects one key/value pair, and adds one item to the dictionary. If you need to add multiple items, you need multiple calls to Dictionary.Add - there's no way around it.
A shortcut that would be allowed though, would be to just grab the values in any 2-column Range and turn that into a dictionary, rather than taking any random two arrays that may or may not be the same size.
Make a function that takes a 2D array and turns it into a dictionary by treating the first column as unique keys, and the second column as values.
Public Function ToDictionary(ByVal keyValuePairs As Variant) As Scripting.Dictionary
If Not IsArray(keyValuePairs) Then Err.Raise 5
If GetDimensions(keyValuePairs) <> 2 Then Err.Raise 5 'see https://stackoverflow.com/q/6901991/1188513
Dim result As Scripting.Dictionary
Set result = New Scripting.Dictionary
Const KEYCOL = 1, VALUECOL = 2
Dim i As Long
For i = LBound(keyValuePairs, KEYCOL) To UBound(keyValuePairs, KEYCOL)
If result.Exists(keyValuePairs(i, KEYCOL)) Then Err.Raise 457
result.Add Key:=keyValuePairs(i, KEYCOL), Item:=keyValuePairs(i, VALUECOL)
Next
Set ToDictionary = result
End Function
Now you can turn any 2-column Range into a Dictionary like this:
Dim things As Scripting.Dictionary
Set things = ToDictionary(Sheet1.Range("A2:B21").Value)
Note that Range.Value yields a 1-based, 2D Variant array whenever it refers to multiple cells.
Nice concept, Mathieu and you can even simplify this a bit. If you don't mind that a later key-value pair overwrites the most recent one then you can skip raising an error and do this:
Public Function ToDictionary(ByVal keyValuePairs As Variant) As Scripting.Dictionary
If Not IsArray(keyValuePairs) Then err.Raise 5
If GetDimensions(keyValuePairs) <> 2 Then err.Raise 5 'see https://stackoverflow.com/q/6901991/1188513
Dim result As Scripting.Dictionary
Set result = New Scripting.Dictionary
Const KEYCOL = 1, VALUECOL = 2
Dim i As Long
For i = LBound(keyValuePairs, KEYCOL) To UBound(keyValuePairs, KEYCOL)
' No need to check if you don't mind have subsequent instance of key-value overwrite the
' the current one.
' If result.Exists(keyValuePairs(i, KEYCOL)) Then err.Raise 457
result(keyValuePairs(i, KEYCOL)) = keyValuePairs(i, VALUECOL)
Next
Set ToDictionary = result
End Function
I have a combobox in an Excel UserForm that I would like to sort alphabetically. I don't have any idea how to add this function, and I would appreciate any help. Here is my VBA:
Private Sub Userform_Initialize()
' Sets range for ComboBox list
Dim rng As Range, r As Range
Set rng = Sheet1.Range("H2:H65536")
For Each r In rng
AddUnique r.value
Next r
End Sub
Sub AddUnique(value As Variant)
Dim i As Integer
Dim inList As Boolean
inList = False
With Me.ComboBox1
For i = 0 To Me.ComboBox1.ListCount - 1
If Me.ComboBox1.List(i) = value Then
inList = True
Exit For
End If
Next i
If Not inList Then
.AddItem value
End If
End With
End Sub
My suggestion is to use a Dictionary to create a collection of only the unique value in your range, then sort it prior to adding the items to your combobox.
If you haven't done so already, add a reference library to your project by going to the Tools menu, then select References. Scroll down the list and find the "Microsoft Scripting Runtime" and check it.
Then it's a simple matter of looping through all the entries -- only adding an item if it doesn't exist already. I lifted a sorting routine from ExcelMastery. Then add the items to your combobox.
Option Explicit
Private Sub Userform_Initialize()
' Sets range for ComboBox list
Dim rng As Range, r As Range
Set rng = Sheet1.Range("H2:H65536")
'--- create a dictionary of the items that will be in
' the combobox
Dim uniqueEntries As Object
Set uniqueEntries = New Scripting.Dictionary
For Each r In rng
'--- all dictionary keys must be a string
Dim keyString As String
If IsNumeric(r) Then
keyString = CStr(r)
Else
keyString = r
End If
If Not uniqueEntries.exists(keyString) Then
uniqueEntries.Add CStr(keyString), r
End If
Next r
Set uniqueEntries = SortDictionaryByKey(uniqueEntries)
CreateComboboxList uniqueEntries
End Sub
Private Sub CreateComboboxList(ByRef dictList As Scripting.Dictionary)
Dim key As Variant
For Each key In dictList.keys
Debug.Print "Adding " & key
'Me.combobox1.AddItem key
Next key
End Sub
'------------------------------------------------------------------
'--- you should put this in a module outside of your userform code
Public Function SortDictionaryByKey(dict As Object, _
Optional sortorder As XlSortOrder = xlAscending) As Object
'--- from ExcelMastery
' https://excelmacromastery.com/vba-dictionary/#Sorting_by_keys
Dim arrList As Object
Set arrList = CreateObject("System.Collections.ArrayList")
' Put keys in an ArrayList
Dim key As Variant, coll As New Collection
For Each key In dict
arrList.Add key
Next key
' Sort the keys
arrList.Sort
' For descending order, reverse
If sortorder = xlDescending Then
arrList.Reverse
End If
' Create new dictionary
Dim dictNew As Object
Set dictNew = CreateObject("Scripting.Dictionary")
' Read through the sorted keys and add to new dictionary
For Each key In arrList
dictNew.Add key, dict(key)
Next key
' Clean up
Set arrList = Nothing
Set dict = Nothing
' Return the new dictionary
Set SortDictionaryByKey = dictNew
End Function
Im copying cells from one sheet to another, finding and matching column header names and pasting to the correct cell. These column header names differ slightly per sheet, altough they contain the same data. My working code has a lot of repetition:
' sub that finds head in a specified worksheet and sets rngCol variable
Sub rngByHead(Sheet As Worksheet, head As String)
' sub for copying data
With Source1
' find and set producer, note name difference)
Call rngByHead(Source1, "bedrijfsnaam")
Dim producent As String
producent = .Cells(docSource1.Row, rngCol).Value
' find and set Fase
Call rngByHead(Source1, "Fase")
Dim fase As String
fase = .Cells(docSource1.Row, rngCol).Value
' find and set Status
Call rngByHead(Source1, "Status")
Dim status As String
status = .Cells(docSource1.Row, rngCol).Value
' find and set versionnumber, note name difference
Call rngByHead(Source1, "Wijziging")
Dim versienummer As String
versienummer = .Cells(docSource1.Row, rngCol).Value
End With
With Target
' find and write all variables to uploadlijst
Call rngByHead(Target, "bestandsnaam")
.Cells(cell.Row, rngCol).Value = bestand
Call rngByHead(Target, "producent")
.Cells(cell.Row, rngCol).Value = producent
Call rngByHead(Target, "fase")
.Cells(cell.Row, rngCol).Value = LCase(fase)
Call rngByHead(Target, "status")
.Cells(cell.Row, rngCol).Value = LCase(status)
Call rngByHead(Target, "versienummer")
.Cells(cell.Row, rngCol).Value = versienummer
End With
I was trying a more cleaner option with a dictionary for matching the different header names in target and data sheets. I also created a secong dictionary to store those values under the specific keys. I keep getting errors on this code, both 424 object missing as ByRef argument type mismatch.
' Create dict
Dim dict As Scripting.Dictionary
' Create dictValues
Dim dictValues As Scripting.Dictionary
Dim key As Object
' Add keys to dict
dict("producent") = "Bedrijfsnaam"
dict("fase") = "Fase"
dict("status") = "Status"
dict("versienummer") = "Wijziging"
dict("documentdatum") = "Datum"
dict("omschrijving1") = "Omschrijving 1"
dict("omschrijving2") = "Omschrijving 2"
dict("omschrijving3") = "Omschrijving 3"
dict("discipline") = "Discipline"
dict("bouwdeel") = "Bouwdeel"
dict("labels") = "Labels"
' store values of sheet Source 1
With Source1
' create second dictValues to store values for each key
Set dictValues = New Scripting.Dictionary
' loop through keys in dict, this line gives error 424
For Each key In dict.Keys
' use dict to pass right value to rngByHead sub
Call rngByHead(Target, dict(key))
' store value of cell to dictValues under same key
dictValues(key) = .Cells(cell.Row, rngCol).Value
Next key
End With
' set values to sheet Target
With Target
' loop through keys in dict
For Each key In dict.Keys
' use dict to pass value of key item to rngByHead sub
Call rngByHead(Target, key)
' set value of cell to dictValues
.Cells(cell.Row, rngCol).Value = dictValues(key)
Next key
End With
What am I doing wrong? I'm new to vba dictionary and can't figure this one out. Thanks for your help!
Try like this:
Dim dict As New Scripting.Dictionary
Dim dictValues As New Scripting.Dictionary
The keyword New initializes an object from type Scripting.Dicitionary. Without it, no new object is initialized, just an object of type Scripting.Dictionary is declared. This is called early binding in VBA. See a bit here - What is the difference between Early and Late Binding?
I fixed it! Posting the code here on Stackoverflow for future reference. It turned out to be very simple, my dictionary was working fine. The key or k variable was set as Variant or Object, so it didn't pass it's value correctly as String to the rngByHead sub. Converting the k to str as String did the trick.
'sub that finds head in a specified worksheet and sets rngCol variable
Sub rngByHead(Sheet As Worksheet, head As String)
'setting up dictionary
Dim dict As New Scripting.Dictionary
Dim dictValues As New Scripting.Dictionary
Dim k As Variant
Dim str As String
'create dictionary
Set dictValues = New Scripting.Dictionary
Set dict = New Scripting.Dictionary
'add keys to dict
dict("producent") = "Bedrijfsnaam"
dict("fase") = "Fase"
dict("status") = "Status"
dict("versienummer") = "Wijziging"
dict("documentdatum") = "Datum"
dict("omschrijving1") = "Omschrijving"
dict("omschrijving2") = "Omschrijving 2"
dict("omschrijving3") = "Omschrijving 3"
dict("discipline") = "Discipline"
dict("bouwdeel") = "Bouwdeel"
dict("labels") = "Labels"
'store values of sheet Source 1
With Source1
'find and set variables using dictionary
'creating array of keys
keys = dict.keys
For Each k In keys
Call rngByHead(Source1, dict(k))
dictValues(k) = .Cells(docSource1.Row, rngCol).Value
Next
End With
With Target
'find and write variables using dictionary
For Each k In keys
'converting k as Variant to str as String
str = k
Call rngByHead(Target, str)
.Cells(cell.Row, rngCol).Value = dictValues(k)
Next
End With
Another note: you have to enable Microsoft Scripting Runtime in microsoft visual basic code editor under Tools > References.
Provided a user has enabled the option Trust Access to the VBA Project object model in File -> Options -> Trust Center -> Trust Center Setttings -> Macro Settings. You can run this code and enable the Microsoft Scripting Runtime reference:
Sub Test()
Dim Ref As Object, CheckRefEnabled%
CheckRefEnabled = 0
With ThisWorkbook
For Each Ref In .VBProject.References
If Ref.Name = "Scripting" Then
CheckRefEnabled = 1
Exit For
End If
Next Ref
If CheckRefEnabled = 0 Then
.VBProject.References.AddFromGUID "{420B2830-E718-11CF-893D-00A0C9054228}", 1, 0
End If
End With
End Sub