Efficient way to group objects in an Array - arrays

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

Related

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.

Assigning Ranges to Array

I have been struggling with this for quite some time, but the error dialogue box that pops up isn't exactly the most helpful. I'm trying to extract a list of names from the worksheet and assigning them to an array using the range function. I tried and tried, but I couldn't seem to get it to work, so I tried reading in the cells 1 by 1 instead, using the Do Until Loop. I didn't expect to be posting this here, so the code of what I was doing before, is already gone, but here's an example:
Dim RangeList As Variant
RangeList = ThisWorkbook.Worksheets("Plan").Range("H1:H132").Value2
I switched it to the next method in hopes that it would lead to a more straightforward approach:
ReDim ResourceList(ResourceLength - 1)
I = 1
Do Until ThisWorkbook.Worksheets("Plan").Cells(I, 8).Value = ""
ResourceList(I) = ThisWorkbook.Worksheets("Plan").Cells(I, 8).Value
Workbooks("NEW PROJECT PLAN").Worksheets("Console").Cells(I, 2).Value = Resource
I = I + 1
Loop
The first one returns an empty range that 'Can't find any cells' and the second one gave me an array of empty strings 169 items long. I feel like I'm pounding my head against a brick wall on this one, any help would be appreciated.
Here is the entirety of the code that I'm trying to troubleshoot:
'Collects the List of Resources
Dim ResourceLength As Long, I As Integer
Dim ResourceList() As String
ResourceLength = ThisWorkbook.FinalRow(8, "Plan")
MsgBox ("Final Row is: " & ResourceLength) 'The Last row used in column 8
ReDim ResourceList(ResourceLength - 1)
I = 1
Do Until ThisWorkbook.Worksheets("Plan").Cells(I, 8).Value = ""
ResourceList(I - 1) = ThisWorkbook.Worksheets("Plan").Cells(I, 8).Value
Workbooks("NEW PROJECT PLAN").Worksheets("Console").Cells(I, 2).Value = Resource
I = I + 1
Loop
ResourceList = ThisWorkbook.FilterArray(ResourceList)
Dim myCount As Integer
Dim Source As Variant
For Each Source In ResourceList
Worksheets("Console").Cells(myCount, 1).Value = Source
myCount = myCount + 1
Next Source
Here is the FilterArray Function:
Public Function FilterArray(UnsortedArray As Variant) As Variant
Dim Intermediate() As Variant
Dim UItem As Variant
' Runs through each item and compares it to the list of items found, if it finds repeats, it throws them out.
For Each UItem In UnsortedArray
If Not ArrayItemExist(Intermediate, UItem) Then
' The Item does not Exist
ReDim Intermediate(UBound(Intermediate) + 1)
Intermediate(UBound(Intermediate)) = UItem
End If
Next UItem
' Returns the Sorted Array.
FilterArray = Intermediate
End Function
Private Function ArrayItemExist(TargetArray() As Variant, TargetItem As Variant) As Boolean
'Searches an Array for TargetItem and returns a boolean stating whether it exists within the Array or not.
Dim ItemFound As Boolean
Dim SItem As Variant
ItemFound = False
For Each SItem In TargetArray
If TargetItem = SItem Then
ItemFound = True
Exit For
End If
Next SItem
ArrayItemExist = ItemFound
End Function
Public Function FinalRow(Column As Integer, Sheet As String) As Long
' Finds the last Row used in the spreadsheet.
FinalRow = Worksheets(Sheet).Cells(Rows.Count, Column).End(xlUp).Row
End Function
When I try to run the software, I receive an error that the For Loop is not initialized, which I traced back to the 'ResourceList' Array/Range being empty.
[Edit]
This function is used to prep an array of names that are extracted from a list of dropdown box resources. This list may contain multiple instances of the same name, so it's sent to the FilterArray function to sort the array into an array with just one instance of each name. Example:
Before and after sorting
After this, it's sent to a module that will inject each name into a dictionary with a corresponding amount of hours that the person is scheduled to work.

VBA Dictionary with Dynamic 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

VBScript get custom object from array

I have some problems with an array in VBScript: I have a central array, in this I save some custom objects. Later on, I want access these elements to print out the objects. But this don't work.
Here is my code;
sub start
redim selektionsArray(0)
for i = 0 to 10
Dim TheDude : Set TheDude = (New Selektion2)("a" & i, "b" & i)
ReDim Preserve selektionsArray(ubound(selektionsArray) + 1)
set selektionsArray(ubound(selektionsArray)) = TheDude
'Works
msgbox selektionsArray(ubound(selektionsArray)).Typ & " = " & selektionsArray(ubound(selektionsArray)).Wert
next
dim i
for i = 0 to ubound(selektionsArray)
set element3 = selektionsArray(i)
'don't work
msgbox selektionsArray(i).Typ & " = " & selektionsArray(i).Wert
next
dim sel
for each sel in selektionsArray
'don't work to
msgbox sel.Wert
next
'strange thing is ubound(selektionsArray) --> 11
end sub
Class Selektion2
Private m_typ
Private m_wert
Public Default Function Init(Typ, Wert)
m_typ = Typ
m_wert = Wert
Set Init = Me
End Function
Public Property Get Typ
Typ = m_typ
End Property
Public Property Get Wert
Wert = m_wert
End Property
public function getWert()
getWert = m_wert
end function
End Class
I don't can access the array-Elements outside the initialisation loop, what is here wrong?
Your error is to assume that
redim selektionsArray(0)
creates an empty array. It does not, the array has an empty first element at index 0. Adding further objects is no problem, but when you try to
set element3 = selektionsArray(i)
for i = 0, or to access the firs sel in the For Each loop, that empty element can't be use to Set the/a variable. So change the 'create my array' statement to
redim selektionsArray(-1)

