Get specific items values from a dictionary key - excel

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"))

Related

How do I add multiple Keys and Values into dictionary at the same time?

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

Private Function skipping For Loop

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

Need help adding alphabetical sort to combobox

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

Excel VBA: Why can't I get a range from a worksheet I passed into a function?

I'm trying to use a helper function to get a range to store it into a variant, but I'm running into some issues.
At first, I just simply tried the following:
Function GetRange(RangeLetter As String, Optional LastUniqueLine As Long = 1048576) As Varient
Static LastUniqueLineStr As String
If LastUniqueLineStr = "" Then
LastUniqueLineStr = LastUniqueLine
End If
Set GetRange = Range(RangeLetter + "2:" + LastUniqueLineStr)
End Function
But that didn't seem to work. Range() seemed to be out of scope here or something, so I figured I had to pass in the worksheet to get it working:
Function GetRange(RangeLetter As String, Optional LastUniqueLine As Long = 1048576, Optional ActiveSheet As Worksheet) As Variant
Static LastUniqueLineStr As String
Static CurrentSheet As Worksheet
'If CurrentSheet = Nothing Then
Set CurrentSheet = ActiveSheet
'End If
If LastUniqueLineStr = "" Then
LastUniqueLineStr = LastUniqueLine
End If
Set GetRange = CurrentSheet.Range(RangeLetter + "2:" + LastUniqueLineStr) ' This is the line where I get the error.
End Function
And that's not working either. I get the following error:
Run-time error '1004':
Method 'Range' of object 'Worksheet' failed
How do I get the range I want out of this when I call it?
Try:
GetRange = Range(RangeLetter + "2:" + RangeLetter + LastUniqueLineStr).Value
You were missing a RangeLetter which was resulting in a malformed address. Also, use the .Value property to return a variant/array, and omit the Set keyword.
I continue to get the error when qualifying as CurrentSheet.Range... there is no ActiveSheet within the context of the Function, so you could pass a worksheet as a variable:
Sub Test()
Dim var As Variant
var = GetRange(ActiveSheet, "A")
End Sub
Function GetRange(sh As Worksheet, RangeLetter As String, Optional LastUniqueLine As Long = 1048576, Optional ActiveSheet As Worksheet) As Variant
Static LastUniqueLineStr As String
Dim myRange As Range
If LastUniqueLineStr = "" Then
LastUniqueLineStr = LastUniqueLine
End If
Set myRange = sh.Range(RangeLetter + "2:" + RangeLetter + LastUniqueLineStr)
GetRange = myRange.Value ' This is the line where I get the error.
End Function
It looks to me like the error is in the line where your setting Your "GetRange" variable. It looks like the result of .Range(RangeLetter + "2:" + Last....) would create a range with an address of "a2:#" which will fail. A range in that format needs to be "a2:e7" or similar. Your range reference has to be symmetrical. "RC:RC", "R:R", or "C:C". Hope that helps.

Extracting a Specific Variable from a Class Module in VBA to a Standard Module

All,
The following code is from Bloomberg. It is designed to extract bulk data from their servers. The code works, but I am trying to extract a specific variable generated in the class module and bring it to the Regular Module for user defined functions. Thanks for the help.
Option Explicit
Private WithEvents session As blpapicomLib2.session
Dim refdataservice As blpapicomLib2.Service
Private Sub Class_Initialize()
Set session = New blpapicomLib2.session
session.QueueEvents = True
session.Start
session.OpenService ("//blp/refdata")
Set refdataservice = session.GetService("//blp/refdata")
End Sub
Public Sub MakeRequest(sSecList As String)
Dim sFldList As Variant
Dim req As Request
Dim nRow As Long
sFldList = "CALL_SCHEDULE"
Set req = refdataservice.CreateRequest("ReferenceDataRequest") 'request type
req.GetElement("securities").AppendValue (sSecList) 'security + field as string array
req.GetElement("fields").AppendValue (sFldList) 'field as string var
Dim cid As blpapicomLib2.CorrelationId
Set cid = session.SendRequest(req)
End Sub
Public Sub session_ProcessEvent(ByVal obj As Object)
Dim eventObj As blpapicomLib2.Event
Set eventObj = obj
If Application.Ready Then
If eventObj.EventType = PARTIAL_RESPONSE Or eventObj.EventType = RESPONSE Then
Dim it As blpapicomLib2.MessageIterator
Set it = eventObj.CreateMessageIterator()
Do While it.Next()
Dim msg As Message
Set msg = it.Message
Dim Security As Element
Set Security = msg.GetElement("securityData").GetValue(0)
Sheet1.Cells(4, 4).Value = Security.GetElement("security").Value
Dim fieldArray As Element
Set fieldArray = Security.GetElement("fieldData")
Dim field As blpapicomLib2.Element
Set field = fieldArray.GetElement(0)
If field.DataType = 15 Then
Dim numBulkValues As Long
numBulkValues = field.NumValues '76
Dim index As Long
For index = 0 To numBulkValues - 1
Dim bulkElement As blpapicomLib2.Element
Set bulkElement = field.GetValue(index)
Dim numBulkElements As Integer
numBulkElements = bulkElement.NumElements '2 elements per each pt
ReDim Call_Sch(0 To numBulkValues - 1, 0 To numBulkElements - 1) As Variant
Dim ind2 As Long
For ind2 = 0 To numBulkElements - 1
Dim elem As blpapicomLib2.Element
Set elem = bulkElement.GetElement(ind2)
Call_Sch(index,ind2)=elem.Value
Sheet1.Cells(index + 4, ind2 + 5) = elem.Value
Next ind2
Next index
Else
Call_Sch(index,ind2)=field.Value
Sheet1.Cells(index + 4, ind2 + 5).Value = field.Value
End If
Loop
End If
End If
End Sub
The variable i am trying to get, specifically, is the Call_Sch. I want a function in the main module to recognize the variable. Thanks again.
It isn't necessary to declare a variable before using ReDim on it; ReDim can declare a variable. However, if you added:
Public Call_Sch() as Variant ' Insert correct data type here
then you would be able to refer to it via:
<YourClassVaraibleName>.Call_Sch

Resources