3D Datastructure with Index - arrays
This is my first question on stackoverflow, and I am earnestly open to feedback on how/where/when to ask better questions and how to contribute to stackoverflow better.
Background:
My ultimate goal is to graph projected equipment usage by date at various test labs.
I have identical equipment in use at several labs, and I'm creating a sheet that will show me a future projection of equipment usage at each lab.
What I'm Starting With:
I have an Excel document with several worksheets, each containing information on what equipment will be used at which test house during what period of time.
My Goal:
To create a graph of equipment usage for each test lab. The graph will show how many of each piece of equipment are in use for a given date. My intention is to have a chart series for each type of equipment with Date as the X-axis and the number of pieces of that equipment in use on the Y-axis.
What I've Done So Far:
I have written code that loops through all my information sheets and creates a vba collection of every unique test lab name and a separate vba collection of every unique piece of equipment I want to track. This code also finds the first date and last date any piece of equipment is used.
Help Request:
Because I essentially have three "dimensions" - Test Lab, Piece of Equipment, and Equipment Use Date - I had planned to use a 3D array to aggregate all my data and provide the source for my usage graphs. This array would have equipment as one dimension, date as the second, and test lab as the third.
However, as I've considered this implementation, it seems rather clumsy. It will hold all my data, but, as far as I can see, I can't refer to the elements of the array by keys or labels. I would have to create separate 2D arrays to hold index labels for each dimension of the 3D array.
Is there a 3D data structure in Excel VBA that supports index keys for each dimension?
Failed Searches and Attempts:
I first tried to create a unique array to hold equipment and usage date, each array named for a unique test lab. I learned from this post that I am not able to dynamically create and name an undefined number of new arrays within a sub: Naming an array using a variable.
I then looked into whether I could use the collections I had already created to somehow function as labels for the array indices, but it seems that I'm not able to find the collection index by the key. I would have to loop through the collection to find the index every time I want to reference an element in the 3D array: Retrieve the index of an object stored in a collection using its key (VBA).
If you need to call out a collection by key, that collection should instead be declared as a dictionary.
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict(Key) = Value
It is much more powerful than a collection. I hope that helps.
FULL INFORMATION: https://excelmacromastery.com/vba-dictionary/
I implemented information from all the comments and answers I received. Thank you Jeremy, Victor K, and HackSlash!
Here's the solution that worked for me in a nutshell: An array of a user-defined data type containing arrays of a user-defined data type containing scripting dictionaries, i.e. an array of arrays of dictionaries. I also created reference dictionaries for use in retrieving data. (See working example below)
First, in order to use scripting dictionaries in VBA, go to Tools > References and check the box next to "Microsoft Scripting Runtime." I learned this here: Does VBA have Dictionary Structure?. I also learned that this setting is included if the sheet is distributed (others won't have to enter VBA and check the box before they can use your sheet): http://www.snb-vba.eu/VBA_Dictionary_en.html.
Public Type ItemTracked
ItemName As String
UseDates As Scripting.Dictionary
End Type
Public Type TrackingStructure
TestLab As String
TrackedItems() As ItemTracked
End Type
Sub Tracking()
Dim TrackingArr() As TrackingStructure
'**************
'Example Data
'**************
'Create array of example dates
Dim DateArray As Variant
DateArray = Array(43164, 43171, 43178) 'Excel date codes for 3/5/2018, 3/12/2018, and 3/19/2018
'Create array of example equipment
Dim EquipArray As Variant
EquipArray = Array("Cooling Pump", "Heating Pad", "Power Supply")
'Create array of example number of pieces of equipment in use
Dim UseArray As Variant
UseArray = Array(0, 1, 2)
'Create array of example test lab names
Dim LabNames As Variant
LabNames = Array("LabABC", "Lab123", "LabDOREMI")
'**************
'Creating and Populating Data Structure
'**************
'Create array of TrackingStructure Type with space to track test labs
ReDim TrackingArr(UBound(LabNames))
'Loop through TrackingArr to populate usage for each test lab
For i = LBound(TrackingArr) To UBound(TrackingArr)
'Record lab name
TrackingArr(i).TestLab = LabNames(i)
'Redimension size of TrackedItems to accomodate example equipment
ReDim TrackingArr(i).TrackedItems(UBound(EquipArray))
'Loop through EquipArray for each test lab
For j = LBound(EquipArray) To UBound(EquipArray)
Set TrackingArr(i).TrackedItems(j).UseDates = New Scripting.Dictionary
TrackingArr(i).TrackedItems(j).ItemName = EquipArray(j)
'Loop through dates and usage for each piece of equipment
For k = LBound(DateArray) To UBound(DateArray)
'Populate date and equipment use
TrackingArr(i).TrackedItems(j).UseDates.Add DateArray(k), UseArray(k)
Next k
Next j
Next i
'**************
'Referencing Data
'**************
'Create and Populate Dictionaries for Use in Referring to Data
Set LabNamesRef = New Scripting.Dictionary
Set EquipArrayRef = New Scripting.Dictionary
For i = LBound(TrackingArr) To UBound(TrackingArr)
LabNamesRef.Add TrackingArr(i).TestLab, i
Next i
For i = LBound(EquipArray) To UBound(EquipArray)
EquipArrayRef.Add EquipArray(i), i
Next i
'Demonstration Print of Entire Data Structure
For i = LBound(TrackingArr) To UBound(TrackingArr)
Debug.Print "Lab Name: " & TrackingArr(i).TestLab
For j = LBound(TrackingArr(i).TrackedItems) To UBound(TrackingArr(i).TrackedItems)
Debug.Print TrackingArr(i).TrackedItems(j).ItemName
For k = 0 To TrackingArr(i).TrackedItems(j).UseDates.Count - 1
Debug.Print TrackingArr(i).TrackedItems(j).UseDates.Keys(k), TrackingArr(i).TrackedItems(j).UseDates.Items(k)
Next k
Next j
Next i
'Access One Example Entry
Debug.Print "Lab Name:" & TrackingArr(LabNamesRef("Lab123")).TestLab
Debug.Print "Equipment:" & TrackingArr(LabNamesRef("Lab123")).TrackedItems(EquipArrayRef("Cooling Pump")).ItemName
Debug.Print "Usage on Date 43164: " & TrackingArr(LabNamesRef("Lab123")).TrackedItems(EquipArrayRef("Cooling Pump")).UseDates(43164)
End Sub
Related
How to merge several arrays of items (Outlook) into one big array
I do collections of Outlook items with VBA taking items from particular Outlook folders. In the code below I collect items from two different folders into two different arrays. (Code is written in Excel) Set olGetArchMeetings = olNS.Folders(2).Folders(4).Items olGetArchMeetings.IncludeRecurrences = True olGetArchMeetings.Sort "[Start]" strRestrictionArch = "[Start] >= '" & mStart & "' AND [End] <= '" & mEnd & "'" Set objArray1 = olGetArchMeetings.restrict(strRestrictionArch) Set olGetMeetings = olNS.GetDefaultFolder(9).Items olGetMeetings.IncludeRecurrences = True olGetMeetings.Sort "[Start]" strRestriction = "[Start] >= '" & mStart & "' AND [End] <= '" & mEnd & "'" Set objArray2 = olGetMeetings.restrict(strRestriction) The questions is: Is there any way to merge two arrays of objects into one? Like add all items from objArray2 to the end of objArray1 and therefore make a new Array that will contain itmes from both arrays? I tried to merge via basic array joining like merging strings arrays but it did not help. I expect to get one big array of items that will contain items from separate arrays
First of all, the Restrict method of the Items class applies a filter to the Items collection, returning a new collection containing all of the items from the original that match the filter, but not an array. The questions is: Is there any way to merge two arrays of objects into one? Like add all items from objArray2 to the end of objArray1 and therefore make a new Array that will contain itmes from both arrays? No, there is no trivial way of getting a single Items collection from different Restrict calls. You may consider building an array of data extracted from items found. But a better yeat approach is to use a single search which can be run in the background in Outlook. The Application.AdvancedSearch method allows performing a search based on a specified DAV Searching and Locating (DASL) search string in multiple folders. To specify multiple folder paths, enclose each folder path in single quotes and separate the single quoted folder paths with a comma. The key benefits of using the AdvancedSearch method in Outlook are: The search is performed in another thread. You don’t need to run another thread manually since the AdvancedSearch method runs it automatically in the background. Possibility to search for any item types: mail, appointment, calendar, notes etc. in any location, i.e. beyond the scope of a certain folder. The Restrict and Find/FindNext methods can be applied to a particular Items collection (see the Items property of the Folder class in Outlook). Full support for DASL queries (custom properties can be used for searching too). To improve the search performance, Instant Search keywords can be used if Instant Search is enabled for the store (see the IsInstantSearchEnabled property of the Store class). You can stop the search process at any moment using the Stop method of the Search class. Read more about that in the article that I wrote for the technical blog: Advanced search in Outlook programmatically: C#, VB.NET.
I have no clue on how to code for outlook, but basic array merging would be like this. That is code for one-dimensional arrays only: Sub ArrayMerge() Dim obA As Object, obB As Object, obC As Object, obD As Object Dim arrA As Variant, arrB As Variant, arrAll As Variant Dim m As Integer, n As Integer, first As Integer, last As Integer 'setting objects Set obA = Cells(1) Set obB = Cells(2) Set obC = Cells(3) Set obD = Cells(4) 'dimensioning arrays ReDim arrA(1 To 2) ReDim arrB(1 To 2) 'filling both arrays Set arrA(1) = obA Set arrA(2) = obB Set arrB(1) = obC Set arrB(2) = obD first = UBound(arrA) + 1 ' = 3 last = UBound(arrA) + UBound(arrB) ' = 4 'Enlarge the first array to join the second one ReDim Preserve arrA(1 To last) For m = first To last n = n + 1 Set arrA(m) = arrB(n) Next m End Sub
Ignore array item if it doesn't exist in slicer items (slicer filtering with array)
I have encountered a problem when filtering an OLAP based pivot table slicer with items from an array. I have an Array consisting of machine numbers that are identical to slicer items captions, that looks like this: machines = Array("Machine1", "Machine2", "Machine3" etc.. ) and I want to use this array to filter the slicer using: ActiveWorkbook.SlicerCaches("MachinesSlicer").VisibleSlicerItemsList = Array(machines) And it works fine until there is a machine number in array that is not visible in the slicer items (for example due to other slicers filtering, such as date etc). Is there a way to bypass those items that cause the problem? I've tried to create a new array, looping through the slicer items and comparing them to array items using: For Each machName In machines For Each si1 In sl1.SlicerItems If si1.Caption = machName Then TestArray = TestArray & Chr(34) & si1.Caption & Chr(34) & Chr(44) End If Next si1 Next machName But what it does is it creates an array that has a single element that is a string looking like: "Machine1", "Machine2", Machine3" ...etc But I need these elements to be seperated in order to pass them into the VisibleSlicerItemList. Maybe some of you will have any idea how to resolve this problem. Any help will be appreciated, thanks!
Ignore my previous answer, blonde moment. You need an array of course. You can create a dynamic array based on sl1.SlicerItems Dim dynArr() Dim i as Integer 'Long Dim si1 For Each si1 In sl1.SlicerItems i = i + 1 ReDim Preserve dynArr(1 To i) dynArr(i) = si1.Caption Next Pozdrawiam kolege ;)
VBA to paste only certain values of cell from one sheet to another
Can some one help me with the below code, what I am looking for is, from sheet "Form" certain values of cells mentioned in 2 sets of Array. 1st set of Array should get copied to sheet "Tracker" C3 onward and second set of array from next cell after the 1set of array ends say EF3 onwards. whereas now first sett is its pasting from A3 and second from A4. Please let me know in case of any question. Following is the code which I am using now: Sub AddEntry() Dim LR As Long, i As Long, cls Dim LR2 As Long, j As Long, cls2 cls = Array("C2", "C3", "G2", "G3", "C5", "C6", "C7", "C8", "C9", "C10", "C11", "C12", "C13", "A17", "C17", "D17", "F17", "G17", "H17", "A18", "C18", "D18", "F18", "G18", "H18", "A19", "C19", "D19", "F19", "G19", "H19", "A20", "C20", "D20", "F20", "G20", "H20", "A21", "C21", "D21", "F21", "G21", "H21", "A25", "B25", "C25", "D25", "E25", "F25", "G25", "H25", "A26", "B26", "C26", "D26", "E26", "F26", "G26", "H26", "A27", "B27", "C27", "D27", "E27", "F27", "G27", "H27", "A28", "B28", "C28", "D28", "E28", "F28", "G28", "H28", "A32", "C32", "E32", "G32", "H32", "A33", "C33", "E33", "G33", "H33", "A34", "C34", "E34", "G34", "H34", "A35", "C35", "E35", "G35", "H35", "A39", "D39", "F39", "A40", "D40", "F40", "A41", "D41", "F41", "A45", "C45", "E45", "G45", "A46", "C46", "E46", "G46", "A47", "C47", "E47", "G47", "D51", "D52", "D53", "D54", "D55", "D56", "D57", "D58", "D59", "D60", "D61", "D62", "D63", "D64", "D65", "D66", "D67") With Sheets("Tracker") LR = WorksheetFunction.Max(3, .Range("C" & Rows.Count).End(xlUp).Row + 1) For i = LBound(cls) To UBound(cls) .Cells(LR, i + 1).Value = Sheets("Form").Range(cls(i)).Value Next i End With cls2 = Array("E51", "E52", "E53", "E54", "E55", "E56", "E57", "E58", "G59", "E60", "E61", "E62", "G63", "E64", "E65", "E66", "E67", "C70", "D70", "E70", "F70", "G70", "H70", "C71", "E71", "G71", "C72", "E72", "G72", "C73", "E73", "G73", "C74", "E74", "G74", "C75", "E75", "G75", "C76", "E76", "G76", "C77", "E77", "G77", "C78", "E78", "G78", "C79", "E79", "G79", "C82", "D82", "E82", "F82", "G82", "H82", "C83", "E83", "G83", "C84", "E84", "G84", "B88", "B89", "B90", "B91", "C88", "C89", "C90", "C91", "D88", "D89", "D90", "D91", "E88", "E89", "E90", "E91", "F88", "F89", "F90", "F91", "G88", "G89", "G90", "G91", "H88", "H89", "H90", "H91") With Sheets("Tracker") LR2 = WorksheetFunction.Max(3, .Range("EW" & Rows.Count).End(xlUp).Row + 1) For j = LBound(cls2) To UBound(cls2) .Cells(LR, j + 1).Value = Sheets("Form").Range(cls2(j)).Value Next j End With End Sub
Assuming that you want to start cell entries in sheet "Tracker" more to the right, you can add the column number instead of +1 (= column A) and write as follows: Array 1: assigning cell values starting from column C .Cells(LR, i + [C1].Column).Value = Sheets("Form").Range(cls(i)).Value Array 2: assigning cell values starting from column EF ' should be LR2 instead of LR :-) .Cells(LR2, j + [EF1].Column).Value = Sheets("Form").Range(cls2(j)).Value Note [C1].column returns the column number (in any worksheet), e.g. column C Counts 3.
I took a look at your file; the first thing I did was flip through the VBA & try to compile it -- which incidentally, I would recommended to anyone as a first step with a downloaded XLSM. (I haven't seen a malicious macro yet and I'd like to keep it that way!) I can see that this file has been a "work in progress" because there are bits of code here and there that don't compile properly, such as Me statements pointing to a missing userform, and references to mis-named worksheets such as Form (View) instead of View_Form. Ideally, this project should be moved from Excel to Access. Excel can be used for filling forms and storing data, but if this is potentially going to sizable, you're best off to use "the right tool for the job". Duplicating your form(s) into Access forms instantly removes the need to copy certain cells to certain sheets, not to mention ease of validation, reporting, security, and unlimited room for expansion plus ease of moving data between Excel, Access, Outlook, etc. (You even called the spreadsheet a database in one spot!) If your concern is that you're unfamiliar with Access, if you designed this workbook, migration to Access will be a breeze once you figure out the basics of table and form design. Even Outlook has some pretty nifty form capabilities which can autopopulate the data table when an emailed form is received. If you need to stay in Excel, how about a User Form instead of the sheet-based form? I too often see people forgetting about Office's built-in features and starting from scratch. That being said, I've been a user of MS Office for 25 years and have never used an Excel User Form. When I think "form", I think MS Access. Another option, if you want to stay with the worksheet-based form, instead of listing all the cells in the array etc, a minor redesign could make it simpler. One way would be to have a hidden row on the form tab so you have a single uninterrupted line of all the data you need to store. For example, you could hide row 1 and 2, make row 1 the headings like Sourced Processed Year Address etc. and then row 2 could be an "interim" place to store the data, so A2 formula is =C2, B2 is =C3', B3 is=C5` etc. Finally another sneaky option could be to add hidden comments in each cell that has data that needs to be saved, and then when the form is complete, loop through all the cells looking for comments, and each comment would contain a title or cell reference indicating where that cell's data needs to go. The destination should be a very straightforward table Use as many columns as you need, but it's not a place for formatting or formulas. (Think database!) For example, C2 (Sourced By) could have a hidden comment like "Tracker:C" then when the form is filled, you could parse the comments and move the data dynamically (instead of hardcoding 250 cell addresses!) with something like: Option Explicit Sub moveData() 'untested; example only Dim cell As Variant, nextBlankRow As Integer Dim comm As String, sht As String, col As String nextBlankRow = 5 'calculate this somehow 'loop through cells with comments For Each cell In ActiveSheet.Cells.SpecialCells(xlCellTypeComments) If cell.Comment.Text <> "" Then 'get comment comm = cell.Comment.Text 'extract location for data like "Sheetname:Columnletter" sht = Left(comm, InStr(comm, ":") - 1) col = Right(comm, Len(comm) - InStr(comm, ":")) 'populate correct location with data Sheets(sht).Range(col & nextBlankRow).Value = cell.Value End If Next cell End Sub As with anything in Excel (or Office in General) there are a dozen ways you could accomplish the same task. Opt for the ones that don't involve repeating the same code over and over, nor hardcoded data. Planning for future (unexpected) growth is very important, as is debugging as-you-go, which is my last suggestion: Option Explicit at the top of every module, and Alt+DLcompile often, removing or commenting-out unused code. Bottom line, best bet: Access, Excel, Outlook all have form capabilities built in. use a form for a form and you'll save yourself a headache now and later. Hopefully this gives you some ideas. Good Luck!
Using Array of Defaults in VBA to Populate Excel WBS with Outline
I've got a WBS (Work Break Down Structure), with multiple rows (top-level of a group outline), and each top-level row is an activity. Directly under the activity are the roles involved. Based on the value of the activity in the top level ("plan", for example), the cells in the level below are populated, according to their values in a related table on another sheet ("defaults" tab). Currently, the rows under the activity (that correspond to roles) are doing an ugly index/match lookup, which multiplied by 25 roles, can grind the spreadsheet to a halt. What I think will solve this issue is taking the Role Defaults table, putting it in a persistent array, and using the values in the array over and over, as the user puts in the top-level activities. I just can't figure out how to make the array persistent (so the VBA doesn't repopulate it ever time a user changes a cell). If the values in the Role Defaults table changes, I can handle that with a worksheet OnChange, so that's not an issue. Row 3 "Activity 1" is what the Activity Rows look like with the group outline collapsed. Rows 4-9 are what the Activity Rows look like with the group outline expanded, showing the underlying roles. For each of the roles, this is the table on another tab that's used to look up the value that should be in the corresponding Activity/Role cell on the WBS tab.
I'm a proponent of using Dictionary objects whenever the need for lookups arise. In my solution below, I use nested dictionaries to return a combination of Top-Level and Activity. (Note: I tried to understand your business need as best as I could, but I'm sure I didn't nail it. I also assumed some knowledge of VBA above a beginner's level. If you have follow up questions, please ask and we'll try and help). First, create a new module to hold the globally available Dictionary. This cannot be a Worksheet module. (In the VBE, go to Insert --> Module). At the very top of the module, before creating a subroutine, declare a publicly available Dictionary Public oDictWbs As Object We only want one instance of this dictionary, so I like to use a Singleton like pattern which returns a Dictionary if already created, and if not, create and return a new one. (Note: I factored out the routine that returns a new dictionary into RefreshWBS so that it can be used to create a new dictionary based on your business rules. So, for example, in the Default worksheet OnChange event, you can call RefreshWBS [code reuse is always fun]). Private Function GetWBS() As Object If Not oDictWbs Is Nothing Then Set GetWBS = oDictWbs Exit Function End If Set GetWBS = RefreshWBS() End Function Private Function RefreshWBS() Dim sDefault As Worksheet Dim rTopLevels As Range Dim rActivities As Range Dim rIterator As Range Dim rInnerIter As Range Set oDictWbs = Nothing 'Both variables below establish the range that stores the fixed info (the default worksheet) 'Instead of hard coding in the range, create your own logic based on your needs and rules Set sDefault = Sheets("Default") Set rTopLevels = sDefault.Range("B1:C1") Set rActivities = sDefault.Range("A3:A4") Set oDictWbs = CreateObject("Scripting.Dictionary") For Each rIterator In rTopLevels If Not oDictWbs.exists(rIterator.Value) Then Set oDictWbs(rIterator.Value) = CreateObject("Scripting.Dictionary") End If For Each rInnerIter In rActivities If Not oDictWbs(rIterator.Value).exists(rInnerIter.Value) Then oDictWbs(rIterator.Value)(rInnerIter.Value) = sDefault.Cells(rInnerIter.Row, rIterator.Column) End If Next rInnerIter Next rIterator Set RefreshWBS = oDictWbs End Function Finally, we create a function that can be accessed from within the Worksheet itself, allowing the user to access information in the WBS Dictionary. You can enter into an Excel cell a function like =GetWbsActivityTime(B1, A4) presuming that cell B1 contains the top-level descriptor and A4 describes the activity. So long as that value is in the dictionary, it will return the value associated with it. Function GetWbsActivityTime(sTopLevel As String, sActivity As String) As Variant Dim oDict As Object Set oDict = GetWBS() If Not oDict.exists(sTopLevel) Then GetWbsActivityTime = CVErr(xlErrRef) Exit Function End If If Not oDict(sTopLevel).exists(sActivity) Then GetWbsActivityTime = CVErr(xlErrRef) Exit Function End If GetWbsActivityTime = oDict(sTopLevel)(sActivity) End Function I know it's a lot to absorb, so review it and let me know of any questions or quirks with which I can help. Also, if I totally missed the point of the exercise, let me know and I'll see if we can salvage parts of the solution.
Array elements disappearing / not loading
I have no idea what is going on here and it's a little bizzare. I'm adapting a VBA macro into a VB.net project, and I'm experiencing what I would describe as some extreemly unusual behavior of a method I'm using to pass data around in VB.net. Here's the set up... I have, for indexing reasons, a collection that consists of all open orders: Public allOpenOrders As New Collection Within this collection, I store other collections, indexed by account number, that each contain information about each open order in an array that is three elements long. Here is how I'm populating it: openOrderData(0) = some information openOrderData(1) = some information openOrderData(2) = some information SyncLock allOpenOrders If allOpenOrders.Contains(accountNumber) Then 'Already in the collection... accountOpenOrders = allOpenOrders(accountNumber) accountOpenOrders.Add(openOrderData) Else 'Not already in collection accountOpenOrders = New Collection accountOpenOrders.Add(openOrderData) allOpenOrders.Add(accountOpenOrders, AccountNumber) End If End SyncLock Here's the thing, if I place a stop after end synclock and check the collection, I can clearly see that the array with all data is there, plain as day. However, when I move on in my code (this is occuring in another thread after the preceeding code has executed) to retrieve it and write it to a workbook... If allOpenOrders.Contains(accountNumber) Then accountOpenOrders = allOpenOrders(accountNumber) For each openOrderArray In accountOpenOrders OutputSheet.Cells(1, 1).value = accountNumber For counter = 0 to 2 OutputSheet.Cells(1, counter + 2).value = openOrderArray(counter) Next counter Next openOrderArray End If I get the first element of the array in column B, but C and D are blank. Even more puzzling, if I put a stop right after the allOpenOrders.Contains line I can look at the collection and the last two elements of the array are now blank. Most puzzling of all, they aren't just blank, they are blanks, a number of blanks equal in length to the original field I recorded in that element of the array?! Any ideas are appreciated. I can tell you I'm using the same type of method to load other data in this workbook with no problems. These are also the only instances in which the allOpenOrders collection is touched... I'm so confused by these results.