VBA dynamically populate nested data structure - vba

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

Related

Why does ".Remove" affect all the items in a 2D structure assigned with a list?

I'm currently trying to create a Sudoku Solver, and on the step of assigning some possible values to a box that is not already preoccupied. (Bit of background info for why I'm doing this shebang: Sudoku is a number game based on a 9x9 grid, its contextual rules allow certain boxes in the grid that are not preoccupied to hold possible values during the process of solving )
To do this I created a structure, defined it as two dimensional, and populated it with a predefined list of integers using a for-loop.
Now when I tried to remove one integer from the list of a particular item in the two dimensional structure, I found out that all the lists of the items in the structure have had that integer removed. There's probably a simple solution to this, but I've been really struggling to find it. Hope the code below clarifies the somewhat confusing verbal explanation.
Structure Element
Dim PossibleValues As List(Of Integer)
Dim ElementValue As Integer
End Structure
Sub Main()
Dim List as New List(Of Integer)({1,2,3})
Dim TDP(8,8) as Element
For x as integer = 0 to 8
For y as integer = 0 to 8
TDP(x,y).PossibleValues = List
Next
Next
TDP(0,0).PossibleValues.Remove(1)
End Sub
Now I expect only TDP(0,0) would have a list of "2,3" when print out its list of integers, but when I check other items , i.e. TDP(1,0), its list is of integer is also "2,3"
Look at the assignment here:
TDP(x,y).PossibleValues = List
List(Of T) is a reference type, so this assigns a reference to the same List object to each of the array elements.
If you want each item to have it's own list of possible items, you need to either deep copy the list or create a new list:
Sub Main()
Dim TDP(8,8) as Element
For x as integer = 0 to 8
For y as integer = 0 to 8
TDP(x,y).PossibleValues = New List(Of Integer)({1,2,3})
Next
Next
TDP(0,0).PossibleValues.Remove(1)
End Sub

VBA Range change selection, value doesn't change

I have a range variable (called Constr) that is based on data that looks like this
Type Bound1 Bound2 Var1 Var2
X 1 2 3 4
Y 1 2 3 4
--
Z 1 2 3 4
I now use this procedure to change the selection to only the entries before the '--'
Sub Adjust_Selection(which As String, what_in As String, columns As Integer)
Dim row_nr_start As Integer
Dim row_nr_end As Integer
Dim row_nr_delta As Integer
Sheets("Main").Select
row_nr_start = Range(which).Find(what:=what_in, LookIn:=xlValues, LookAt:=xlWhole).Row
row_nr_end = Range(which).Find(what:="--", LookIn:=xlValues, LookAt:=xlWhole).Row
row_nr_delta = row_nr_end - row_nr_start
Range(which).Resize(row_nr_delta, columns).Select
This works and I can see that the selection changes, if I now call it using
Call Adjust_Selection("Constr", "Type", 5)
myitem("Constraints") = Range("Constr").Value
myitem is of type
Dim myitem As New Scripting.Dictionary
however when I access the value it still has everything in it. How can I update the value to only the first few lines up until the '--'?
You are calling Adjust_Selection with the named range Constr and afterwards refer to the named range Constraints. So, of course the result is different because you are referring to two different named ranges.
Furthermore, the named range Constr is not altered. It is merely used as a starting point and then a sub-set is Selected. But by selecting something you are not changing a named range (especially not a differently named range).
So, I am guessing that this is what you are searching for:
Call Adjust_Selection("Constr", "Type", 5)
ThisWorkbook.Names.Add Name:="Constraints", RefersTo:=Selection
myitem("Constraints") = Range("Constraints").Value
Note, that the selection of Adjust_Selection is now "saved" in the new named range Constraints and then myitem is being assigned this named range which is limited to the (correct) selection. Hence, the resulting variable (being a dictionary) contains all elements without the --.
Hi ThatQuantDude, I don't quite understand your question even after trying it out on my own. Based on the examples you gave, I assumed you want to store the selected range data into "Constraints" key? Apart from this, your sub function for selecting the range is working fine.
Call Adjust_Selection("Constr", "Type", 5)
myitem("Constraints") = Range("Constraints").Value
Appreciate if you could elaborate it further so I can better understand what you are trying to do? Thanks.
Range.Resize is a function. It will not change the range; it returns a new one. You just happen to select it, which isn't necessary. Turn your sub into a function returning the result of Range.Resize, and use this function directly on the right hand side of your assignment.
Note that you're not using the same name for your range in both lines of code, which I assume is a typo.

