17

I have worked in Python earlier where it is really smooth to have a dictionary of lists (i.e. one key corresponds to a list of stuff). I am struggling to achieve the same in vba. Say I have the following data in an excel sheet:

Flanged_connections 6
Flanged_connections 8
Flanged_connections 10
Instrument  Pressure
Instrument  Temperature
Instrument  Bridle
Instrument  Others
Piping  1
Piping  2
Piping  3

Now I want to read the data and store it in a dictionary where the keys are Flanged_connections, Instrument and Piping and the values are the corresponding ones in the second column. I want the data to look like this:

'key' 'values':

'Flanged_connections' '[6 8 10]'
'Instrument' '["Pressure" "Temperature" "Bridle" "Others"]'
'Piping' '[1 2 3]'

and then being able to get the list by doing dict.Item("Piping") with the list [1 2 3] as the result. So I started thinking doing something like:

For Each row In inputRange.Rows

    If Not equipmentDictionary.Exists(row.Cells(equipmentCol).Text) Then
        equipmentDictionary.Add row.Cells(equipmentCol).Text, <INSERT NEW LIST>
    Else
        equipmentDictionary.Add row.Cells(equipmentCol).Text, <ADD TO EXISTING LIST>
    End If

Next

This seems a bit tedious to do. Is there a better approach to this? I tried searching for using arrays in vba and it seems a bit different than java, c++ and python, with stuft like redim preserve and the likes. Is this the only way to work with arrays in vba?

My solution:

Based on @varocarbas' comment I have created a dictionary of collections. This is the easiest way for my mind to comprehend what's going on, though it might not be the most efficient. The other solutions would probably work as well (not tested by me). This is my suggested solution and it provides the correct output:

'/--------------------------------------\'
'| Sets up the dictionary for equipment |'
'\--------------------------------------/'

inputRowMin = 1
inputRowMax = 173
inputColMin = 1
inputColMax = 2
equipmentCol = 1
dimensionCol = 2

Set equipmentDictionary = CreateObject("Scripting.Dictionary")
Set inputSheet = Application.Sheets(inputSheetName)
Set inputRange = Range(Cells(inputRowMin, inputColMin), Cells(inputRowMax, inputColMax))
Set equipmentCollection = New Collection

For i = 1 To inputRange.Height
    thisEquipment = inputRange(i, equipmentCol).Text
    nextEquipment = inputRange(i + 1, equipmentCol).Text
    thisDimension = inputRange(i, dimensionCol).Text

    'The Strings are equal - add thisEquipment to collection and continue
    If (StrComp(thisEquipment, nextEquipment, vbTextCompare) = 0) Then
        equipmentCollection.Add thisDimension
    'The Strings are not equal - add thisEquipment to collection and the collection to the dictionary
    Else
        equipmentCollection.Add thisDimension
        equipmentDictionary.Add thisEquipment, equipmentCollection
        Set equipmentCollection = New Collection
    End If

Next

'Check input
Dim tmpCollection As Collection
For Each key In equipmentDictionary.Keys

    Debug.Print "--------------" & key & "---------------"
    Set tmpCollection = equipmentDictionary.Item(key)
    For i = 1 To tmpCollection.Count
        Debug.Print tmpCollection.Item(i)
    Next

Next

Note that this solution assumes that all the equipment are sorted!

