VBA Dictionary with Dynamic Arrays - arrays

I'm trying to create A dynamic dictionary that contains dynamic arrays.
Sample Row from spreadsheet:
Facility Name|Contact Name|Contact Role
The relationship between facilities and contacts are M2M. I would like to recreate a sheet that looks like this:
Contact Name| Facility1 - role, Facility2 - role
What I would like to do is create a dictionary of names with unique names serving as keys
New Dictionary Names(name)
The values for Names(name) will be an array of all the row numbers where this name appears. For instance, say "Joe Rose" appears in rows 3, 7 and 9:
names("Joe Rose") = [3,7,9]
I know how I could do this in JS, Python, PHP, but VBA is driving me crazy!
Here is what I kind of got so far:
Dim names As Dictionary
Set names = New Dictionary
Dim name
For i=1 To WorkSheets("Sheet1").Rows.Count
name = WorkSheets("Sheet1").Cells(i,2)
If Not names(name) Then
names(name) = i
Else
'help!
'names(name)) push new i, maybe something with redim preserve?
End If
Next i
Even just pointing me to some article that I could reference would be great! VBA has been so frustrating coming from a PHP background!
Thank you

It's a bit tricky since you have to pull the array out of the Dictionary to work with it, then put it back:
Sub Tester()
Dim names As Dictionary
Set names = New Dictionary
Dim name, tmp, ub, i, k
For i = 1 To Worksheets("Sheet1").UsedRange.Rows.Count
name = Trim(Worksheets("Sheet1").Cells(i, 2).Value)
If Len(name) > 0 Then
If Not names.Exists(name) Then
names(name) = Array(i)
Else
tmp = names(name)
ub = UBound(tmp) + 1
ReDim Preserve tmp(0 To ub)
tmp(ub) = i
names(name) = tmp
End If
End If
Next i
For Each k In names.Keys
Debug.Print k, Join(names(k), ",")
Next k
End Sub

Let's do this. First build the dictionary's Value as a comma-delimited string. Then, if you need/want, you can use the SPLIT function to convert that to an array.
Dim names As Dictionary
Set names = New Dictionary
Dim name
For i = 1 To WorkSheets("Sheet1").Rows.Count
name = WorkSheets("Sheet1").Cells(i,2)
If names.Exists(name) Then
names(name) = names(name) & "," & i
Else
names(name) = i
Next
Next i
For each name in names
names(name) = Split(name, ",")
Next

Try to avoid using [worksheet].rows.count when looping, its value is more than 1 million for excel 2010.
Public Sub test()
Dim names As Dictionary
Dim name
Dim cell As Object
'finds last row in column 2
lastRow = Worksheets("Sheet1").Cells(Rows.Count, 2).End(xlUp).Row
Set names = New Dictionary
For Row = 1 To lastRow
Set cell = Worksheets("Sheet1").Cells(Row, 2)
name = Split(cell.Text, "|")(0)
If names.Exists(name) Then
names(name) = names(name) & ", " & Row
Else
names.Add name, Row
End If
Next Row
End Sub

Related

VBA stop using temporary ranges

I'm new to vba so I need some help making my macro more efficient. It does return the desired outcome however I know there must be a much quicker way to do so I just do not have the vba experience to know how.
I have a column which contains names of people assigned to a project. Some are only one name, and others may be multiple, for example:
At the moment, my code goes through this column, separates the names by comma, and enters them individually into a new range like so:
I then use a collection for the unique names and enter them in the final desired list. The names must show up three times, blank row, next three rows are the next name, so on.It should look like this in the end:
Currently my code is the following
Sub FindUniques()
Dim Ws As Worksheet, Ns As Worksheet
Dim SubString() As String, m As Integer, k As Long, NameCount As Integer
Dim allNames As New Collection, tempRng As Range
Set Ns = Worksheets("Sheet2")
Set Ws = Worksheets("Sheet1")
'Loops through the Assigned To column, separates and finds unique names
On Error Resume Next
For i = 1 To Ws.Range("A:A").End(xlDown).Row - Range("Assigned_to").Row
SubString = Split(Range("Assigned_to").Offset(i), ", ")
For j = 0 To UBound(SubString)
allNames.Add (allNames.count), SubString(j)
Next j
Next i
On Error GoTo 0
NameCount = allNames.count
For k = 1 To NameCount
For m = 1 To 4
Ns.Cells((k - 1) * 4 + m + 7, 2) = allNames.Key(k)
Next
Range("Names").Offset((k - 1) * 4).ClearContents
Next
End Sub
It works, however there must be some way that is more efficient than entering the names into a new range and then deleting the range. How can I use a collection or an array or something of the sort to make it quicker? Any ideas would be really appreciated
edit: I have now updated the code and it is using an collection, taking values from the substring. This enters the item (0, 1, 2, ...) in the cells instead of the keys (keys here are the names). How do I get it to return the key instead of the item number?
The slowest part of VBA are worksheet interactions so we should attempt to minimize that as much as possible.
Sub FindUniques()
Dim ws As Worksheet, ns As Worksheet
Dim splitStr() As String, nameStr As Variant
Dim dict As New Dictionary
Dim lastRow As Long, i As Long
Set ns = Worksheets("Sheet2")
Set ws = Worksheets("Sheet1")
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
'Loops through the Assigned To column, separates and finds unique names
For i = 2 To lastRow
splitStr = Split(CStr(ws.Cells(i, 1).Value), ", ")
For Each nameStr In splitStr
If Not dict.Exists(nameStr) Then dict.Add nameStr , 0
Next
Next i
i = 2
For Each nameStr In dict.Keys
ns.Cells(i, 1).Resize(3).Value = nameStr
i = i + 4
Next
End Sub
Edited With #Toddleson & #BigBen 's suggestions
Good Luck!

