Using expressions in your forms: a reference for all operators and functions
In the form designer, you can build constraint, relevance, and calculation expressions using simple wizards that are built right into the designer. Using the wizards in the form designer is the easiest way to ensure that all of your expressions are written in the correct syntax and will function properly.
You can also edit expressions by hand, in which case the following reference materials will come in handy. Even when you're editing spreadsheet form definitions directly, you can use the constraint-builder, the relevance-builder, and/or the calculation-builder, all of which are available as tools in the Your forms and datasets section of your server's Design tab.
Referencing current values
${age}
will return the exact value that was entered in the field named age
..
" refers to the user's proposed entry or selection for the current field.
. < 3
checks to see if the new proposed value for the current field is less than 3.Comparison operators
Operator | Operation | Example | Example answer |
---|---|---|---|
= |
Equal | ${fieldname} = 3 |
true or false |
!= |
Not equal | ${fieldname} != 3 |
true or false |
> |
Greater-than | ${fieldname} > 3 |
true or false |
>= |
>-or-equal | ${fieldname} >= 3 |
true or false |
< |
Less-than | ${fieldname} < 3 |
true or false |
<= |
<-or-equal | ${fieldname} <= 3 |
true or false |
Logical operators
${age} = 3 or ${age} = 4
will return true if the age is either 3 or 4.
${age} > 3 and ${age} < 5
will return true if the age is between 3 and 5.
not(${age} > 3 and ${age} < 5)
will return true if the age is not between 3 and 5.
Working with strings
'Male'
or ' '
for a single space. An exception to this is if you want to include single-quotes in your literal string. For example:if(${yesno} = 1, "a string with 'single quotes' in it", "no single quotes here")
will work, butif(${yesno} = 1, 'a string with 'single quotes' in it', 'no single quotes here')
will not work.Additionally, beware of smart quotes, which will break your expressions. Many rich text editors will automatically convert your straight quotes (
""
or ''
) into smart quotes or curly quotes (“”
or ‘’
).
string(34.8) = '34.8'
string-length(.) > 3 and string-length(.) < 10
can be used as a constraint expression to make sure the current field is between 3 and 10 characters.
substr(${phone}, 0, 3)
will return the first three digits of a phone number.
concat(${firstname}, ' ', ${lastname})
will return a full name by combining the values in the
firstname
and lastname
fields.concat(${field1}, linebreak(), ${field2}, linebreak(), ${field3})
will return a list of three field values with linebreaks between them.
lower('Street Name')
will return "street name".
upper('Street Name')
will return "STREET NAME".
Working with select_one and select_multiple fields
count-selected(.) = 3
can be used as a constraint expression to make sure exactly three choices are selected.
selected(${gender}, 'Male')
can be used as a relevance expression to only show a group or field if the respondent selected
Male
as their gender.selected()
function should always specify the choice value, not the choice label. In other words, always use the value from the value column in the choices worksheet of the form definition.
selected-at(${fieldname}, 0) = 'Shona'
can be used as a relevance expression to only show a group or field if the first selected choice is
Shona
.choice-label(${selectonefield}, ${selectonefield})
will return the choice label for whatever choice is currently selected in the field named
selectonefield
.choice-label(${selectmultfield}, selected-at(${selectmultfield}, 0))
will return return the label for the first selected choice in the field named
selectmultfield
.pulldata()
function in a calculate field to directly pull the label from the appropriate column of the external data.Please note: this is a similar, but improved version of the old
jr:choice-name()
function.
Working with repeated data
In SurveyCTO, if you want to ask the same question(s) multiple times, you can put a field inside a repeat group. This results in multiple instances of the same field. The following functions can help you deal with these repeated fields, and the repeated data that they produce. See the help topic on repeating fields for more details.
join(', ', ${hh_member_name})
will generate a single comma-separated list from all entered names.
join()
, except that it will check each instance in repeatedfield's repeat group using the supplied expression. If the expression evaluates to false, the item will be omitted from the output.
join-if(', ', ${hh_member_name}, ${age} >= 18)
will generate a single comma-separated list from the names of only adult household members (those with ages 18 and over).
count(${repeatgroupname})
will return the number of 'instances' of the group.
count()
, except that it will check each instance in repeatgroup using the supplied expression. If the expression evaluates to false, the item will be omitted from the output.
count-if(${hhmembers}, ${age} >= 18)
will return the count of adult household members, based on the age field within the hhmembers repeat group.
sum(${loan_size})
will return the total value of all loans.
sum()
, except that it will check each instance in repeatedfield's repeat group using the supplied expression. If the expression evaluates to false, the item will be omitted from the output.
sum-if(${loan_size}, ${loan_size} > 500)
will return the total value of all loans that are over 500. Smaller loans will be ignored.
min(${hh_member_age})
will return the age of the youngest member in the household.
min()
, except that it will check each instance in repeatedfield's repeat group using the supplied expression. If the expression evaluates to false, the item will be omitted from the output.
min-if(${hh_member_age}, ${hh_member_age} >= 18)
will return the age of the youngest adult in the household. Those younger than 18 will be ignored.
max(${hh_member_age})
will return the age of the oldest member in the household.
max()
, except that it will check each instance in repeatedfield's repeat group using the supplied expression. If the expression evaluates to false, the item will be omitted from the output.
max-if(${hh_member_age}, ${hh_member_age} >= 18)
will return the age of the oldest adult in the household. Those younger than 18 will be ignored.
index()
when this calculate expression is within a repeat group, will return 1 for the first instance of the repeat, 2 for the second, and so on.
index()
instead of the ODK function position(..)
. While position(..)
will function similarly to index()
in some cases, it will fail if you use it in a non-repeating group that is inside a repeating group.
indexed-repeat(${name}, ${names}, 1)
will return the first name available when the
name
field is inside a prior repeat group named names
.
indexed-repeat(${name}, ${names}, index())
From inside a later repeat group, this calculate expression will pull the xth name from the prior repeat group, where x is the instance number of the current repeat group (e.g., if currently in the fourth instance of a repeat group, it will return the fourth name from the earlier repeat group).
indexed-repeat(${name}, ${families}, ${familynumber}, ${names}, ${membernumber})
will pull a particular family member's name when family member names are inside a repeat group that is itself inside a repeat group of families.
rank-index(1, ${random_draw})
will calculate the rank of the first instance, based on the value of its
random_draw
field as compared with other instances' values.
rank-index()
, except that it will check each instance in repeatedfield's repeat group using the supplied expression. If the expression evaluates to false, the item will be omitted from the calculation.
rank-index-if(1, ${age}, ${age} >= 18)
will calculate the age rank within the set of adults.
Working with lists of items
count-items(',', ${list_of_addresses})
if the list_of_addresses field contains a comma-separated list, this will return the number of addresses.
item-at(',', ${list_of_addresses}, 0)
if the list_of_addresses field contains a comma-separated list, this will return the first address in the list.
item-at()
. If value is not found, this will return -1. If value occurs more than once in the list, this will return the index of the first occurence.
item-index(',', ${list_of_names}, ${name})
if the list_of_names field contains a comma-separated list, this will return the index of the first time ${name} occurs.
item-present(',', ${list_of_addresses}, '')
if the list_of_addresses field contains a comma-separated list, this will check to see if any items in that list are empty.
de-duplicate(' ', ${fieldname})
will remove duplicate values from the space-separated list contained in the field named
fieldname
.join(' ', ${repeatedfield})
, but that list might then contain the same selections multiple times. The calculate expression:de-duplicate(' ', join(' ', ${repeatedfield}))
would join together all of the selections, and remove duplicates. You could then use
count-items()
, item-at()
, etc. on the combined list in the calculated field.
rank-value(3, '4 2 1 9 3 7')
will calculate the rank of
3
in the given list. Since 3
is the 4th highest value, this expression will return 4
.
Working with geographical data
distance-between(${start_gps}, ${end_gps})
area(${gps_reading})
Call this expression from outside the repeat group that contains the
gps_reading
geopoint field to calculate the total area between all the points.
geo-scatter(${location}, 20)
This will take the GPS point stored in the location field and move it to a random point within 20 meters of the original point.
short-geopoint(${location})
Working with numbers
Operator | Operation | Example | Example answer |
---|---|---|---|
+ |
Addition | 1 + 1 |
2 |
- |
Subtraction | 3 - 2 |
1 |
* |
Multiplication | 3 * 2 |
6 |
div |
Division | 10 div 2 |
5 |
mod |
Modulus | 9 mod 2 |
1 |
number('34.8') = 34.8
int('39.2') = 39
min()
, then the minimum of the passed fields will be returned. (See further above for the version that accepts a single repeated field.)
min(${father_age}, ${mother_age})
will return the age of either the mother or the father, whichever is smaller.
max()
, then the maximum of the passed fields will be returned. (See further above for the version that accepts a single repeated field.)
max(${father_age}, ${mother_age})
will return the age of either the mother or the father, whichever is larger.
format-number(${income})
This calculate expression might format "120000" as "120,000".
round(${interest_rate}, 2)
e^x
.
Working with dates and times
duration()
Use this expression in a regular calculate field to capture the total duration spent on the form overall.
once(duration())
Use this expression in a calculate_here field to find the total time it took the user to first reach that calculate_here field. You can then subtract one captured duration from another to get the time spent in between those two fields.
today()
inside of format-date-time()
like in the example below.
format-date-time(today(), '%Y-%b-%e')
once(format-date-time(now(), '%Y-%b-%e %H:%M:%S'))
Use this expression in a calculate_here field to save the date and time at which a particular point in your form is first reached.
now()
by itself will store only the date, without the time. To return and store a value that also includes time, now()
must be used with format-date-time()
, like the above example.
${birthday} > date('2013-01-31')
Use this expression to calculate if the birthday (presumably a date field) is after Jan. 31, 2013.
format-date-time(date-time('2013-01-31T16:42:00'), 'at %H:%M:%S on %e %b, %Y')
Use this expression to convert a string that represents a date time into a different format. Do no use
date-time()
for comparisons when the time part matters, since the time part is not taken into account. Use decimal-date-time()
(below) when it comes to comparisons that should also account for the time part.
decimal-date-time(now()) < decimal-date-time('2013-01-31T16:42:00')
Use this expression to calculate if the current date and time is before Jan. 31, 2013, 4:42pm. This can be useful in relevance expressions that prevent a field from being shown after a certain date-time.
decimal-time('2013-01-31T18:00:00')
This expression returns 0.75.
format-date-time(${fieldname}, '%Y-%b-%e %H:%M:%S')
%Y | four-digit year |
%y | two-digit year |
%m | two-digit month |
%n | one-or-two digit month |
%b | three-letter month |
%d | two-digit day |
%e | one-or-two-digit day |
%a | three-letter day of week |
%H | two-digit hour |
%h | one-or-two-digit hour |
%M | two-digit minute |
%S | two-digit seconds |
%3 | three-digit milliseconds |
format-date-time()
are stored as strings. This means you will be limited to using string functions on that product, unless you format the value again using functions like date-time()
or int()
.
Working with enumerators
Working with phone calls
event_name|phone_number|timestampIn each entry, event_name describes the type of event, phone_number records the phone number (if applicable), and timestamp records the time since the form was started. The timestamp will show seconds since the start of the form and is calculated in the exact same way as the duration() function. These log entries will contain a much greater level of detail when Collect is set as the default phone app.
phone-call-log()
Used alone in a calculate field, this might return something like the following:
Form started||0 Call started|5555555555|12 Form exited (call in progress)|5555555555|25 Form resumed (call in progress)|5555555555|26 Call ended|5555555555|45 Form exited||77
if(phone-call-duration() div duration()>0.5, 'yes', 'no')
will return 'yes' if there was an active phone call during more than half of the time spent filling out the form.
collect-is-phone-app()
will return true if Collect is the default phone app.
Other functions
relevant(${followup_question})
will check to see if the followup_question field is currently relevant.
relevant()
above). Returns false if the field has a non-blank value.
empty(${consent})
will check to see if the consent field is currently empty.
if(selected(${country}, 'South Africa') or selected(${country}, 'Zimbabwe'), 'SADC', 'Non-SADC')
will check to see if the selected country is either
South Africa
or Zimbabwe
. If yes, it will return SADC
. If neither of those countries were selected, it will return Non-SADC
.
pulldata('hhplotdata', 'plot1size', 'hhid_key', ${hhid})
will pull a value either from an attached .csv file named
hhplotdata.csv
or from an attached dataset with the unique ID hhplotdata
. The value will come from the plot1size
column of the pre-loaded data, and the hhid
field will be used to identify the matching row in the pre-loaded data's hhid_key
column.
random()
, this would draw a new random number every time the form is edited and saved – which is not generally what you want.
once(format-date-time(now(), '%Y-%b-%e %H:%M:%S'))
Use this expression in a calculate_here field to save the date and time at which the field was first reached. Enclosing the
format-date-time(now(), '%Y-%b-%e %H:%M:%S')
function within once()
will ensure that the date-time is only recorded the first time the user reaches that field, even if they go back and revisit the field later.
once()
on the very outside of an expression. For example: once(random()*2)
is okay, but 2*once(random())
is not. To avoid potential problems, use once(...)
alone in one calculate or calculate_here field, then use that field in more complex expressions elsewhere.
once(random())
, then refer to that field's value in other expressions in your form.- Never nest
once(random())
directly inside another function or expression, even in constraint or calculate expressions. - You should always call
random()
inside theonce()
function. - Never use
random()
directly in a relevance expression, because you don't want to generate a new random number every time the relevance is calculated: you want just one stable random number for each filled-out form.
coalesce(${id}, ${id2})
regex()
in relevance expressions, constraint expressions, choice filtering, and any other places that accept expressions. SurveyCTO uses a Java engine for evaluating expressions, but for most expressions you can use a Javascript tool like RegExr to develop and test your expressions.
regex(., '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}')
will check for a valid-looking email address.
hash(${name}, ${birthday})
will return a hash of the
name
and birthday
fields, which can then be stored and used to verify a match later on, without storing any PII (personally identifiable information).
once(uuid())
Android | [BRAND]|[MODEL]|[ANDROID_API_VERSION_NUMBER]|SurveyCTO Collect [VERSION] ([BUILD_ID]) |
iOS | Apple|[DEVICE NAME]|[iOS_VERSION_NUMBER]|SurveyCTO Collect [VERSION] ([BUILD_ID]) |
Web forms | [browser user-agent]|SurveyCTO web forms [SERVER_VERSION] |
google|Pixel 3|10|SurveyCTO Collect 2.70.1 (26c7d74)
iOS:
Apple|My iPhone|13.1.2|SurveyCTO Collect 2.70.1 (52.18)
Web Forms:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.83 Safari/537.36|SurveyCTO web forms 2.70.2
plug-in-metadata(${counter})
will return the plug-in metadata saved in the
counter
field, if any.
item-at(' ', plug-in-metadata(${counter}), 0)
will return the first item in a space-separated list of items in the plug-in metadata saved in the
counter
field, if any. See further above for more about item-at()
.