Krøllebølle
  • 2,878
  • 6
  • 54
  • 79
  • *...add everything to the dictionary fails because the same key is being added several times (as it should)* I don't follow you. Even in python, a `dict` object can only contain unique key values. – David Zemens Jul 15 '13 at 15:13
  • can you not add an ID column and use that as a key instead? –  Jul 15 '13 at 15:13
  • 1
    You can definitely do this, and if you find it too verbose that's unfortunately the nature of VBA when compared to some other languages... One thing to note though: do not try to modify the array while it's still in the dictionary: you need to pull it out by assigning it to a variable, expand it (using Redim Preserve), and then re-assign it back to the Dictionary. – Tim Williams Jul 15 '13 at 16:20
  • @DavidZemens Yeah, bad sentence that didn't add any value to the question. It is removed. – Krøllebølle Jul 16 '13 at 06:50
  • @TimWilliams What's the reason you cannot modify the the array directly? Something with pass by reference/values? It seems like this is the case for my solution as well - the collection has to be pulled out, data added to it, remove it from the dictionary and add it back in. Not that it matters much for at this point since this is static, read-only-once data. – Krøllebølle Jul 16 '13 at 06:57
  • Discussed here: http://stackoverflow.com/questions/1402876/populating-collection-with-arrays/1402986#1402986 – Tim Williams Jul 16 '13 at 16:04

3 Answers3

9

Arrays in VBA are more or less like everywhere else with various peculiarities:

  • Redimensioning an array is possible (although not required).
  • Most of the array properties (e.g., Sheets array in a Workbook) are 1-based. Although, as rightly pointed out by @TimWilliams, the user-defined arrays are actually 0-based. The array below defines a string array with a length of 11 (10 indicates the upper position).

Other than that and the peculiarities regarding notations, you shouldn't find any problem to deal with VBA arrays.

Dim stringArray(10) As String
stringArray(1) = "first val"
stringArray(2) = "second val"
'etc.

Regarding what you are requesting, you can create a dictionary in VBA and include a list on it (or the VBA equivalent: Collection), here you have a sample code:

Set dict = CreateObject("Scripting.Dictionary")
Set coll = New Collection
coll.Add ("coll1")
coll.Add ("coll2")
coll.Add ("coll3")
If Not dict.Exists("dict1") Then
    dict.Add "dict1", coll
End If

Dim curVal As String: curVal = dict("dict1")(3) '-> "coll3"

Set dict = Nothing 
Aiken
  • 2,628
  • 2
  • 18
  • 25