Efficient way to group objects in an Array

I have an Array of user defined Objects. Lets say the classy, dogs.
Now I need a way to only look at all brown dogs with spots (both attributes of the Object).
I could create a new array "brown dogs" go through the original array take each dog that has brown fur and pack him in the new array. Then do another array "brown, spotted dogs" and so on you get the idea...
Works but isnt really great.
But I actually need to look at all dogs with each possible color, so all brown dogs in a group, all white dogs etc. And then create further sub groups based on further attributes.
It would be easy in an excel sheet, just filter for brown, and the other attribute, then ran the macro you want, then (by hand) filter to another color, ran code again...
I have a construct in mind that could do that in theory (take value of first attribute, create "already done colors" array, put it in their, go through whole original array, find all with the same color and then do this over and over again for all subsequent attributes) But this would be just very complexe, and hard to maintain. Isnt their an easier and quicker option to only a specific subset of values of the array (brown dogs) and repeat it for all permutations of that attribute?
Maybe this picture helps to illustrate the idea:
The basic challange is: find if the the combination of b-z (in column 2 and 3) exists for each group of column 1.
A human sees quiet quickly that for group "1" (blue) in column 1 there are 2 cases of b-z (Rows 5 and 7) but none for group "2" (green).
A programm would need to find all instances where column one is of the first value ("2") and then find all other rows with this value (rows 2,5 and 6) then for this list check column 2 and 3.
Go to the next row and check if attribute 1 was already used (it wasnt, its "1" in line two and was "2" in line one) and then compare again.
This may be doable for the given example but gets burdensome when you have several stages of grouping. So first group for Attribute one, then for ( a hyphotetical) attribute 4 and attribute 5 and then for this sub group check columns 2 an d 3 values.
Iam now looking for a way to do this "grouping" in a more managable way, is there any option for this?
Code example for what I think is a not so great version for grouping/filtering this:
dim ArrayofObjectInstances() as SpecificObject
dim ArrAlreadyUsedIds() as integer
dim ArrayOfObjectsWithSameID() as SpecificObject
For each element in ArrayofObjectInstances
If not IsinArray(elemnt.ID, ArrAlreadyUsedIds) then
For i = 1 to LenghtOfArray
if ArrayofObjectInstances(i).ID Like element.ID then
'In here I would start the for each and the for loop again for a second Attribute I want to group by, and then again for a third, each time needing more arrays
'fill the "ArrayOfObjectsWithSameID" with the current element (and the first one), then lopp through this array and check 2 Attributes if they are spevific values,
'just to then be able to say, yes the grouping for this ID has at least one instance where the reuirment is meet
end if
next
end if
next element
One way to go:
Test class clsTest:
Option Explicit
Public Id As Long
Public Color As String
Public Weight As String
Public Attitude As String
'added this for Debug output
Public Property Get AsString()
AsString = Join(Array(Me.Id, Me.Color, Me.Weight, Me.Attitude), "~")
End Property
Test code:
Option Explicit
Sub Tester()
Dim i As Long, obj As clsTest, col As New Collection
'create a collection of test objects with randomized property values
For i = 1 To 10
Set obj = New clsTest
obj.Id = i
obj.Color = Array("Blue", "Brown", "Yellow") _
(Application.RandBetween(0, 2))
obj.Weight = Array("Heavy", "Light")(Application.RandBetween(0, 1))
obj.Attitude = Array("Good", "Bad")(Application.RandBetween(0, 1))
col.Add obj
Next i
DumpNestedDict Classify(col, Array("Color"))
DumpNestedDict Classify(col, Array("Color", "Weight"))
DumpNestedDict Classify(col, Array("Weight", "Color", "Attitude"))
End Sub
'Classify a collection of objects according to an array of property names
'Returns a scripting dictionary (nested if >1 property) with objects
' contained in one or more collections
Function Classify(col, arrProps)
Dim dict As Object, pv, i As Long, curr As Object, obj As Object
Set dict = CreateObject("scripting.dictionary")
For Each obj In col
Set curr = dict 'start at the top level...
For i = LBound(arrProps) To UBound(arrProps)
pv = CallByName(obj, arrProps(i), VbGet) 'get the property value
If Not curr.exists(pv) Then
If i < UBound(arrProps) Then 'at the last property?
'not at last property, so create a nested dictionary
curr.Add pv, CreateObject("scripting.dictionary")
Else
'end of the road is a collection of objects
curr.Add pv, New Collection 'end of the road is a collection of objects
End If
End If
If i < UBound(arrProps) Then Set curr = curr(pv) 'one level down in the nesting
'last property, so add the object itself
If i = UBound(arrProps) Then
curr(pv).Add obj
End If
Next i
Next obj
Set Classify = dict
End Function
'create a text output from a nested dictionary containing collections of objects
' object must have an "AsString" property
Sub DumpNestedDict(d As Object, Optional level As Long = 0)
Dim k, v, s, obj
s = String(level * 3, " ")
For Each k In d.Keys
Debug.Print s & "Key:" & k
If TypeName(d(k)) = "Dictionary" Then
DumpNestedDict d(k), level + 1
ElseIf TypeName(d(k)) = "Collection" Then
For Each obj In d(k)
Debug.Print String((level + 1) * 3, " ") & obj.AsString
Next obj
End If
Next k
If level = 0 Then Debug.Print "---------------------------", vbLf
End Sub
Example output:
Key:Yellow
1~Yellow~Light~Good
3~Yellow~Light~Bad
4~Yellow~Heavy~Good
6~Yellow~Light~Bad
10~Yellow~Heavy~Good
Key:Brown
2~Brown~Light~Bad
7~Brown~Heavy~Bad
8~Brown~Heavy~Bad
Key:Blue
5~Blue~Heavy~Bad
9~Blue~Light~Good
---------------------------
Key:Yellow
Key:Light
1~Yellow~Light~Good
3~Yellow~Light~Bad
6~Yellow~Light~Bad
Key:Heavy
4~Yellow~Heavy~Good
10~Yellow~Heavy~Good
Key:Brown
Key:Light
2~Brown~Light~Bad
Key:Heavy
7~Brown~Heavy~Bad
8~Brown~Heavy~Bad
Key:Blue
Key:Heavy
5~Blue~Heavy~Bad
Key:Light
9~Blue~Light~Good
---------------------------
Key:Light
Key:Yellow
Key:Good
1~Yellow~Light~Good
Key:Bad
3~Yellow~Light~Bad
6~Yellow~Light~Bad
Key:Brown
Key:Bad
2~Brown~Light~Bad
Key:Blue
Key:Good
9~Blue~Light~Good
Key:Heavy
Key:Yellow
Key:Good
4~Yellow~Heavy~Good
10~Yellow~Heavy~Good
Key:Blue
Key:Bad
5~Blue~Heavy~Bad
Key:Brown
Key:Bad
7~Brown~Heavy~Bad
8~Brown~Heavy~Bad
---------------------------
Please, test the next code. I tried commenting it in a way to be understandable for somebody not familiar with dictionaries handling:
Sub testDictionariyWay()
'it needs a reference to 'Microsoft Scripting Runtime'
'The code may use late binding, but not being experienced in dictionary using, adding this reference
'you will have access to intellisense suggestions. I will also place a piece of code able to put it automatically...
Dim sh As Worksheet, arr, arrOb, dict As New Scripting.Dictionary
Dim i As Long, j As Long, boolExisting As Boolean
Set sh = ActiveSheet
arr = sh.Range("A2:C7").value 'put the range to be processed in an array (only to make the code faster and more compact)
For i = 1 To UBound(arr)
If Not dict.Exists(arr(i, 2)) Then 'if the attribute does not exist like a dictionary key:
dict.Add arr(i, 2), Array(arr(i, 1)) 'a key is created and an array of one element is placed like its value
Else
ReDim arrOb(UBound(dict(arr(i, 2))) + 1) 'redim an array able to keep all precedent array elements, plus one to be added
For j = 0 To UBound(arrOb) - 1 'iterate between the existing array elements:
If dict(arr(i, 2))(j) <> arr(i, 1) Then 'check if the object is not already in the array:
arrOb(j) = dict(arr(i, 2))(j) 'if not, it will be placed in the new array
Else
boolExisting = True: Exit For 'if the object already exists, the loop is exited and a boolean variable becomes True
End If
Next j
If Not boolExisting Then 'if not the object already exist:
arrOb(j) = arr(i, 1) 'place the new object in the last array element
dict(arr(i, 2)) = arrOb 'add the array as dictionary key value
End If
boolExisting = False 'reinitialize he boolean variable
End If
'do the same for the third column:
If Not dict.Exists(arr(i, 3)) Then
dict.Add arr(i, 3), Array(arr(i, 1))
Else
ReDim arrOb(UBound(dict(arr(i, 3))) + 1)
For j = 0 To UBound(arrOb) - 1
If dict(arr(i, 3))(j) <> arr(i, 1) Then
arrOb(j) = dict(arr(i, 3))(j)
Else
boolExisting = True: Exit For
End If
Next j
If Not boolExisting Then
arrOb(j) = arr(i, 1)
dict(arr(i, 3)) = arrOb
End If
boolExisting = False
End If
Next i
'testing the dictionary content. Now, being a strings array, Debug.Print can be used.
'Otherwise, an object will be returned and it should be Set and use one of its attributes to check the code:
Dim El As Variant
For Each El In dict("a")
Debug.Print "a: " & El
Next El
For Each El In dict("x")
Debug.Print "x: " & El
Next El
End Sub
The next piece of code will automatically add the necessary reference. Please, first run the following code, save the workbook (to keep the reference) and then run the above one:
Sub addScrRunTimeRef()
'Add a reference to 'Microsoft Scripting Runtime':
'In case of error ('Programmatic access to Visual Basic Project not trusted'):
'Options->Trust Center->Trust Center Settings->Macro Settings->Developer Macro Settings->
' check "Trust access to the VBA project object model"
Application.VBE.ActiveVBProject.References.AddFromFile "C:\Windows\SysWOW64\scrrun.dll"
End Sub
Edited to add a variant involving a class and its instances, to be more elocvent (at least, this is what I hope):
Create a class module, named 'clsFoo' and paste the next code:
Option Explicit
Private pmyName As String
Private pmyColor As String
Private pmyTag As String 'to identify the class Object instance (string) name...
Public Property Get myName() As String
myName = pmyName
End Property
Public Property Let myName(value As String)
pmyName = value
End Property
Public Property Get myColor() As String
myColor = pmyColor
End Property
Public Property Let myColor(value As String)
pmyColor = value
End Property
Public Property Get myTag() As String
myTag = pmyTag
End Property
Public Property Let myTag(value As String)
pmyTag = value
End Property
Try the next code showing how to create dictionaries for both class properties:
Sub testClassInstancesInDict()
Dim arrFoo(4), Foo1 As New clsFoo, Foo2 As New clsFoo, Foo3 As New clsFoo, Foo4 As New clsFoo, Foo5 As New clsFoo
Dim El As Variant, dictName As New Scripting.Dictionary, dictColor As New Scripting.Dictionary, arrClss() As clsFoo, i As Long
Foo1.myName = "Name1": Foo1.myColor = "red": Foo1.myTag = "Foo1": Set arrFoo(0) = Foo1
Foo2.myName = "Name2": Foo2.myColor = "black": Foo2.myTag = "Foo2": Set arrFoo(1) = Foo2
Foo3.myName = "Name1": Foo3.myColor = "green": Foo3.myTag = "Foo3": Set arrFoo(2) = Foo3
Foo4.myName = "Name4": Foo4.myColor = "black": Foo4.myTag = "Foo4": Set arrFoo(3) = Foo4
Foo5.myName = "Name1": Foo5.myColor = "white": Foo5.myTag = "Foo5": Set arrFoo(4) = Foo5
For Each El In arrFoo
'process dictName dictionary:
If Not dictName.Exists(El.myName) Then
dictName.Add El.myName, Array(El)
Else
ReDim arrClss(UBound(dictName(El.myName)) + 1)
For i = 0 To UBound(dictName(El.myName))
Set arrClss(i) = dictName(El.myName)(i)
Next i
Set arrClss(i) = El: dictName(El.myName) = arrClss
End If
'process dictColor dictionary:
If Not dictColor.Exists(El.myColor) Then
dictColor.Add El.myColor, Array(El)
Else
ReDim arrClss(UBound(dictColor(El.myColor)) + 1)
For i = 0 To UBound(dictColor(El.myColor))
Set arrClss(i) = dictColor(El.myColor)(i)
Next i
Set arrClss(i) = El: dictColor(El.myColor) = arrClss
End If
Next
'test the resulted dictionaries:
Debug.Print "DictName Name1 key has " & UBound(dictName("Name1")) & " clsFoo objects"
Debug.Print "DictColor black key has " & UBound(dictColor("black")) & " clsFoo objects"
Dim j As Long
'all dictName keys/items. myTag returns the object string name:
Debug.Print "dictName _________________________"
For j = 0 To dictName.count - 1
For i = 0 To UBound(dictName.items(j))
Debug.Print "KeyName: " & dictName.Keys(j) & vbTab & dictName.items(j)(i).myName & _
vbTab & dictName.items(j)(i).myColor & vbTab & dictName.items(j)(i).myTag
Next i
Next
'all dictColor keys/items:
Debug.Print: Debug.Print "dictColor ________________________"
For j = 0 To dictColor.count - 1
For i = 0 To UBound(dictColor.items(j))
Debug.Print "KeyColor: " & dictColor.Keys(j) & vbTab & dictColor.items(j)(i).myName & _
vbTab & dictColor.items(j)(i).myColor & vbTab & dictColor.items(j)(i).myTag
Next i
Next
End Sub

