VBA how to use a dictionary - vba

I am getting issues in using a dictionary in VBA. I want to add values from a sheet to a dictionary. If I use simple lists, there is no error in the code. Like this.
Function Account(Place As String) As String
Dim cities(500)
Dim accounts(500)
For i = 2 To 500
cities(i) = Worksheets("Sheet2").Cells(i, 2).Value
accounts(i) = Worksheets("Sheet2").Cells(i, 3).Value
Next i
placeName = StrConv(Place, vbProperCase)
Account = placeName
End Function
This code does not give an issue but if I add the code for the dictionary, there is some issue.
Function Account(Place As String) As String
Dim cities(500)
Dim accounts(500)
Dim dict
Set dict = CreateObject(Scripting.Dictionary)
For i = 2 To 500
cities(i) = Worksheets("Sheet2").Cells(i, 2).Value
accounts(i) = Worksheets("Sheet2").Cells(i, 3).Value
dict(cities(i)) = accounts(i)
Next i
placeName = StrConv(Place, vbProperCase)
Account = placeName
dict = Nothing
End Function
Can someone point out the error. I am new to vba so I dont know much about it.

The folowing UDF loads a dictionary object with places as keys (unique) and associated accounts as items. After the dictionary has been loaded, it looks up the Place parameter passed into the function and returns the account if found.
Option Explicit
Function Account(Place As String) As String
Static d As Long, dict As Object
If dict Is Nothing Then
Set dict = CreateObject("Scripting.Dictionary")
dict.comparemode = vbTextCompare
Else
dict.RemoveAll
End If
With Worksheets("Sheet2")
For d = 2 To .Cells(.Rows.Count, "B").End(xlUp).Row
dict.Item(.Cells(d, "B").Value2) = .Cells(d, "C").Value2
Next d
End With
If dict.exists(Place) Then
Account = dict.Item(Place)
Else
Account = "not found"
End If
End Function
Note that beyond other corrections, the code to instantiate the dictionary object is CreateObject("Scripting.Dictionary") not CreateObject(Scripting.Dictionary).

One possible area of concern, brought to mind by one of your comments, lies in the use of "Sheet1" and "Sheet2". In Excel VBA, there are two different ways to refer to a worksheet. The is the Name of the worksheet, which is what the user sees on the tabs in Excel, and the user can change at will. Thtese default to names like "Sheet1", "Sheet2", etc.
There is also the "Codename" for each worksheet. In the Visual Basic Editor, the project explorer window will list all the worksheets under "Microsoft Excel Objects". There you'll see the Codename for each worksheet, with the Name of the worksheet in parentheses.
When you use Worksheets("Sheet1"), the "Sheet1" refers to the Name, not the Codename. It's possible to end up with a worksheet with the Name "Sheet1" and the codename "Sheet2".
As far as your functions are concerned, I note that in both cases you declare local variables -- the arrays 'cities' and 'accounts' in the first, and those two plus the dictionary 'dict' in the second. You have code to fill those local variables, but then do nothing with them. The return value of the function is not dependent on any of those local variables.
Once the function code completes, those local variables lose their values. VBA returns the memory it used to store those variables to its pool of available memory, to be reused for other purposes.
Try commenting-out the entire for...next loop, and you'll see that the value return from the function is unchanged.
I'm not certain what you intend to accomplish in these functions. It would be helpful for you to explain that.

Related

VBA - remove duplicates (Case NOT sensitive) separated by a comma inside cells in excel? [duplicate]

