Dictionary seemingly pulling same value for all keys (VBA) - vba

I have a dictionary in which each key is a number 0-7. Each item is a collection of two number values. For each value in the dataset I am iterating through, the code checks which key 1-7 it belongs to, pulls the appropriate collection from the dictionary, adds the data to the collection, and inserts the collection back into the dictionary. It also adds every value to the 0 key in the dictionary, so that at the end the 0 key will contain a grand total i.e. the output should look like this:
Key:Value
0:100
1:20
2:10
3:10
4:20
5:10
6:5
7:25
The problem that I am having is that the output is something like:
Key:Value
0:100
1:100
2:100
3:100
4:100
5:100
6:100
7:100
It seems like each time I pull a collection from the dictionary using a key it, pulls the same collection regardless of key and then adds the data to that collection.
Dictionary:
For region = 0 To 7
regDict.Add region, blankColl
Next region
Adding items:
thisRegion = 'some number 1-7 found elsewhere
' pull the collection from the regDict
Set subtotalColl = regDict.Item(thisRegion)
subtotalSales = subtotalColl("Item") + thisSales
subtotalColl.Remove ("Item")
subtotalColl.Add Item:=subtotalSales, Key:="Item"
' replace the collection for thisRegion with the new one
regDict.Remove thisRegion
regDict.Add thisRegion, subtotalColl
' ----------- "region 0" gets every record no matter
' ----------- what the region of the record is
' pull the collection at 0 from the regDict
Set zeroSubtotalColl = regDict.Item(0)
subtotalSales = zeroSubtotalColl("Item") + thisSales
zeroSubtotalColl.Remove ("Item")
zeroSubtotalColl.Add Item:=subtotalSales, Key:="Item"
' replace the collection for Region 0 with the new one
regDict.Remove 0
regDict.Add 0, zeroSubtotalColl
The problem is that when I check the dictionary after all of this is done, every collection contains the same values! Even if I debug within this, zeroSubtotalColl from regDict(0) contains the the "new" value that I just put back into regDict(thisRegion) as subtotalColl.
Any help much appreciated.

blankColl is always a reference to the same collection, and you add it for each key, so all the "values" point to the same object.
Current:
Set regdict = CreateObject("scripting.dictionary")
Set blankColl = New Collection 'guessing here what you did...
For region = 0 To 7
regdict.Add region, blankColl
Next region
regdict(1).Add "hello"
Debug.Print regdict(7).Count '>>1 oops - should be empty!
Fix:
For region = 0 To 7
regdict.Add region, New Collection
Next region
regdict(1).Add "hello"
Debug.Print regdict(7).Count '>>0 still empty!

Related

Matching and Placing Values from Another Table

I'm trying to develop a check box switch on a form that auto-fills a check mark when the correct value is selected.
Item
Group
Category
Keyboard
Medium
Electronics
Laptop
High
Electronics
Mouse Pad
Low
Electronics
Apple
Low
Food
Wine
High
Food
Milk
Medium
Food
Goal:
I have a listbox that allows multiple selection of items, given a certain criteria "Category". The list automatically updates based on the category selected. Once selected, I want to see if the item selected falls under a certain "Group", either low, medium, or high.
Current Code:
' Create Array for multiple selection box
Dim ct As Integer, v, ArrCT()
ReDim ArrCT(ct = 0 To Me.testbutton.ItemsSelected.Count - 1)
For Each v In Me.testbutton.ItemsSelected
ArrCT(ct) = Me.testbutton.ItemData(v)
ct = 1 + ct
Next v
' Load table for matching
Dim db As Database
Dim rs As Recordset
Set db = CurrentDb
Set rs = db.OpenRecordset("MasterList")
Dim qmct As Integer, x, NewArr()
ReDim NewArr(qmct = 0 to Me.testbutton.ItemsSelected.Count - 1)
For Each x In Me.testbutton.ItemsSelected
NewArr(qmct) = ArrCT(x)
If NewArr(qmct) = rs![Item] Then
NewArr(qmct) = rs![Group]
End If
qmct = 1 + qmct
Next x
My idea was to create a temporary array which stores the selection, and then have a second for-loop go through the array to match the correct "group".
With the second array, despite triple checking the field name, I get:
Run-time error '3265':
Item not found in this collection.
Item information is loaded in the ArrCT array, and adjusts based on the number of selection.
Is there an easier way to call for a value in the table?
*Note: sample table is not my actual data, that has approximately 20 times more items, category and grouping.