varocarbas
  • 12,354
  • 4
  • 26
  • 37
  • This is the easiest way for me to understand what's going on, thanks! – Krøllebølle Jul 16 '13 at 06:48
  • 1
    Arrays in VBA are 0-based by default, not 1-based, unless you use `Option Base 1`. `MsgBox LBound(Array(1,2,3))` will give `0` – Tim Williams Jul 16 '13 at 16:02
  • When you dimension an array by using the aforementioned statement without performing any other modification, it gets 1-base. This array is 1-base. If you rely on other dimensioning (the array type I replied a post on this lines todays), it is zero base. So I don't think that you can be taxative on this front. What is true is that most of the properties in VBA are 1-based and the kind of array I wrote in my example is 1-base – varocarbas Jul 16 '13 at 16:05
  • @TimWilliams I have updated my answer, I hope that you will update your statement accordingly (or tell me where I am wrong). – varocarbas Jul 16 '13 at 16:14
  • Your first example `Dim stringarray(10) as String` declares an array with *upper bound* of 10, not a *size* of ten. The size is actually 11 (0 to 10). http://msdn.microsoft.com/en-us/library/aa266179(v=vs.60).aspx "Because the default base is 0, the Option Base statement is never required" – Tim Williams Jul 16 '13 at 16:24
  • @TimWilliams if you are right, you are right. I usually rely on 1-based arrays everywhere (by ignoring the zero position). What made me think that this was the case is VBA (never thought too much about it), is that most of the arrays-based properties are 1-based (e.g., sheets in a workbook). Anyway, I have corrected my statement right now. – varocarbas Jul 16 '13 at 16:28
  • @Krøllebølle by following the correction of TimWilliams I have changed one of the statements in my answer: VBA arrays are zero-based by default. The whole point of my answer was giving you a quick guide through the most likely problems you will find and that's why I mentioned the 1-based thing: most (if not all) of the in-built, array-based properties in VBA Excel are 1-based (example: Sheets array). In any case, I said something wrong which hopefully hasn't caused you any problem; you can take a look at the correction I wrote. – varocarbas Jul 16 '13 at 16:38
  • And just in case someone wonders how I cannot have what apparently is so evidently completely clear, is that I always rely on 1-based arrays (let the zero position for defining). In VBA I don't like to use arrays too much because had some memory-leak issues in the past (perhaps it was because of dealing with very old software but since then I don't trust too much). Anyway I use arrays quite a lot but have always started them from the position 1 without wondering why VBA is different than VB.NET (+ the big issue of the array-properties). I apologise but by thinking that this quite irrelevant. – varocarbas Jul 16 '13 at 16:58
  • @Aiken nice correction. It belonged to my long-time-ago-abandoned struggle against certain behaviours here in SO. Feel free to let me know about any other text on these lines (I wrote quite a few of them when I was an active member here) and I will delete it myself right away, as far as they don't have a real point anymore (i.e., it has been a long enough period for a not-strictly-technical warning). – varocarbas Jan 21 '15 at 16:28
  • @Aiken (I read your comment on the correction right now) note that It was partially relevant to the answer as far as this specific downvote wasn't presumably motivated by a true interest/knowledge. So my comment had two motivations: on one hand, highlighting for future readers the fact that most likely the assessment of my answer (and the other ones) wasn't too accurate; on the other hand, it was trying to show how pointless is misusing the assessment system of a correctness-/knowledge-based site for childish reasons. In any case, it has been here long enough time and agree with the deletion. – varocarbas Jan 21 '15 at 16:35
  • @Varocarbas Any user is free to use their votes however they please. If someone wants to downvote your answer solely because you happened to post it at 15:06 on a Monday then they're well within their rights to do so. Whether or not this is morally okay is still up for debate and I won't go into that in comments, but the bottom line is that those are the rules. Adding an addendum to your post to effectively call someone out on their vote isn't ever relevant to an answer IMO, especially when most people won't (or can't) look at anything other than the net votes. – Aiken Jan 22 '15 at 07:53
  • @Aiken I don't think in this way (this is precisely why I stopped participating here). Although it is true that anyone CAN vote because of any reason; they SHOULD focus on technical aspects as far as this is the whole point of this site (IMO). A more extreme example: anyone can kill/steal/... but nobody should do it; and that's why there are authorities making sure that everyone behave as they should (actually, the same than in SO; as far as unfair votes can be removed by moderators). In any case, I was sharing my opinion with those thinking like me (actually you did restrict my freedom) :) – varocarbas Jan 22 '15 at 08:36
  • "Most of the array properties (e.g., Sheets array in a Workbook) are 1-based." I think this is mixing up concepts a bit. [`Sheets`](https://learn.microsoft.com/en-us/office/vba/api/Excel.Sheets) is a [collection](https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/collection-object), not an [array](https://learn.microsoft.com/en-us/office/vba/language/concepts/getting-started/declaring-arrays). A collection's default method, `Item`, takes numeric index values starting at 1, but this is unrelated to array indexing (and unaffected by `Option Base`). – jcb May 14 '19 at 02:59
  • I have deleted my replies to jcb because of being too long/off-topic. The summary is that I will not update my answer. IMO, that comment is an out-of-context, unrelated-to-what-matters-here remark to an old post addressing a very specific issue. I also think that the criticised statement is clear enough and can be helpful for those with limited experience in VBA. I am not even sure about the good faith and exact motivation of that person/comment (writing this here and now makes no sense to me). I welcome all the well-intentioned, sensible, relevant critics to my posts, not anything else. – varocarbas May 16 '19 at 07:44
5

You can have dictionaries within dictionaries. No need to use arrays or collections unless you have a specific need to.

Sub FillNestedDictionairies()

    Dim dcParent As Scripting.Dictionary
    Dim dcChild As Scripting.Dictionary
    Dim rCell As Range
    Dim vaSplit As Variant
    Dim vParentKey As Variant, vChildKey As Variant

    Set dcParent = New Scripting.Dictionary

    'Don't use currentregion if you have adjacent data
    For Each rCell In Sheet2.Range("A1").CurrentRegion.Cells
        'assume the text is separated by a space
        vaSplit = Split(rCell.Value, Space(1))

        'If it's already there, set the child to what's there
        If dcParent.Exists(vaSplit(0)) Then
            Set dcChild = dcParent.Item(vaSplit(0))
        Else 'create a new child
            Set dcChild = New Scripting.Dictionary
            dcParent.Add vaSplit(0), dcChild
        End If
        'Assumes unique post-space data - text for Exists if that's not the case
        dcChild.Add CStr(vaSplit(1)), vaSplit(1)
    Next rCell

    'Output to prove it works
    For Each vParentKey In dcParent.Keys
        For Each vChildKey In dcParent.Item(vParentKey).Keys
            Debug.Print vParentKey, vChildKey
        Next vChildKey
    Next vParentKey

End Sub
Dick Kusleika
  • 32,673
  • 4
  • 52
  • 73
  • I do not think this would work for this specific problem since I need to store more than one item for each key. Or maybe I misunderstood something here? – Krøllebølle Jul 16 '13 at 06:59
  • 1
    It's the same as varocarbas' answer except instead of storing collections in a dictionary, I'm storing dictionaries in a dictionary. I don't know why you'd use a collection if you already have a reference to Scripting Runtime. – Dick Kusleika Jul 16 '13 at 13:41
  • @Dick in what ways is Dictionary better then Collection, so that you say you prefer Dict whenever possible? – Ádám Bukovinszki Apr 16 '18 at 07:38
  • Two things: the `Exists` method prevents the error trapping way of seeing if something is in the collections and, my favorite, you can use `Items` or `Keys` to create an array. Very convenient for filling a combobox. The downside is you can only add to the end of the stack. – Dick Kusleika Apr 16 '18 at 13:48
1

I am not that familiar with C++ and Python (been a long time) so I can't really speak to the differences with VBA, but I can say that working with Arrays in VBA is not especially complicated.

In my own humble opinion, the best way to work with dynamic arrays in VBA is to Dimension it to a large number, and shrink it when you are done adding elements to it. Indeed, Redim Preserve, where you redimension the array while saving the values, has a HUGE performance cost. You should NEVER use Redim Preserve inside a loop, the execution would be painfully slow

Adapt the following piece of code, given as an example:

Sub CreateArrays()

Dim wS As Worksheet
Set wS = ActiveSheet

Dim Flanged_connections()
ReDim Flanged_connections(WorksheetFunction.CountIf(wS.Columns(1), _
    "Flanged_connections"))

For i = 1 To wS.Cells(1, 1).CurrentRegion.Rows.Count Step 1

    If UCase(wS.Cells(i, 1).Value) = "FLANGED_CONNECTIONS" Then   ' UCASE = Capitalize everything

        Flanged_connections(c1) = wS.Cells(i, 2).Value

    End If

Next i

End Sub
Julien Marrec
  • 11,605
  • 4
  • 46
  • 63
  • 1
    Alternatively, you can just `ReDim Flanged_Connections(0 to wS.Cells(1, 1).CurrentRegion.Rows.Count)` at the beginning. This avoids the need to shrink the array at the end, and doesn't muddle the code with an unnecessary size assignment. – David Zemens Jul 15 '13 at 15:13
  • You'll get an array that is not the perfect size since it will be of size 10 in the example he gave, instead of being of size 3. so you'll still need to shrink it if you want to to use Ubound – Julien Marrec Jul 15 '13 at 15:16
  • Point being that it would be *most* ideal to properly dimension your array initially, i.e., use some logic to determine the appropriate `UBOUND` and then set that initially. Perhaps not applicable in this circumstance if the `UBOUND` can't be determined in advance of the loop. Cheers. – David Zemens Jul 15 '13 at 15:18
  • 1
    I fully agree with that (I upvoted your comment). And in any case, it'd be better to use what you initially proposed instead of using an arbitrary value like I did. I'll update my answer with a countif – Julien Marrec Jul 15 '13 at 15:24