In VBA, how can I store an entire excel table into an easily accessible array that contains all the relevant child information?

I have a very large excel table that contains these elements:
Name Date StartWorkedAt FinishWorkAt HoursWorked
I am creating a button to manipulate this data, but am having trouble deciding what format to store all the data in. Each person in the list will have multiple dates that they all worked, and so I would like to be able to check the start and finish times for each person for various dates.
I wrote a short script to count how many unique names I have so that I could use a multidimensional array to access the data of each person like so:
workTable[0][0]
So this would ideally give me the start and/or finish time of the first person on the first date that he/she worked on.
but the issue I was having was that the data is in various formats. The name is a string, the date is a Date, and the hoursWorked is an integer.
What is an easier way to store the data in VBA so that I can access each person individually and find out what date they worked and when they started and finished?
Use a Class module with properties for each of the columns you need and create a Collection of that class.
For example, create a class module (named say ExcelRow) with the following properties:
Private pName As String
Private pDate As Date
Private pStartWorkedAt As Date
Private FinishWorkAt As Date
Private HoursWorked As Integer
You'll need public properties for EACH of these private variables. Here's an example of setting up Get and Let for the pName property. The public vars can be differently named to the private vars:
Public Property Get Name() As String
Name = pName
End Property
Public Property Let Name(Value As String)
pName = Value
End Property
Then you can have a collection and add instances of each class module row to it:
Dim ExcelRows As Collection
Dim Row As ExcelRow
Set ExcelRows = New Collection
Set Row = New ExcelRow
Row.Name = "Joe"
Row.HourseWorked = 3
ExcelRows.Add Row
Set Row = New ExcelRow
Row.Name = "Sam"
Row.HourseWorked = 54
ExcelRows.Add Row
'Or you could use a For Loop for this
If your table has a unique identifier, consider using that on as the key for the collection item for easy access to the data rows later on. Ash's last line of code would change to:
ExcelRows.Add Row, Row.Name 'given that Name is unique across the table rows
Later you can access the data with:
x = ExcelRows("Joe").HourseWorked '= 3

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.

VB.Net - multi-column data variable object

I want to create an in-memory object in VB.Net with multiple columns. What I am trying to do is create an index of some data. It will look like:
Row 1: 23 1
Row 2: 5 1
Row 3: 3 38
...
I know I can use a rectangular array to do this, but I want to be able to use indexOf opearations on this object. Is there any such structure in VB.Net?
WT
Define a row class, and then create a list of rows, like so:
Class row
Inherits Collections.ArrayList
End Class
Dim cols As New List(Of row)
Now you can access your objects using a x/y notation:
cols(0)(1)
Note this is just a simple example, your structure is uninitialized and untyped.
You can also Shadow the IndexOf function in your own class, for example finding the indexOf by an item's name:
Class col
Inherits Generic.List(Of Object)
Shadows Function IndexOf(ByVal itemName As String) As Integer
Dim e As Enumerator = Me.GetEnumerator
While e.MoveNext
If CType(e.Current, myType).name = itemName Then
Return e.Current
End If
End While
End Function
End Class
You can then access it like so:
Private cols As New col
cols.IndexOf("lookingfor")
If the number of cells in each row is constant and you don't need to grow or shrink the structure, then a simple two-dimensional array is probably the best choice, because its exposes the best possible locality characteristics. If it is not sorted, you can implement indexOf via a simple linear search.
You can do this with a Dictionary.