How to pass Value from Last Loop?

I have been attempting many ways to retrieve 2 collections together, while the first collection holds a comma-separated value in a column, we can not find a solution to passing the first collection value to the second For Each.
This code simply retrieves database rows and adds each result to our list control using the Add() method.
Dim transferstable As New DataTable
count = 0
For Each row As DataRow In transferstable.Rows
Dim name = Truncate(row.Item("name"), 42)
ListControl1.Add(name, row.Item("username")", row.Item("added"), avatars, row.Item("online"), images(count), 0)
count += 1
Next
Problem
We need to nest the loops, so we get the value from the first collection from the "avatars" column (image1,image2,image3) and call it from Add() - 4th parameter.
We only get always 1 string result into the view, while the actual query reports many rows with 2 strings (image1, image2) so I tried this:
Dim lst As New List(Of String) From {
transferstable.Rows(0).Item(8)
}
count = 0
For Each item As String In lst
For Each row As DataRow In transferstable.Rows
Dim name = Truncate(row.Item("name"), 42)
ListControl1.Add(name, row.Item("username")", row.Item("added"), item, row.Item("online"), images(count), 0)
count += 1
Next
Next
And the still the same single result! (8) is the GROUP_CONCAT column for "avatars" How do we pass this list over to the 4th parameter?
We want to retrieve these as URL remote images and render them to view with Bitmap.
Expected result:
A list of 15x15 pictures that represent each split result from GROUP_CONCAT(avatars)
I've been at all different ways to do this for most of the day, I know nesting is the right direction but I can't figure out why only 1 result is coming back (image1 - not image1,image2, etc.)
Some physical image files do not exist anymore, so rendering that to view also has it halt after a few single string results, so it quits and gives an error, like a 404 but does not proceed with the 180 other rows.
https://learn.microsoft.com/en-us/dotnet/visual-basic/language-reference/statements/for-each-next-statement

VBA dynamically populate nested data structure

I have a few SQL tables, some which are linked, that I would like to query once and store locally in a single variable. I can't predict the length of the data ahead of time so I need a dynamic data structure.
Example data I'm querying:
Table 1
NameA
Red
Green
Blue
Table 2
NameA NameB
Red A
Red B
Red C
Blue D
Blue E
Green F
Table 3
NameA NameC
Red One
Blue Two
Blue Three
Blue Four
Blue Five
Green Six
Green Seven
I need to be able to filter and access NameB and NameC based on NameA values. I would prefer a nested dictionary structure where I could query like below:
Table1("0") 'will equal "Red"
Table2("Red")("0") 'will equal "A"
Table2("Blue")("1") 'will equal "E"
Table3("Green")("1") 'will equal "Seven"
'note: point here is data structure, not order of results
I have tried using VBA's nested dictionaries but have been unable to get around the lack of a "deep copy" function. One algorithm I wrote:
With SqlQueryResult
i = 0
Do Until .EOF
Call Table1.Add(CStr(i), .Fields(0).Value)
i = i + 1
.MoveNext
Loop
End With
For Each key In Table1.Keys
SqlQueryResult = GetResultsFromQuery(SELECT NameB WHERE NameA = Table1(key))
With SqlQueryResult
i = 0
Do Until .EOF
Call TempDict.Add(CStr(i), .Fields(0).Value)
i = i + 1
.MoveNext
Loop
End With
Set Table2(Table1(key)) = TempDict
TempDict.RemoveAll
Next key
Unfortunately assigning a Dict to another Dict only sets a reference and doesn't actually copy over data -- when I delete TempDict, the nested data from Table2 is also removed.
I also can't have a new dictionary per "branch" in the nest structure as I need this data to be available at a module-level scope, and therefore need to define these in the top of the module before program execution.
I've looked at multi-dimentional dynamic arrays - these can't be assigned to a parent structure like a dictionary. I also can't predict the size of each of these tables, e.g. Table1 might be 5/20/100/etc in size, Red may have 2/5/100/etcetc results in Table 2, Blue have 1/20/etcetc results in Table 2. Redim only works on a single dimension in an array.
I've had a brief look at Collections as well, and I am not sure these are viable.
I don't have much experience with classes and I would rather avoid a very involved process - I want it to be easy to add linked and unliked (i.e. data linked to Table 1, like Table 2 and 3, vs stand-alone data not related to any other table) to this program should I need to in the future. (My benchmark for "easy" is a pandas dataframe in python).
A simple wrapper class for scripting dictionaries which implements a clone method. This should work fine with primitive datatypes.
Option Explicit
Private Type State
Dict As scripting.Dictionary
End Type
Private s As State
Private Sub Class_Initialize()
Set s.Dict = New scripting.Dictionary
End Sub
Public Function Clone()
Dim myClone As scripting.Dictionary
Set myClone = New scripting.Dictionary
Dim myKey As Variant
For Each myKey In s.Dict
myClone.Add myKey, s.Dict.Item(myKey)
Next
Set Clone = myClone
End Function
Public Property Get Item(ByVal Key As Variant) As Variant
Item = s.Dict.Item(Key)
End Property
Public Property Set Item(ByVal Key As Variant, ByVal Value As Variant)
s.Dict.Item(Key) = Value
End Property
Public Sub Add(ByVal Key As Variant, ByVal Item As Variant)
s.Dict.Add Key, Item
End Sub
You will now be able to say
Set Table2.Item(Table1.Item(key)) = TempDict.Clone

Remove duplicate values and cells from one column

I have tried so many methods from the removeduplicates, selections and scripting dictionaries and I cannot get this to work. I do understand there are multiple ways to do this but if any of you can help, that would be great.
I have one list of values that I am pulling through from another sheet (up to approx 80k rows) into cell B13 downwards. I am then trying to remove the duplicate values and cells so I am left with unique values which I can then use to perform lookups on other sheets.
Sub Address_Sage()
Dim dataBook As Workbook
Dim dict As Object
Dim Sage_Data As Worksheet, Address As Worksheet
Dim dataSource As Range, dataDest As Range
Dim sourceDataRowCount As Integer, index As Integer
Dim rowCount As Long
Dim strVal As String
Set dataBook = Application.ThisWorkbook
Set sheetSource = dataBook.Sheets("Sage_Data")
Set sheetDest = dataBook.Sheets("Address")
Set dict = CreateObject("Scripting.Dictionary")
Set dataSource = sheetSource.Range("A3", _
sheetSource.Range("A90000").End(xlUp))
sourceDataRowCount = dataSource.Rows.Count
Set dataDest = sheetDest.Range("B13", "B" & _
sourceDataRowCount)
For index = 1 To sourceDataRowCount
dataDest(index, 1).Value = dataSource(index, 1).Value
Next index
Sheets("Address").Select
rowCount = ActiveSheet.Range("B13").CurrentRegion.Rows.Count
Do While rowCount > 0
strVal = Address.Cells(rowCount, 1).Value2
If dict.exists(strVal) Then
ActiveSheet.Rows(rowCount).EntireRow.Delete
Else
dict.Add strVal, 0
End If
rowCount = rowCount - 1
Loop
'Set dict = Nothing
End Sub
It always gets stuck on strVal line. I have tried changing value2 to value1 as I only have column but no luck.
thank you
Not super experienced in VBA so I can't speak to exactly what you're doing and what your code is saying but I thought I'd share this with you. Last week I had to create a macrobook that returned the unique entries of electrical defects that different crews observed while on the job. I made a dictionary that read all of the entries in the spreadsheet and then later printed all of the unique entries. I'll post the code and try to walk you through it.
If .Range("A" & i) <> "" Then
If dict.Exists(data) Then
dict(data) = dict(data) + 1
Else
dict.Add Key:=Data, Item:="1"
End If
End If
So the code basically says if column A (i is simply an incrementer) is not empty, then we're going to read the entries of column A. Data is simply a variable and you would set it equal to the range of values you'd like read in the dictionary. Obviously dictionary keys are unique and cannot repeat, so the code asks if the key already exists in the dictionary. If so, we will add one to it's count or value. And if not we will add that key to the dictionary. At the end of your loop, your dictionary will have stored all unique entries and the number of times they appeared.
Now we can reference them or print them.
For r = 0 To dict.Count
Sheets("Results").Range("B" & iResults) = dict.Keys(r)
Sheets("Results").Range("C" & iResults) = dict(dict.Keys(r))
Next
This second piece of code is a loop from 0 to the number of entries in your dictionary. It starts at zero because the dictionary is stored like an array and VBA arrays are base zero. The first statement will print the unique keys for every r until there are no more entries in the dictionary. The second statement will print the value or items associated with them. It will be an integer value equal to the number of times that unique entry showed up in your data.
You can use this same method for other purposes as well, not just printing the data but referencing it and using it somewhere else. But I am sure you will find that the For-loop with dict.Keys(r) is the easiest way to run through your dictionary entries. Took me a few days to figure it out and it revolutionized my program. Hope this helps you out.

Creating an Excel Macro to delete rows if a column value repeats consecutively less than 3 times

The data I have can be simplified to this:
http://i.imgur.com/mn5GgrQ.png
In this example, I would like to delete the data associated with track 2, since it has only 3 frames associated with it. All data with more than 3 associated frames can stay.
The frame number does not always start from 1, as I've tried to demonstrate. The track number will always be the same number consecutively for as many frames as are tracked. I was thinking of using a function to append 1 to a variable for every consecutive value in column A, then performing a test to see if this value is equal >= 3. If so, then go onto the next integer in A, if no, then delete all rows marked with that integer (2, in this case).
Is this possible with Visual Basic in an Excel Macro, and can anyone give me some starting tips on what functions I might be able to use? Complete novice here. I haven't found anything similar for VBA, only for R.
I assume you understand the code by reading it.
Option Explicit
Public Function GetCountOfRowsForEachTrack(ByVal sourceColumn As Range) As _
Scripting.Dictionary
Dim cell As Range
Dim trackValue As String
Dim groupedData As Scripting.Dictionary
Set groupedData = New Scripting.Dictionary
For Each cell In sourceColumn
trackValue = cell.Value
If groupedData.Exists(trackValue) Then
groupedData(trackValue) = cell.Address(False, False) + "," + groupedData(trackValue)
Else
groupedData(trackValue) = cell.Address(False, False)
End If
Next
Set GetCountOfRowsForEachTrack = groupedData
End Function
Public Sub DeleteRowsWhereTrackLTE3()
Dim groupedData As Scripting.Dictionary
Set groupedData = GetCountOfRowsForEachTrack(Range("A2:A15"))
Dim cellsToBeDeleted As String
Dim item
For Each item In groupedData.Items
If UBound(Split(item, ",")) <= 2 Then
cellsToBeDeleted = item + IIf(cellsToBeDeleted <> "", "," + cellsToBeDeleted, "")
End If
Next
Range(cellsToBeDeleted).EntireRow.Delete
End Sub
GetCountOfRowsForEachTrack is a function returning a dictionary (which stores track number as key, cell address associated with that track as string)
DeleteRowsWhereTrackLTE3 is the procedure which uses GetCountOfRowsForEachTrack to get the aggregated info of Track numbers and cells associated with it. This method loops through the dictionary and checks if the number of cells associated with track is <=2 (because splitting the string returns an array which starts from 0). It builds a string of address of such cells and deletes it all at once towards the end.
Note:
Add the following code in a bas module (or a specific sheet where
you have the data).
Add reference to "Microsoft Scripting.Runtime" library. Inside VBA, click on "Tools" -> "References" menu. Tick the "Microsoft Scripting.Runtime" and click on OK.
I have used A2:A15 as an example. Please modify it as per your cell range.
The assumption is that you don't have thousands of cells to be deleted, in which case the method could fail.
Make a call to DeleteRowsWhereTrackLTE3 to remove such rows.