Introduction
While Sawtooth Script has functions for most of the operations you're likely to perform, there are some situations where our automated scripts don't quite fit users needs and unverified Perl is required. One of the most common cases is in situations where you need to run a script multiple times, iterating over a list, number of items, etc. In these cases, Perl for loops can be very helpful.
Let's imagine we have a constant sum question named "Q1" with a list named "list1," and we need to create a second question "Q2" that only shows the items of Q1 with a response within some range (say, between 10 and 20). While we'll need to use a constructed list, the existing Sawtooth Script functions cannot satisfy our requirements: AIG can get us items above 10 and AIL can get us items below 20, but we can't do both. We need to use unverified Perl! With our knowledge of Perl's if and logical checks, we start by writing this:
[% Begin Unverified Perl
if (GETVALUE('Q1_1') > 10 && GETVALUE('Q1_1') < 20)
{
ADD('list1', 1);
}
End Unverified %]
That achieves the behavior we're after, but only for the first item of Q1. We need this to apply to every item in the constant sum question. One option might be to simply copy-and-paste what we have above, adjusting the item number as we go.
[% Begin Unverified Perl
if (GETVALUE('Q1_1') > 10 && GETVALUE('Q1_1') < 20)
{
ADD('list1', 1);
}
if (GETVALUE('Q1_2') > 10 && GETVALUE('Q1_2') < 20)
{
ADD('list1', 2);
}
if (GETVALUE('Q1_3') > 10 && GETVALUE('Q1_3') < 20)
{
ADD('list1', 3);
}
...
End Unverified %]
Doing this works but it comes with a lot of downsides. Not only can it take a long time to program if we have a large list, but the process of creating and editing repetitive code like this is error prone. Edits (such as changing our logic or renaming the question) must be done for each block of code, and manual editing introduces greater opportunities for errors such as skipping items, referencing incorrect variables, etc. These errors are also often difficult to catch in testing!
If we ever find ourselves wanting to copy-and-paste code like we did above, we should pause and consider ways to automate the repetitive aspects of our code. In many cases, that solution is to use loops. Take a look at this code that works like our copy-and-pasted code, but without any repeated if:
[% Begin Unverified Perl
for (my $i = 1; $i <= 5; $i++)
{
my $surveyVariable = 'Q1_' . $i;
my $response = GETVALUE($surveyVariable);
if ($response > 10 && $response < 20)
{
ADD('list1', $i);
}
}
End Unverified %]
The for Loop
Let's take a closer look at the previous code and try to get a better understanding of how it works. All for loops consist of four parts.
[% Begin Unverified Perl
for (my $i = 1; $i <= 5; $i++)
{
my $surveyVariable = 'Q1_' . $i;
my $response = GETVALUE($surveyVariable);
if ($response > 10 && $response < 20)
{
ADD('list1', $i);
}
}
End Unverified %]
•The initialize code is run once at very beginning of the process. This code usually creates a new counting variable (named $i in this case) and establishes a starting value (= 1). $i will be used in the rest of the code as a counter and to represent the current iteration in our logic.
•The termination code is run once per loop. The code used in this part should return a Boolean result: if the value is true, the loop continues; if false, the loop is ended. In our case, we want the loop to look at the values 1 through 5 (e.g., $i = 1, $i = 2, ... $i = 5), so we already set the initialize value at 1, and now we set the termination logic as $i <= 5.
•The iteration code is also run once per loop. It is used to define the pattern with which we will increment our counting variable. It is common to use the ++ operator as a convenient shorthand to increase our counting variable by one, so our code $i++ indicates that we want to add 1 to our counting variable each iteration. So for the first iteration, $i = 1 because that was our initial value. In the second iteration, we add 1 due to the ++ so $i = 2, etc.
•Finally, the body code is where we put the code we actually want to execute in the loop. We can put just about any code in here. If we need to know what loop iteration we're on during this, we can use $i to refer to the current iteration's value for our counter. In the above example, we need to create the names of the constant sum variables: "Q1_1," "Q1_2," "Q1_3," and so on. So we make use of Perl's . operator to concatenate (join) the text "Q1_" with our loop iteration counter $i.
So the above loop runs like this:
1.Create $i and set it to 1.
2.Check to see if $i is less than or equal to 5. It is, so the process continues.
3.Run the body with $i equal to 1.
4.Increment $i from 1 to 2.
5.Check to see if $i is less than or equal to 5. It is, so the process continues.
6.Run the body with $i equal to 2.
7.Increment $i from 2 to 3.
8....
9.Run the body with $i equal to 5.
10.Increment $i from 5 to 6.
11.Check to see if $i is less than or equal to 5. It is not, so the process terminates.
Deeper Dive of the for Loop
The initialize code
While it is very common to use simple variable names, especially $i, as our iterator, we don't have to. If we're doing something complex with our iterator, we may find it easier to read the code if we give the iterator a name that is more descriptive of what it represents. Try changing the variable name in our previous example, making sure to also update the name wherever it appears in the termination, iteration, and body code.
The termination code
We often want to iterate through all the items of our predefined list. While we could simply put in the number of items in the predefined list in our termination code, the loop would no longer work correctly if we later added items to or removed items from the predefined list. We could save a lot of trouble if we could write our code so that it just works no matter the number of items. This is where we can use ListLength, a Sawtooth Script function that returns the number of items in a list. Here is a simple example where we loop over a list named "list1" and add its items to our current constructed list:
[% Begin Unverified Perl
for (my $i = 1; $i <= LISTLENGTH('list1'); $i++)
{
ADD('list1', $i);
}
End Unverified %]
The iteration code
While it is very common to use an iterator like $i++, we can put other code in this section as well. For example, we could run our code on just the odd numbered items by initializing $i = 1 and then iterating using $i = $i + 2 or $i += 2 instead. Or maybe there's a reason why we want to iterate backwards, so we write our loop like this:
[% Begin Unverified Perl
for (my $i = 5; $i >= 1; $i--)
{
my $surveyVariable = 'Q1_' . $i;
my $response = GETVALUE($surveyVariable);
if ($response > 10 && $response < 20)
{
ADD('list1', $i);
}
}
End Unverified %]
That code works similar to our first loop example, but now adds items to the constructed list in the other direction! Note how we now need to use >= rather than <= in the termination code.
Nested loops
We can put just about any code in the loop's body, and that includes the possibility to include other loops! This is often useful when we we have a grid question and want to evaluate both a series of row values AND a series of column values. Remember that the inner loop needs to use a different iteration variable name than the outer loop. Here's an example for grid question "Q2" with 10 rows and 2 columns:
[% Begin Unverified Perl
for (my $i = 1; $i <= 10; $i++)
{
for (my $j = 1; $j <= 2; $j++)
{
my $surveyVariable = 'Q2_r' . $i . '_c' . $j;
my $response = GETVALUE($surveyVariable);
...
}
}
End Unverified %]
Using names $i, $j, $k, ... is not uncommon when nesting loops, but it can sometimes lead to confusion if we mix up which variable is which. We may want to replace $i with $row and $j with $column in the above code.
next and last
next and last are special commands that give us even more control over how our loops work. The former instruction tells the code to skip the rest of the body code and go straight to the next iteration code. The latter tells the code to skip the rest of the loop entirely, immediately going on to whatever comes after the loop. These are often used in if statements in the loop where we don't want to continue running the body code if some criteria is true. For example, if we only want to add the first item that meets our requirements, we would modify our original loop like this:
[% Begin Unverified Perl
for (my $i = 1; $i <= 5; $i++)
{
my $surveyVariable = 'Q1_' . $i;
my $response = GETVALUE($surveyVariable);
if ($response > 10 && $response < 20)
{
ADD('list1', $i);
last;
}
}
End Unverified %]
Other Loops
Perl comes with several types of loops. While all looping functionality is possible with for, you may find it convenient to use the other loop types in certain situations. If you find yourself constrained by the counter variable, look into the while loop type. If you need to loop over a Perl array (say, looking at a constructed list instead of a predefined list), check out the foreach loop type. Information about each of these can be found online at various external websites. Search for terms like "Perl while loop" or "Perl foreach loop". You can use the LISTVALUESARRAY function to populate a Perl array with the constructed list member values.