I have set Dictionary as an object an added several items to that dictionary, however it seems to be case-sensitive. Is there anyway I can set the dictionary to recognize different versions?
My Code:
Sub Test()
Dim sheet1 As String
Dim Dict As Object
Dim c As Range
Sheet1= "TEST"
Set Dict = CreateObject("Scripting.Dictionary")
Dict.Add "MIKE", 0
Dict.Add "PHIL", 0
Dict.Add "Joe", 0
For Each c In ActiveWorkbook.Worksheets(Sheet1).UsedRange
If Dict.Exists(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) Then
Dict(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) = Dict(ActiveWorkbook.Worksheets(Sheet1).Cells(c.Row, c.Column).Value) + 1
End If
Next
Sheet1.Cells(25, 3) = Dict("MIKE")
Sheet1.Cells(25, 3) = Dict("PHIL")
Sheet1.Cells(25, 3) = Dict("Joe")
Set Dict = Nothing
End Sub
So I want to recognize "mike" for MIKE and "Phil" for PHIL etc.
Thanks in advance!
Adding onto #Ralph
dict.CompareMode = TextCompare
is what I changed the file to.
Some clarifications regarding the comments:
TextCompare is only available with Early Binding, it is a member of Scripting.
vbTextCompare is always available in VBA.
Both are = 1.
? Scripting.CompareMethod.TextCompare
1
? VBA.VbCompareMethod.vbTextCompare
1
Note: you can only set dict.CompareMode if dict is empty, i.e. you haven't added any members yet. Otherwise you will get an "Illegal procedure call" error.
I always like to set things straight for all of my coding. So, all modules and code lying on my sheets or in forms start with the following three lines before writing any additional code.
Option Base 0
Option Explicit
Option Compare Text
If I want to have something handled differently in a particular Sub for some reason, then I do so in this particular sub only and do as proposed in the comment above (example):
dict.CompareMode = BinaryCompare 'if I need a case-sensitive compare in this sub
Since VBE knows that dict is a Dictionary it can provide propositions for auto-complete. This is only possible with early-binding. With late binding VBE will not provide any auto-complete propositions.

Searching and Returning bold values in VBA