Visual Basic, VBA array loop

I used http://www.homeandlearn.org/arrays_and_loops.html to help me with this.
I have data connections that filter to one table. Unfortunately one of the sources randomly places incorrect data (usually a date) in the wrong column (Client column) when the program exports the file to Excel. What I'd like to do is something similar to an index/match function. I'd like to check each reservation number in this master table (A) against another table (B) within the same workbook. If the reservation number from the other sheet (B) matches the reservation number in the master table (A), I would like to have the correct Client value entered into the master table (A). I'm still pretty new to VBA so any help is appreciated. I've tried to modify my code here and there but to no avail. Also, I was originally running this as a practice without the real data so I didn't mess up my original file. I tried to add the appropriate syntax to refer to other sheets and whatnot so I suppose that could be entered incorrectly as well. Here's the closest original code I came up with:
Sub TransferData()
Dim MyArray(1 To 19) As Single
MyArray(1) = 81899
MyArray(2) = 87172
MyArray(3) = 87275
MyArray(4) = 87394
MyArray(5) = 87446
MyArray(6) = 87496
MyArray(7) = 87621
MyArray(8) = 87631
MyArray(9) = 87726
MyArray(10) = 87822
MyArray(11) = 87858
MyArray(12) = 88041
MyArray(13) = 88097
MyArray(14) = 88127
MyArray(15) = 88160
MyArray(16) = 88191
MyArray(17) = 88359
MyArray(18) = 88487
MyArray(19) = 88545
For i = 1 To 19
If Worksheets("Sheet1").Range("B" & i).Value = MyArray(i) Then
Worksheets("Sheet2").Range("P" & i).Value = _
Worksheets("Sheet1").Range("E" & i).Value
End If
Next i
End Sub
I don't recall the error because the code wasn't exactly as above but close to it. I believe the issue it was running into was that when the i variable went above 19, the system couldn't find arrays > 19. I need VBA to check 19 arrays in an ever changing number of rows that's currently at 3k+. I attempted to add another variable thinking if I kept the variables separate, I could have VBA check the 19 arrays against all the rows. Here's the code I came up with for that....
Sub TransferData()
Dim MyArray(1 To 19) As Single
MyArray(1) = 81899
MyArray(2) = 87172
MyArray(3) = 87275
MyArray(4) = 87394
MyArray(5) = 87446
MyArray(6) = 87496
MyArray(7) = 87621
MyArray(8) = 87631
MyArray(9) = 87726
MyArray(10) = 87822
MyArray(11) = 87858
MyArray(12) = 88041
MyArray(13) = 88097
MyArray(14) = 88127
MyArray(15) = 88160
MyArray(16) = 88191
MyArray(17) = 88359
MyArray(18) = 88487
MyArray(19) = 88545
For i = 1 To 5000
For j = 1 To 19
If Worksheets("Sheet1").Range("B" & i).Value = MyArray(j) Then
Worksheets("Sheet2").Range(i, 16).Value = Worksheets("Sheet1"). _
Range(i,5).Value
Next j
End If
Next i
End Sub
With this code I get compile error: Next without For. In searching online I found it might be because I have 2 "For"s, an if statement, "next" statement within the if statement, then another "next" statement outside of the loop. I was thinking it had to be done this way so that each cell in the B column gets checked against all the array possibilities.
See pictures below. I need the value of Column P (Actual Billing Name) from sheet: TMRtoSPIde to be entered into Column D (Billing Name) on sheet: RawData when the Reservation # in Column K from sheet: TMRtoSPIde matches the reservation in sheet: RawData. You'll notice the sheet: RawData has an erroneous 5 digit serial date in the Billing Name column. These are what I'm trying to replace.
Dictionaries and Collections are ideal for matching unique values. In this example I use a Scripting.Dictionary to store Unique ID's and references to the EntireRow that they are found.
Note: Range().Range() will return a reference that is relative to the first range object (e.g. Range("A10").EntireRow.Range("ZZ1").Address returns $ZZ$10).
It would have simpler to store just the needed value, I just wanted to demonstrate that you can store Objects references in a Dictionary. It is important to note that you can store Objects as both keys and/or values in a Dictionary. A common mistake people make is to try and store range references as keys dictionary.Add Cells(1,1), Cells(1,2) will store a reference to Cells(1,1) as a key and Cells(1,2) as it's value. The problem with this is that Dictionaries don't know how to compare cells and you will not be able to look up your values based on there key relationships. dictionary.Add Cells(1,1).Value, Cells(1,2) is the correct syntax.
Sub TransferData()
Dim r As Range, Source As Range
Dim d As Object
Set d = CreateObject("Scripting.Dictionary")
With Worksheets("TMRtoSPIde")
For Each r In .Range("B2", .Range("B" & .Rows.Count).End(xlUp))
If Not d.Exists(r.Value) Then d.Add r.Value, r.EntireRow
Next
End With
With Worksheets("RawData")
For Each r In .Range("B2", .Range("B" & .Rows.Count).End(xlUp))
If d.Exists(r.Value) Then
r.EntireRow.Range("K1").Value = d(r.Value).Range("P1").Value
End If
Next
End With
End Sub
Your loop should probably be like this:
For i = 1 To 5000
For j = 1 To 19
If Worksheets("Sheet1").Cells(i, "B").Value = MyArray(j) Then
Worksheets("Sheet2").Cells(i, "P").Value = Worksheets("Sheet1").Cells(i, "E").Value
'Exit from the "For j" loop if we found a match
Exit For
End If
Next j
Next i