Comparing two large lists with multiple columns (same number in each list) in excel VBA and do...more stuff

I've searched far and wide and I can't quite find anything to fit my needs.
The situation:
I have two lists of data with the same type data in each column (10 columns but the last 2 are useless), but the lists are of varying length (currently 55k in one, 18k in the other). The longer list is going to be a running list of items with the most up to date data in each column for the unique ID # in column A. The other list is linked to a SharePoint list that I update a couple times each day.
The need:
I need the list that updates from SharePoint to be compared to the running list. If there are matching Unique ID #'s in the lists, then the running list needs to be updated to the pulled data. If the running list doesn't contain a Unique ID that is in the pulled list, the new line needs to be added to the running list (which will be sorted later).
I first tried doing this with cell references in two for loops and for only 10 rows this worked fine. When I tried running it for every line, I had problems. So I tried using arrays instead, but this is new territory for me. The code seems to be working, but it's taking a really long time to run (I've let it go for 10 minutes before force stopping). I've tried adding some efficiency increases like turning off screen updating and calculations, but they shouldn't have any effect since I'm using arrays and not actually updating the cells until the array comparison is finished. If arrays are more efficient, great, but I don't know how to combine the data from the pulled list's array to the running list's array.
Here is the code that I have so far:
Sub Data_Compile_Cells()
Dim sdata As Worksheet, spull As Worksheet
Dim p As Long, d As Long, c As Long
Dim lrdata As Long, lrpull As Long
Dim rdata As Range, rpull As Range
Dim Newvalue As Boolean
Dim apull As Variant, adata As Variant
Dim nrows As Long, ncols As Integer
Set sdata = Sheets("Data")
Set spull = Sheets("Data Pull")
Newvalue = "FALSE"
i = 1
apull = spull.Range("A1").CurrentRegion
adata = sdata.Range("A1").CurrentRegion
'lrdata = sdata.Range("A" & Rows.Count).End(xlUp).Row
'lrpull = spull.Range("A" & Rows.Count).End(xlUp).Row
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
sdata.Activate
'*****UniqueID Check******
'Run through list of Unique ID's pulled from SharePoint
For p = 2 To UBound(apull, 1)
'I tried to add a status bar to see if the code was actually running
'Application.StatusBar = "Progress: " & p & " of " & UBound(apull, 1) & " : " & Format(p / UBound(apull, 1), "0%")
'Compare each one to the Unique ID's already listed
For d = 2 To UBound(adata, 1)
'Check for matching Unique ID's
If adata(d, 1) = apull(p, 1) Then
'Check each cell in the row with the matching Unique ID
For c = 2 To 10
'If a cell does not have the same data, replace the Data array value with the value from the Pull array
If adata(p, c) <> apull(d, c) Then
adata(d, c) = apull(p, c)
End If
Next c
'If a match is found, skip to the next p value
Exit For
Else
Newvalue = "TRUE"
'Need code to append new line to Data array
End If
Next d
Next p
'Sort the data
'Range("A2").CurrentRegion.Sort key1:=Range("A2"), order1:=xlAscending, Header:=xlYes
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
End Sub
Any direction would be much appreciated.
This ran in <1 sec for me, using 20k rows "data", ~3k rows "pull" (mix of updates and new).
EDIT: tidied up and added some comments...
Sub tester()
Const NUM_NEW As Long = 20000 'large enough ?
Dim arrPull, arrData, arrDataId, arrNew()
Dim ubP As Long, ubD As Long
Dim numNew As Long, r As Long
Dim v, c As Long
Dim t, tmp, coll As Collection
t = Timer
'grab the current and new data
arrPull = Sheets("Pull").Range("A1").CurrentRegion.Value
arrData = Sheets("Data").Range("A1").CurrentRegion.Value
ubP = UBound(arrPull, 1)
ubD = UBound(arrData, 1)
numNew = 0
ReDim arrNew(1 To NUM_NEW, 1 To 10) 'array for new data
'create a collection to map ID to "row number"
Set coll = New Collection
For r = 1 To ubD
coll.Add Item:=r, Key:=arrData(r, 1)
Next r
For r = 1 To ubP
tmp = arrPull(r, 1)
v = 0
'collection has no "exists" function, so trap any error
On Error Resume Next
v = coll.Item(tmp)
On Error GoTo 0
If v > 0 Then
'Id already exists: update data
For c = 2 To 10
arrData(v, c) = arrPull(r, c)
Next c
Else
'new Id: add to the "new" array
numNew = numNew + 1
If numNew > NUM_NEW Then
MsgBox "Need larger `new` array!"
'a more sophisticated approach would be to dump the full
' array to the sheet and then redimension it for more
' data...
Exit Sub
End If
For c = 1 To 10
arrNew(numNew, c) = arrPull(r, c)
Next c
End If
Next r
'drop updated and new (if any) to the worksheet
With Sheets("Data")
.Range("A1").CurrentRegion.Value = arrData
If numNew > 0 Then
.Cells(ubD + 1, 1).Resize(numNew, 10).Value = arrNew
End If
End With
Debug.Print "Done in " & Timer - t & " sec"
End Sub
You would be better off using MSAccess to do this. Link to both tables and then do an inner join on the id field or which ever field links the items in the two lists.

Resources