Noemi
New Contributor III

As we've seen in part 1, we can manage data in memory using individual variables. However, often you need to handle multiple elements in a similar manner: you might want to retrieve all base members under a particular Account, or check parameters passed to a Custom Calculate. For these tasks, we need to interact with objects that are effectively collections of other objects; typically this means working with a List or a Dictionary.

Note: as our code gets a bit more complex, commands get longer and longer. This makes for awkwardly long lines, which can be difficult to read even on large screens. For this reason, we will start using the line-continuation combination of characters, which tells the system to continue reading the next line as it was part of the current one. Example with pseudo-code:

' very long
Dim something as List(Of SomethingElse) = SomeClass.Doing.VeryDifficult.Operations(argument1, argument2, argument3)

' same as above, but spread over three lines using " _"
' (space and underscore). 
Dim something as List(Of SomethingElse) = _
   SomeClass.Doing.VeryDifficult.Operations( _
     argument1, argument2, argument3 )

List

List objects allow you to keep a number of objects of the same type, inside a single variable. Like a shopping list, it will contain objects in a specific order; and it's pretty magic, because it will automatically shrink or enlarge to hold any amount of elements (well, not really "any amount" - the practical limit is around 1 billion objects; but if your List ever gets into that region, you're probably doing something wrong). Lists are good for performing the same operation on a number of objects, using loops.

Creating a new List and adding or removing elements is simple, and you can then retrieve individual elements by specifying its position:

' create a List
Dim myList As New List(Of String)

' add some elements to the List
myList.Add("Apple")
myList.Add("Banana")
myList.Add("Cherry")

' actually, my kid hates bananas, so let's remove that
myList.Remove("Banana") 

' what's the first thing in the List?
Dim firstToBuy as String = myList(0)  ' firstToBuy will now contain "Apple"

Note how we had to declare the type of objects the List will hold, in this case Strings.

In real OneStream code, you'll be more likely to actually receive a List produced by some API call. For example:

' a "dimension primary key" (dimPk) points to a specific Dimension hierarchy
Dim myAccountDimPk as DimPk = api.Pov.AccountDim.DimPk
Dim myU2DimPk as DimPk = api.Pov.UD1Dim.DimPk
' we need the ID of a parent member
Dim incomeId as Integer = api.Members.GetMemberId( _
	myAccountDimPk.DimId, "Income Statement")

' get base members under "Income Statement"
Dim baseAccounts as List(Of Member) = api.Members.GetBaseMembers( _
	dimPk, incomeId)

' get direct children of U1#Top
Dim childrenOfTop as List(Of MemberInfo) = api.Members.GetMembersUsingFilter( _
	myU2DimPk, "U1#Top.Children")

Both GetBaseMembers and GetBaseMembersUsingFilter return Lists, so if we want to examine what they contain, we first have to create a variable of type List to hold those results. To then loop over that List, you can use a For Each loop. Here's how:

' loop over a List of Member objects, 
' writing member names to the system Error Log
For Each myMember as Member In baseAccounts
	api.LogMessage(myMember.Name)
Next

This code means that, for each element in the baseAccounts List, the system will assign it to a variable called "myMember" (which can hold a Member instance), and then execute any code between line 3 and line 5 (excluded) - in this case, just writing the member name to the the log.

If you want to know how many elements are in a List, you can use the Count property:

Dim numberOfBaseAccounts As Integer = baseAccounts.Count

You might also find yourself in a situation where you want to filter a List, in order to retain only elements that satisfy some condition. There are a few ways to do that. If you're working with OneStream metadata members, you probably want to use APIs like the above-mentioned GetMembersUsingFilter; if you're working with other types of object, you can use the .Where function:

' Find all String objects in the List that start with "A"
' and place them in a new List called "filteredFruits"
Dim filteredFruits as List(Of String) = myList.Where( _
	Function(fruit) fruit.StartsWith("A"))

This code creates a function to test any String object ("fruit") passed, returning True if the String starts with "A". It then applies the test to all elements currently in myList, and places the ones that satisfy such test into a new List called filteredFruit.

Dictionaries ("Dicts")

Dictionaries also are collections of multiple objects, but they work differently from the List. Instead of just holding contained elements in a particular order, it defines key items and can assign a value to each of them. This way, we can retrieve those values later if we can remember the key:

' create a new Dictionary. Keys will be String objects, and values will be numbers (Integer)
Dim fruitInventory as New Dictionary(Of String, Integer)

' add a few values
fruitIntentory.Add("Banana", 123)
fruitInventory.Add("Peach", 34)

' I said we don't want bananas!
fruitInventory.Remove("Banana")

' How many peaches do we have again?
Dim howManyPeaches as Integer = fruitInventory("Peach")  
' howManyPeaches will now contain 34

Dictionaries are good for holding values that you need to look up over and over. With a List, you would have to remember the exact position of each element, which could even change as items are added and removed; with a Dictionary, you just need a key that never changes.

In OneStream you'll often interact with dictionaries called "NameValuePairs". These property-objects typically hold the arguments passed to functions. For example, you might have created a Custom Calculate step in DataManagement, with the Parameters property set to "Param1=MyMember,Param2=MyProduct". In the rule, you would retrieve them like this:

Dim nvpDict as Dictionary(Of String, String) = args.CustomCalculateArgs.NameValuePairs

Dim param1content as String = nvpDict("Param1")
' param1content now contains "MyMember"

Dim param2content as String = nvpDict("Param2")
' param2content now contains "MyProduct"

Dictionaries can generate errors if you try to retrieve values for keys that don't exist. To avoid that situation, OneStream provides a utility method that, in those cases, will return a default value:

' assuming the same "Param1=MyMember,Param2=MyProduct" configuration as before ...
Dim param3content as String = nvp.XFGetValue("Param3", "My Default Text")
' param3content will now contain "My Default Text", 
' because Param3 did not exist so XFGetValue gave us the default

And that's it for today!