How to delete duplicate elements from VBscript/VBA array to use in drop down box in Excel

I'm finding a lot of articles close to mine, but not quite what I'm looking for. I'm working with the IBM Personal Communications emulator to gather patient history data. There can be several pages of claims on the patient history, so the service codes used later in the program need to be gathered and saved in an array. Once duplicates are removed, the remaining codes will be stored in a drop down box.
' Copies entire current history screen
MHIScreen = objUNET.autECLPS.GetText(3, 1, 1680)
' Location of the place of service code header
POSLoc = InStr(MHIScreen, "PS SVC")
' Location of service code
ServLoc = POSLoc + 3
' Used for array index
j = 1
Row = 4
Do
Serv(j) = Mid(MHIScreen, ServLoc, 6)
Range("D" & Row).Value = Serv(j)
ServLoc = ServLoc + 320
j = j + 1
Row = Row + 1
Loop Until SMonth > EMonth
The output of this could look like this:
12345
12345
23456
12345
34567
34567
12345
98765
The desired result would be to filter through the duplicates and end up with this:
12345
23456
34567
98765
These would the be put into a drop down box for the user to choose from. I wanted to see where in the element one particular code would be located, but when I add the following line of code, I get 0 because j is already higher than the index of the last code and Serv(j) is empty:
Result = InStr(Serv(j), "34567")
Is there anyone who can guide me to a solution?
Assuming you have an array of string containing your output, the following code will produce your desired result:
Public Function TestRemoveDupsAndSort()
'all your preceding code has been removed for clarity
Do
Serv(j) = Mid(MHIScreen, ServLoc, 6)
Range("D" & Row).Value = Serv(j)
ServLoc = ServLoc + 320
j = j + 1
Row = Row + 1
Loop Until SMonth > EMonth
result = RemoveDupsAndSort(Serv)
End Function
Public Function RemoveDupsAndSort(data() As String) As String()
On Error Resume Next
Dim i As Integer
Dim j As Integer
Dim c As Collection
Dim d() As String
'sort and remove dups
Set c = New Collection
For i = LBound(data) To UBound(data)
For j = 1 To c.Count
If data(i) < c(j) Then
c.Add data(i), data(i), j
End If
Next
If j - 1 = c.Count Then c.Add data(i), data(i)
Next
'convert from a collection back to an array
ReDim d(0 To c.Count - 1)
For i = 0 To c.Count - 1
d(i) = c(i + 1)
Next
RemoveDupsAndSort= d
End Function
If I understand correctly, your code has to do the following:
Generate a collection with an arbitrary number of elements, from a string
Store the elements in the cells of an Excel worksheet
Get another collection with only the unique elements
1. Generate a collection with an arbitrary number of elements
The most basic technique is to use Redim Preserve to continuously resize the array:
Dim arr(), j
j = 0
Do
Redim Preserve arr(j)
arr(j) = Mid(MHIScreen, ServLoc, 6)
ServLoc = ServLoc + 320
j = j + 1
Loop Until SMonth > EMonth
However, if you are using VBA, then a Collection object is the natural choice here, because you don't have to worry about extending the size of the array:
Dim col As New Collection
Do
col.Add Mid(MHIScreen, ServLoc, 6)
ServLoc = ServLoc + 320
Loop Until SMonth > EMonth
If you are using VBScript, then I would suggest using a .NET ArrayList in the same way (besides its many other benefits):
Dim al
Set al = CreateObject("System.Collections.ArrayList")
Do
al.Add Mid(MHIScreen, ServLoc, 6)
ServLoc = ServLoc + 320
Loop Until SMonth > EMonth
NB. In your comment, you mentioned sorting the array. One of the benefits of ArrayList over Collection is that it has built-in sorting, via the Sort method. If sorting the values is also a goal, I would use an ArrayList even in VBA.
2. Store the elements in the cells of an Excel worksheet
If you're using an array, you can simply set the Value property of an appropriately sized range. For an array:
'app is a variable referring to the Excel Application instance
Dim rng
Set rng = app.Workbooks("MyWorkbook").Worksheets("MyWorksheet").Range("D4").Resize(UBound(arr) + 1, 1)
rng.Value = xlApp.WorksheetFunction.Transpose(arr)
For a collection or an ArrayList, you have to iterate and write the values by hand. A collection's first index is 1:
Dim rng As Range, i As Integer
Set rng = ActiveSheet.Range("A1")
For i = 1 To col.Count
rng.Value = col.Item(i)
Set rng = rng.Offset(1)
Next
while an ArrayList's first index is 0:
Dim rng, i
Set rng = Application.Workbooks("MyWorkbook").Worksheets("MyWorksheet").Range("D4")
For i = 0 To al.Count -1
rng.Value = al.Item(i)
Set rng = rng.Offset(1)
Next
3. Get another collection with only the unique elements
You can use a Scripting.Dictionary for this purpose:
Dim dict, x
Set dict = CreateObject("Scripting.Dictionary")
For Each x In arr 'can be used equally well with a Collection or an ArrayList
dict(x) = 1 '1 is a dummy value
Next
'prints the keys of the dictionary, which are unique
For Each x In dict.Keys
Debug.Print x
Next
The code in your answer can be simplified as follows:
As long as you use the default Item property, there is no need to check if the key already exists in the dictionary. Only if you use the Add method will you have a problem when adding an existing key.
You can iterate over the keys in the dictionary directly; you don't need a second array:
Like so:
Dim objDictionary, strItem
Set objDictionary = CreateObject("Scripting.Dictionary")
For Each strItem In Serv
objDictionary(strItem) = 1
Next
For Each strItem In objDictionary.Keys
Sheet1.RHICodes.AddItem strItem
Next
Thanks to those who reached out to help me. With their combined efforts, and a little extra research on what they were trying to tell me, I have came up with a solution that works.
Dim objDictionary, strItem, intItems, p, strKey, CodeList
Set objDictionary = CreateObject("Scripting.Dictionary")
For Each strItem In Serv
If Not objDictionary.Exists(strItem) Then
objDictionary.Add strItem, strItem
End If
Next
intItems = objDictionary.Count - 1
ReDim arrItems(intItems)
p = 0
For Each strKey In objDictionary.Keys
arrItems(p) = strKey
p = p + 1
Next
For Each strItem In arrItems
With Sheet1.RHICodes
.AddItem strItem
End With
Next
This now takes all of the service codes that I collected from IBM PCOMM, enters them into an array, uses the Scripting.Dictionary to sort them, then create another array with only the options I want with no duplicates and enters them into a drop down box.