I know that this probably isn't the most ideal way to to do this but just bear with me.
I have a document with a few tables on it. I'm using a userform to search the tables/sub-categories and return the relevant values. I want to select the sub categories with a range of option buttons on a userform, these will in turn set the range for the search function to look within. I also want to dynamically update the option buttons if a new table was to be added or anything along those lines.
The only thing that differentiates the title of a sub-category/table, and the items within it, is that the title of a sub-category/table is bold. So what I'm looking to do is search the first column of the spreadsheet and return the names of any entries in bold. These values are then used to set the names of the option buttons :).
The following function is my attempt at finding the text entities in column a that are in bold, returning them and setting each to an individual variable to be used in another function. The bold1 .... variables are all globally defined variables as I need them in another sub, as is the page variable which contains the relevant page to be used. Currently the code returns an error stating "variable or with block not set" and using the debugger I can see that bold1 .... and all the other boldx variables have no value set. Does anybody know whats going on/how to fix this function.
Thanks in advance :)
Sub SelectBold()
Dim Bcell As Range
For Each Bcell In Worksheets(Page).Range("A1:A500")
If Bcell.Font.Bold = True Then
Set bold1 = Bcell
End If
Next
End Sub
EDIT: I simplified the above function, to remove clutter and help narrow in on the issue. I want the above function to store the contents of the found cell (any cell in the document in bold at this stage) in the variable bold1
This will return an array of values from bold cells in column A of Page.
You can fill a combo or list box with theses values using their list property.
ComboBox1.List = getSubCategories("Sheet1")
Function getSubCategories(Page As String) As String()
Dim arrSubCategories() As String
Dim count As Long
Dim c As Range
With Worksheets(Page)
For Each c In .Range("A2", .Range("A" & Rows.count).End(xlUp))
If c.Font.Bold Then
ReDim Preserve arrSubCategories(count)
arrSubCategories(count) = c.Value
count = count + 1
End If
Next
End With
getSubCategories = arrSubCategories
End Function
you may find useful to have a Range returned with subcategories cells found:
Function SelectBold(Page As String, colIndex As String) As Range
With Worksheets(Page)
With .Range(colIndex & "1", .Cells(.Rows.Count, colIndex).End(xlUp)).Offset(, .UsedRange.Columns.Count)
.FormulaR1C1 = "=if(isbold(RC[-1]),"""",1)"
.Value = .Value
If WorksheetFunction.CountA(.Cells) < .Rows.Count Then Set SelectBold = Intersect(.SpecialCells(xlCellTypeBlanks).EntireRow, .Parent.Columns(1))
.Clear
End With
End With
End Function
Function IsBold(rCell As Range)
IsBold = rCell.Font.Bold
End Function
to be possibly exploited as follows:
Option Explicit
Sub main()
Dim subCategoriesRng As Range, cell As Range
Set subCategoriesRng = SelectBold(Worksheets("bolds").Name, "A") '<--| pass worksheet name and column to search in
If Not subCategoriesRng Is Nothing Then
For Each cell In subCategoriesRng '<--| loop through subcategories cells
'... code
Next cell
End If
End Sub

Building a Vlookup between separate workbooks

I am new to VBA coding and am attempting to build a Vlookup to connect two seperate workbooks. Provided belwo is my coding which is currently producing a Run-Time 1004 Method 'Range' of Object'_Global' Failed error on the vlookup line.
Sub dataEntry(agent As Integer, month As Integer)
Dim lookupReturn As Integer
Dim i As Integer
Dim lookupValue As String
Dim lookupBook As String
i = 1
'set excel book to preform vlookup within
lookupBook = sheetName & "-Daily Report Daily-Monthly Grid.xlsx"
'Preforms a Vlookup to fill in data points
Do While Workbooks("Cumulative Agent Ranking Template").Sheets(sheetName).Cells(i, 1).Value <> ""
lookupValue = Workbooks("Cumulative Agent Ranking Template").Sheets(sheetName).Cells(i, 10).Value
lookupReturn = Sheets(sheetName).WorksheetFunction.VLookup(Range("C2"), [lookupBook] & sheet33 & Range("!$A$2" & ":$C" & agent), 2, False)
Workbooks("Cumulative Agent Ranking Template").Sheets(sheetName).Cells(i, 11).Value = lookupReturn
i = i + 1
lookupValue = ""
lookupReturn = 0
Loop
I think there are probably a lot of things going wrong here.
Let's start with the most obvious source of 1004 error, which is unqualified range objects.
Example: in a standard module, Range("C2") always refers to the Active sheet. In a sheet module, it always refers to the parent sheet.
To Resolve: fully qualify your range variables, e.g., Workbooks(_name_).Worksheets(_sheetname_).Range("C2")
Another potential problem I notice is the way you're calling the Vlookup.
Sheets(sheetName).WorksheetFunction...
There is no such method WorksheetFunction of a worksheet object. This is an application-level method, so invoke it like:
WorksheetFunction.Vlookup...
Additional problems or potential problems
Undeclared variables: sheetname (maybe this is a public or module-level variable?)
Unused variables: lookupValue You've declared this, and you've assigned to it, but you haven't done anything to it or used it anywhere else in the code. is this the value you're trying to search for?
The rest of your formula is also pretty gnarly and I'm not even going to try and fix it in its current state. If you use better-defined object variables, your code will be easier to read and understand. You will also benefit from intellisense if the variables are strongly typed in declaration.
Here is an example, which I will leave up to you to work out for your own needs:
Dim lookupBook as Workbook
Dim lookupRange as Range
Dim lookupValue as String
Set lookupBook = Workbooks("some other file.xlsx")
Set lookupRange = lookupBook.Worksheets("some worksheet").Range("A1:B50")
lookupValue = "cat"
Range("A1").Value = WorksheetFunction.Vlookup(lookupValue, lookupRange, 2, False)

Subscript Out of Range, even though value is defined

I am writing a piece of code that transfers selected data on an Excel sheet into an array, which is then used to print the data on a new spreadsheet. However, I am getting a "Subscript Out of Range" error, even though a value appears when I scroll over selectArr(i - 1). Here is my code:
Sub Marascuilo()
Dim numRows As Integer 'Number of rows selected
numRows = Selection.Rows.Count
Dim selectArr() As Double 'Array containing numbers from selected cells
selectArr = loadArr(numRows) 'Load values into array
For i = 2 To UBound(selectArr) - LBound(selectArr) + 2
Sheets("Sheet 4").Cells(i, 2).Value = selectArr(i - 1)
Next
End Sub
'This function loads the values from the selected cells into selectArr.
Function loadArr(numRows) As Double()
Dim ResultArray() As Double
r = 1
For Each v In Selection
ReDim Preserve ResultArray(1 To r)
If v <> "" Then
ResultArray(r) = v.Value
r = r + 1
End If
Next
loadArr = ResultArray
End Function
Any ideas as to how I fix this issue?
Thanks!
Jay
Instead of using Sheets("Sheet 4"), you might consider using the sheet's CodeName. If you look in the Project Explorer window, every sheet has a Name and a CodeName. It might look like this
Sheet1 (Sheet1)
Sheet2 (Sheet2)
The first one is the CodeName (can't be changed from the UI). The one in parens is the tab name. Select the sheet in the Project Explorer and press F4 to open the Properties dialog. Go to the (Name) property (a poorly named property) and change it to something meaningful. I change all my sheets' CodeNames and use a wsh prefix. My sheet that's a log has a CodeName of
wshLog
Now I can use wshLog in my code and I get some benefits. The first is that if someone renames the sheet in the UI, the code still works. The second is I can type wshlog (all lower case) and the VBE will change it to wshLog and I get that visual cue that I spelled it right. Finally, my code is more readable, ex wshFinalReport vs. Sheets("Sheet1").

Can I use VBA function to return a (dynamic) list of acceptable values into Excel's data validation?

For a given cell, I select Data/Validation and set Allow to "List". I now wish to set Source like so:
=rNames(REGS)
but that does not work (name not found). So I go Insert/Name/Define and create "REGNAMES" by simply assigning the formula above (no cell range). I then return to the Data/Validation and when I set Source like so:
=REGNAMES
Now I get "Source currently evaluates to error". Unfortunately, this error does not go away even after I ignore it. I can create a range formula in the sheet like so:
{=REGNAMES}
and drag this to the right across a couple cells and the rNames function faithfully returns
Option #1 | Options #2 | ...
That is, the function returns a range as intended.
I know that I can use macro code to manipulate the List setting for that cell out of VBA. I don't like these side-effects much. I would prefer a clean dependency tree built on functions. Any ideas how to get the Data/Validation to accept the array values returned from rNames?
Thanks.
PS: rNames returns the result range as a Variant, if that has any bearing.
I think the problem is that data validation dialog only accepts the following "lists":
an actual list of things entered directly into the Source field
a literal range reference (like $Q$42:$Q$50)
a named formula that itself resolves to a range reference
That last one is key - there is no way to have a VBA function just return an array that can be used for validation, even if you call it from a named formula.
You can write a VBA function that returns a range reference, though, and call that from a named formula. This can be useful as part of the following technique that approximates the ability to do what you actually want.
First, have an actual range somewhere that calls your arbitrary-array-returning VBA UDF. Say you had this function:
Public Function validationList(someArg, someOtherArg)
'Pretend this got calculated somehow based on the above args...
validationList = Array("a", "b", "c")
End Function
And you called it from $Q$42:$Q$50 as an array formula. You'd get three cells with "a", "b", and "c" in them, and the rest of the cells would have #N/A errors because the returned array was smaller than the range that called the UDF. So far so good.
Now, have another VBA UDF that returns just the "occupied" part of a range, ignoring the #N/A error cells:
Public Function extractSeq(rng As Range)
'On Error GoTo EH stuff omitted...
'Also omitting validation - is range only one row or column, etc.
Dim posLast As Long
For posLast = rng.Count To 1 Step -1
If Not IsError(rng(posLast)) Then
Exit For
End If
If rng(posLast) <> CVErr(xlErrNA) Then
Exit For
End If
Next posLast
If posLast < 1 Then
extractSeq = CVErr(xlErrRef)
Else
Set extractSeq = Range(rng(1), rng(posLast))
End If
End Function
You can then call this from a named formula like so:
=extractSeq($Q$42:$Q$50)
and the named formula will return a range reference that Excel will accept an allowable validation list. Clunky, but side-effect free!
Note the use of the keyword 'Set' in the above code. It's not clear from your question, but this might be the only part of this whole answer that matters to you. If you don't use 'Set' when trying to return a range reference, VBA will instead return the value of the range, which can't be used as a validation list.
I was just doing some research on accessing the contents of a Shapes dropdown control, and discovered another approach to solving this problem that you might find helpful.
Any range that can have a validation rule applied can have that rule applied programmatically. Thus, if you want to apply a rule to cell A1, you can do this:
ActiveSheet.Range("A1").Validation.Add xlValidateList, , , "use, this, list"
The above adds an in-cell dropdown validation that contains the items "use," "this," and "list." If you override the Worksheet_SelectionChange() event, and check for specific ranges within it, you can call any number of routines to create/delete validation rules. The beauty of this method is that the list referred to can be any list that can be created in VBA. I needed a dynamically-generated list of an ever-changing subset of the worksheets in a workbook, which I then concatenated together to create the validation list.
In the Worksheet_SelectionChange() event, I check for the range and then if it matches, fire the validation rule sub, thus:
Private Sub Worksheet_SelectionChange(ByVal Target as Range)
If Target.Address = "$A$1" Then
UpdateValidation
End If
End Sub
The validation list-builder code in UpdateValidation() does this:
Public Sub UpdateValidation()
Dim sList as String
Dim oSheet as Worksheet
For Each oSheet in Worksheets
sList = sList & oSheet.Name & ","
Next
sList = left(sList, len(sList) -1) ' Trim off the trailing comma
ActiveSheet.Range("A1").Validation.Delete
ActiveSheet.Range("A1").Validation.Add xlValidateList, , , sList
End Sub
And now, when the user clicks the dropdown arrow, he/she will be presented with the updated validation list.
Sounds like your rNames function is probably returning a 1-dimensional array (which will be treated as a row).
Try making your function return a column as a 1-based 2-dimensional array (Ansa(1,1) then Ansa(2,1) etc)
Couln't you rather use dynamic range names ? That's quite easy and does not require any vba.
For the future:
Following is then used in a named range and the named range set as the 'Data Validation' 'List' value
Function uniqueList(R_NonUnique As Range) As Variant
Dim R_TempList As Range
Dim V_Iterator As Variant
Dim C_UniqueItems As New Collection
On Error Resume Next
For Each V_Iterator In R_NonUnique
C_UniqueItems.Add "'" & V_Iterator.Parent.Name & "'!" & V_Iterator.Address, CStr(V_Iterator.Value2)
Next V_Iterator
On Error GoTo 0
For Each V_Iterator In C_UniqueItems
If R_TempList Is Nothing Then
Set R_TempList = Range(V_Iterator)
End If
Set R_TempList = Union(R_TempList, Range(V_Iterator))
Next V_Iterator
Set uniqueList = R_TempList
End Function
#user5149293 I higly appreciate your code, but I recommend to prevent the collection from throwing an error, when adding duplicate values. The usage of a custom formula in the data validation list or in Name-Manager-Formula prevents the code from using the vbe debugger, which makes it very hard to trace back errors here (I ran into this problem myself, when using your code).
I recommend to check the existence of key in the collection with a separate function:
Function uniqueList(R_NonUnique As Range) As Variant
'Returns unique list as Array
Dim R_TempList As Range
Dim V_Iterator As Variant
Dim C_UniqueItems As New Collection
For Each V_Iterator In R_NonUnique
'Check if key already exists in the Collection
If Not HasKey(C_UniqueItems, V_Iterator.Value2) Then
C_UniqueItems.Add Item:="'" & V_Iterator.Parent.Name & "'!" & V_Iterator.Address, Key:=CStr(V_Iterator.Value2)
End If
Next V_Iterator
For Each V_Iterator In C_UniqueItems
If R_TempList Is Nothing Then
Set R_TempList = Range(V_Iterator)
End If
Set R_TempList = Union(R_TempList, Range(V_Iterator))
Next V_Iterator
Set uniqueList = R_TempList
End Function
Function HasKey(coll As Collection, strKey As String) As Boolean
'https://stackoverflow.com/questions/38007844/generic-way-to-check-if-a-key-is-in-a-collection-in-excel-vba
Dim var As Variant
On Error Resume Next
var = coll(strKey)
HasKey = (Err.Number = 0)
Err.Clear
End Function