Building and comparing arrays

I have the below code I'm trying to get to work. This is my first time dealing with arrays in VBA. Here's the plain english version of what I'm going for:
Load SSBarray with column A from worksheet SSB.
Load EDMarray with Column I from worksheet EDM.
Compare the above arrays and sort into two new arrays IDarray and noIDarray based on a possible match.
Output the new arrays into their respective worksheets.
Step 4 is temporary to just make sure the code is even working. The entire project is compiling all the data from 3 sheets into these two lists. Worksheet 1 has Data point A only, Worksheet 2 may or may not have Data point A, B, and/or C, and Worksheet 3 may or may not have Data point A, B, and/or C. The code I have is my start to check for all of the data point A's in worksheet 1 are in worksheet 2. Run time is also a factor. I'll take any and all the help I can get at this point. Thanks.
'Build Arrays
Dim i As Long, j As Long
Dim SSBarray
Dim EDMarray
Dim IDarray
Dim noIDarray
Dim YCounter As Long
Dim NCounter As Long
Dim inArray As Boolean
endSSB = SSB.Range("A" & Rows.Count).End(xlUp).Row
endEDM = EDM.Range("A" & Rows.Count).End(xlUp).Row
BBlast = BB.Range("A" & BB.Range("A" & Rows.Count).End(xlUp).Row)
ReDim SSBarray(1 To endSSB)
ReDim EDMarray(1 To endEDM)
For i = 2 To endSSB
SSBarray(i) = SSB.Cells(i, 1).Value2
Next i
For i = 2 To endEDM
EDMarray = EDM.Cells(i, 9).Value2
Next i
For i = 2 To endSSB
inArray = False
For j = 2 To endEDM
If SSBarray(i) = EDMarray(j) Then
inArray = True
YCounter = YCounter + 1
ReDim Preserve IDarray(1 To YCounter)
IDarray(YCounter) = SSBarray(i)
Exit For
End If
Next j
If inArray = False Then
NCounter = NCounter + 1
ReDim Preserve noIDarray(1 To NCounter)
noIDarray(NCounter) = SSBarray(i)
End If
Next i
For i = 1 To UBound(IDarray)
Identifiers.Cells(i, 4) = IDarray(i)
Next i
For i = 1 To UBound(noIDarray)
NoIdentifiers.Cells(i, 4) = noIDarray(i)
Next i
End Sub
Revised Code:
'Sort and Compile Data
Dim i As Long
endSSB = SSB.Range("A" & Rows.Count).End(xlUp).Row
endEDM = EDM.Range("A" & Rows.Count).End(xlUp).Row
BBlast = BB.Range("A" & BB.Range("A" & Rows.Count).End(xlUp).Row)
Public Type otherIDs
SEDOL As Variant
ISIN As Variant
End Type
Dim SSBIds As New Scripting.Dictionary
Dim IDs As otherIDs
For i = 2 To endSSB
'Add an ID\row number pair
SSBIds.Add SSB.Cells(i, 1).Value2
Next i
Dim EDMIds As New Scripting.Dictionary
For i = 2 To endEDM
IDs.SEDOL = EDM.Cells(i, 8).Value2
IDs.ISIN = EDM.Cells(i, 7).Value2
EDMIds.Add EDM.Cells(i, 9).Value2, IDs.SEDOL, IDs.ISIN
Next i
Dim IdMatches As New Scripting.Dictionary
Dim IdMisMatches As New Scripting.Dictionary
Dim key As Variant
For Each key In SSBIds
'If it's in the other dictionary...
If EDMIds.Exists(key) Then
'...add the row to the matches...
IdMatches.Add key, EDMIds(key)
Else
'...otherwise add the row to the mismatches.
IdMisMatches.Add key, EDMIds(key)
End If
Next
i = 1
For Each key In IdMatches.Keys
Identifiers.Cells(i, 4) = key
Identifiers.Cells(i, 5) = IdMatches.IDs.SEDOL
Identifier.Cells(i, 6) = IdMatches.IDs.ISIN
i = i + 1
Next
i = 1
For Each key In IdMisMatches.Keys
NoIdentifiers.Cells(i, 4) = key
i = i + 1
Next
Arrays aren't the best containers to be using here. Dictionaries have an .Exists method that uses a much faster hash lookup than a simple iteration that compares every value.
Not only that, repeatedly calling Redim Preserve is incredibly inefficient compared to adding items to a Dictionary. Every time you increase the array dimension, the entire data set gets copied to a newly allocated area of memory and the data pointer for the array gets updated to point to it.
Example using Dictionaries (you'll need to add a reference to Microsoft Scripting Runtime):
Dim SSBIds As New Scripting.Dictionary
For i = 2 To endSSB
'Add an ID\row number pair
SSBIds.Add SSB.Cells(i, 1).Value2, i
Next i
Dim EDMIds As New Scripting.Dictionary
For i = 2 To endEDM
EDMIds.Add EDM.Cells(i, 9).Value2, i
Next i
Dim IdMatches As New Scripting.Dictionary
Dim IdMisMatches As New Scripting.Dictionary
Dim key As Variant
For Each key In SSBIds
'If it's in the other dictionary...
If EDMIds.Exists(key) Then
'...add the row to the matches...
IdMatches.Add key, EDMIds(key)
Else
'...otherwise add the row to the mismatches.
IdMisMatches.Add key, EDMIds(key)
End If
Next
i = 1
For Each key In IdMatches.Keys
Identifiers.Cells(i, 4) = key
i = i + 1
Next
i = 1
For Each key In IdMisMatches.Keys
NoIdentifiers.Cells(i, 4) = key
i = i + 1
Next
Note that this assumes that your key columns have unique values. If they don't, you can either test for the presence of the key before adding a value (this matches your code's behavior of only taking the first match), or you can create a Collection of values to store in the Dictionary for each key, or something else entirely depending on your requirement.

Resources