Calculations in Hidden Values
CommCare allows application builders to leverage sophisticated calculations using hidden values. With hidden values, you can use calculations in both designing your applications as well as in calculating data inputs.
Logic Syntax Guidelines
This section provides guidance for some of the basic principles and practices of writing calculations or logic in CommCare.
Single vs Double Quotations
Generally values in display or calculation conditions will be inside quotes
You can use single quotes or double quotes, both work fine.
Spaces
Spaces generally will not matter (i.e. Will be ignored) unless they are inside quotes.
Logic for Checkbox Questions
Overview
Checkbox questions have unique logic that differ from Multiple Choice questions with a single answer.
Display Condition General Structure
To create a display condition that will show a question if a particular option in a proceeding checkbox question is selected, the structure will look like this: selected(#form/checkbox_questionid, 'select_item_name')
Example: if you have the a checkbox question called "symptoms" and you want to show a text question called "symptoms_other" when the user selects the "other" choice, the logic would look like:
selected(#form/symptoms, 'other')
You can use the expression builder to make theses expressions - just make sure that in the drop-down you have selected "has selected value" instead of "is equal to"
Display Condition Referencing Number of Options Selected
To create a display condition for a question to show only if a certain threshold of a number of responses is selected you can also use the count-selected(). count-selected() is a way to reference the number of choices selected.
Example: to display a question only if at least 3 options have been selected from a previous checkbox question, use the display condition: count-selected(#form/some_checkbox_question) > 3.
Validation Against Number of Options Selected
You can create a validation condition which limits the number of choices a user can select. This is also done using the "count-selected()" function.
Example: if you want a validation constraint that a user must select less than 3 options: count-selected(.) < 3
Validation Condition Preventing the Selection of None Along with Another Option
If you have a checkbox question which has "none" as one of the options you may want to prevent your user from selecting both "none" in addition to another choice.
Example: Validation condition for a question with a choice that has the item value "none": not(selected(., 'none') and count-selected(.) > 1)
This essentially says "do not allow the user to choose both "none" and an additional item.
Validation Condition for Surveys
A standard survey question may have the answers "Yes", "No", and "(Optional) Additional Comments." With this, you'd want the respondent to 1) be able to select yes and no 2) and would want them to also add in comments.
Example: You can use validation logic in the following way: (not(selected(., 'yes') and selected(., 'no'))) and (not(selected(., 'comments') and count-selected(.) < 2))
Calculations for Hidden Values
You can use a special question type called a hidden value to store values from other forms and carry out calculations without it being visible to the mobile worker. The important field is the calculate field, which you can put calculation expressions. Some common examples include:
Age in years: int((today() - date(#form/dob)) div 365.25)
Age in months: int((today() - date(#form/dob)) div 30.4)
Age in weeks: int((today() - date(#form/dob)) div 7)
Estimated Date of Delivery (EDD) from Last Menstrual Period (LMP): date(#form/lmp + 280)
Summation of three questions: int(#form/question_one) + int(#form/question_two) + int(#form/question_three)
Expressions in CommCare are evaluated left to right. The following example illustrates how this impacts expressions:
|
is equivalent to:
|
Should you wish to evaluate expressions differently you must use parentheses to indicate which elements should be evaluated together e.g.:
|
Display Logic for Hidden Values
You can put a display condition in a hidden value
The hidden value will hold the null value '' until its display condition is met. Once its display condition is met, it holds the calculated or loaded value.
If you update the case from a hidden value, it will only update the case if its display condition is met.
Simple Calculation Example
Icon
This is an example of a simple calculation where the Date of Birth is being calculated. The logic statement goes in the "Calculate Condition" of the Hidden Value.
Complex Calculation Example
Icon
This is an example of a hidden value where the Estimated Date of Delivery (EDD) is being calculated. The logic statement goes in the "Calculation Condition" of the Hidden Value.
Calculating Dates & Times
In CommCare you can do calculations with dates and times just like you would with numbers or strings, and there are a few CommCare Functions that are useful for doing so. The key ones explained here are:
| |
| For Web Apps it returns the current server date, but in the browser's time zone. E.g.:
|
| For Web Apps it returns the current server date/time value, but in the browser's time zone. E.g.:
|
More details about these functions, and other functions that may be useful for performing calculations with dates, can be found at https://dimagi.atlassian.net/wiki/x/VTLKfw.
For further guidance on doing calculations with dates and times, see https://dimagi.atlassian.net/wiki/x/lyTKfw.
Examples
Age in years (estimate):
int((today() - date(#form/dob)) div 365.25)
Age in years (precise):
if(format-date(today(), "%m%d") >= format-date(#form/dob, "%m%d"),
format-date(today(), "%Y") - format-date(#form/dob, "%Y"),
format-date(today(), "%Y") - format-date(#form/dob, "%Y") - 1)
Age in months:
int((today() - date(#form/dob)) div 30.4)
Age in months (by day of month)
Age in weeks:
Estimated Date of Delivery (EDD) from Last Menstrual Period (LMP):
This is what a DOB calculation might look like in the Form Builder; the logic statement goes in the "Calculate Condition" box:
This is an example of a hidden value where the Estimated Date of Delivery (EDD) is being calculated. The logic statement goes in the "Calculation Condition" box:
The following shows how to restrict a Date question's answer to be within a certain range:
Converting date values to type Date
One important concept to understand about dates in CommCare is that a date value can often stored in a string or numeric format, rather than as a Date type, but any date computations need to be performed on arguments that are actually of the type date. So when you have a date value that may be of type string or numeric rather than of type date, you can use date() to convert it before doing computations. Some examples:
When doing arithmetic on a date from a "Date" type question, you can do the arithmetic directly on that value since it's already a date. But at the end, you should convert the entire result to a date. So if #form/lmp is a "Date" type question, you can compute the EDD using the following expression: date(#form/lmp + 280)
When doing arithmetic on a date from a "Hidden Value", you need to convert the value stored in the hidden value to a date first, then do the arithmetic, and then convert the result to a date. So if #form/lmp is a hidden value that contains the LMP, then to compute the EDD you should use the following expression: date(date(#form/lmp) + 280)
Doing calculations with the now() function
In CommCare, you can use the now() function to get the current date and time.
To perform any computations using now(), make sure to consider the following:
Because the result of now() contains time data in addition to date data, you cannot do math or comparisons on this data type directly, and you also should not convert it to a date using date(), or the time portion will be dropped. Instead, you must first convert it to a numeric representation, using the function double(), and then do any desired computations.
Similarly, if you are saving the value of now() as a case property, you must convert it to a number before saving it as a case property, i.e. double(now()). This will convert the now() value to a number that represents the date/time in days since January 1, 1970. Saving now() directly will only save the date portion without the time portion.
As an example of doing computations using now(), suppose you want to compute the amount of time a user spends doing some activity/workflow. You could achieve this by doing the following:
In the form that represents the beginning of the workflow, you can capture the full date and time in a hidden value with the calculation:
Save that hidden value to the case as a case property
In the form that represents the end of the workflow, you can load the value you saved in the previous form (let's call it #form/start_time)
Then to compute the time elapsed, you would use:
The result is a decimal value in days/fractions of days. To convert to the # of hours (rounded down), you could use the following calculation:
To convert now() to a date and time in Excel
First convert it to a decimal using the "double()" function. The double function will convert the now() value to a number that represents the date/ time in days since 1/1/1970. This is similar to doing calculations on dates, when you use int() to convert a date value into number of days.
Save this as a case property or use it as a hidden value in form exports
After exporting this value to excel, you can convert it to a date and time by 1/1/1970 to it
In excel, if you want to take that value and show the current time (in UTC), you simply add it to 1/1/1970 to it.
Time values are stored as strings, and require similar manipulation to do any validation conditions or comparisons in calculations. For example, here is the validation on a 'time' question type if you wanted to make sure the answers were between 9am and 9pm:
int(substr(., 0, 2)) >= 9 and int(substr(., 0, 2)) < 21
To convert a time saved to a case into HH:MM AM/PM so it can be displayed in the app, create an additional hidden value with the following calculate condition:
if(int(substr(#form/time_value, 0, 2)) < 12, concat(substr(#form/time_value, 0, 5), ' AM'), if(int(substr(#form/time_value, 0, 2)) = 12, concat(substr(#form/time_value, 0, 5), ' PM'), concat(int(substr(#form/time_value, 0, 2)) - 12, substr(#form/time_value, 2, 5), ' PM')))
For more information on substr and other string functions, see CommCare Functions | substr.
When using double() with values from other questions to do calculations you may need to cast the question as a date. For example: double(date(#form/somequestion))
To extract a nicely-formatted time from now():
Create a hidden value called double_now to store the value of now():
#form/double_now
Create a hidden value called seconds_since_midnight to store only the time, represented as the number of seconds since midnight:
#form/seconds_since_midnight
You can then create an hours hidden value:
#form/hours
And a minutes hidden value:
#form/minutes
And a seconds hidden value:
#form/seconds
And finally display the time nicely:
#form/time_formatted
Note that this will display the time in 24-hour format and will not zero-pad, so 2:05:30 in the afternoon would display as 14:5:30. You can use if statements in the formatted time calculation to address this.
Date Formatting
To convert or format a date into another format (e.g. day of week, name of month, etc.) see the format-date function.
When you enter day, month, and year in as three separate numerical responses, you can convert those three responses into one date as follows
First, remember that dates are typically saved in the format YYYY-MM-DD
This means that the number entered for month and the number entered for day must be a two-digit value
To ensure that the format is correct, create hidden values for month and day using the calculate condition of: if(data/month<10, concat('0',data/month), data/month) or if(data/day<10, concat('0', data/day), data/day) to ensure that if someone types in a single-digit day, a zero will be added in front of it to create a two-digit number
To create the date using the three inputs, create another hidden value (we'll call this example calc_date), and use the following syntax:
if(data/day !='' and data/month !='' and data/year !='', concat(data/year, '-', data/hiddenvaluemonth, '-', data/hiddenvalueday), '')NOTE: the reason we have to use an if statement is because otherwise, you will return a type mismatch error. This will happen because until the day, month, and year are entered into their respective questions, the calc_day command will try to create a date but cannot because the values from those questions will be blank. To avoid that, the if statement tells the calc_date to only calculate the date if the day, month, and year questions are NOT blank. If they are blank, the if statement tells the calc_day to return a blank value.
Calculations using time question types
You can use time collected in forms to do different calculations, e.g. the number of hours between the time of birth and now. Some things to keep in mind when doing these calculations:
When you use "double(#form/dob)", the value you get back is a number like "16925", which is the number of days it's been since January 1st, 1970.
When you use "double(now())", the value is similar but contains a decimal, like "16925.50833333", which is the same number of days, but it also includes the time, represented as a fraction of a day. So "16925" is May 4th, 2016, and doesn't include time information. Whereas "16925.5" is May 4th, 2016, at noon (halfway through the day), and "16925.75" is May 4th, 2016 at 6:00 p.m. "16925.50833333" happens to be May 4th, 2016 at 12:12 p.m.
To perform calculations with the results of date & time questions and now(), you need to first convert the values from the questions into decimals, in order for them to be compatible with double(now()).
In the following example, we show how to compute the number of hours since time of birth (this is done assuming that you are collecting date and time as 2 different question types to trigger different calculations; similar calculations can also be triggered using the date/time question type, which collects both together):
Collect date and time of birth in date and time questions in your form (call these #form/dob and #form/time_of_birth)
Calculate minutes since midnight for the time of birth (call this #form/minutes_since_midnight_birth):
Convert minutes since midnight into a decimal, by dividing it by the total number of minutes in a day (call this #form/time_of_birth_decimal):
Sum that numeric time with a numeric version of the dob. This will get you to a numeric datetime value like 16925.50833333 (call this #form/birth_datetime):
Then, in the form where you need the hours calculation, subtract your datetime from double(now()) and convert to hours (call this #form/hours):
If you wish, you can also use the round() function to output a rounded # of hours:
Rounding Numbers
You can put the relevant expression into a hidden value that represents the integer form of the value.
Round a number down to the nearest integer
If you want to round a number down you can use the int() function to cut off the decimal point (i.e. 2.2 -> 2; 2.7 -> 2)
int(#form/value)
Round a number up to the nearest integer
Use the following calculation to always round a number up (i.e. 2.2 -> 3; 2.7 -> 3)
If( int(#form/value) < #form/value, int(#form/value) + 1, int(#form/value))
This compares whether the decimal form of the lowest integer is smaller than the current value, and if so, rounds up, and otherwise truncates the value.
Round a number up from .5, otherwise down
As of CommCare 2.19, the "round" function is available. Simply write round(#form/value). Read more here.
You can also use the int() function in combination with multiplying the input by 2, adding 1, and then dividing by 2 (i.e. 2.2 -> 2; 2.7 -> 3)
int ( ( #form/value*2 +1 ) div 2 )
To round to a different number of decimal places, appropriately adjust the constant '2' in the preceding calculation. The constant should be twice the inverse of the desired precision. For example, for a precision of 0.1, constant = 1 / precision * 2 = 1 / 0.1 * 2 = 20. Consequently use int ( ( #form/value*20 +1 ) div 20 ).
Round a number to a certain decimal place
You can use the same principles to round to the nearest .1, .01, .001, etc. with the following formula:
Round to nearest decimal: round(#form/value*10) div 10
To round to two decimals change the 10's in the formula above to 100. To three decimals change them to 1000, etc.
Example:
round( 1.145 * 10 ) div 10 = 1.1
round( 1.145 * 100 ) div 100 = 1.15
Join two values ("concatenate")
You can join two or more values in CommCare using "concatenate."
Create a hidden value that joins together the items- for example item1, item2, item3
concat(#form//item1,#form//item2,#form//item3)
Examples
This example of the concatenate function takes a "first name" and "surname" and puts them into a value called #form//fullname. The quotations ' ' put a space between the two values. This would output a name as "John Snow" instead of "JohnSnow."
If Statements in Calculations
There are certain scenarios when you need to create more complex logic (than available in the built in logic builder (e.g. X statement is equal to Y value)) or more complex calculations. One option is to create a custom IF statement. An IF statement follows the construction, IF this, THEN this, OTHERWISE this. The format to express this kind of logic is the following: IF(evaluative statement, result if statement is true,result if statement is false).
For example, you could use an IF statement to conditionally store a value in a Hidden Value. Let's take an example where you register a client (a case) in a registration form and update the case property birth_facility. Later in the Follow Up form you want to update the case property birth_facility to the value of the question birth_facility_new IF the answer to change_facility is 'yes'; otherwise you want to leave it as the original value from the registration form. You would do the following:
Make sure that in the registration form you save birth_facility to the case
In the follow up form create a second hidden value called birth_facility and enter an if statement in the "calculate" block: if(#form/change_facility = 'yes', #form/birth_facility, #case/birth_facility)
If "And" Statement
You might have multiple conditions for an IF statement, where you need to use the OR or AND functions. Below are examples that demonstrate how you can write your expressions. Note that there is no need to enter IF twice for each argument.
Example with AND: if(#form/sys_bp >= 120 and #form/sys_bp <=139, 1, 0) where 1 refers to high risk
If "Or" Statement
Example with OR: if((#form/gender = "male" and #form/waist_circumference > 90) or (#form/gender = "female" and #form/waist_circumference > 85), 1, 0) where 1 refers to a high risk
Argh! I can't figure out what the mistake I made is!
Icon
You might get a generic error message and not understand why your complex statement is not working. Here are some common sticking points:
Check all of your parentheses and quotation marks! They must all be in pairs. If you can't find it, try copying the statement into a tool like Notepad++ or Sublime Text as these tools can help you find missing parentheses
Check your spellings!
Anytime you use "and" or "or" they must be lowercase
Create a Score Tally
Sometimes it can be useful to create a tally of the response to a series of questions. For example, some simple diagnostics tools tally responses to generate a preliminary diagnosis. The easiest way to do this is to set the item values to a numeric value.
Click below to expand and learn more
For example, if you have the question: " How do you feel today?" The choices could be coded as:
0- Excellent
1- Fair
2- Sick
In this case, maybe a higher score on several different questions could trigger the need to referral. You would create a hidden value (i.e. #form/total_score) that sums the value of all the relevant questions. This will result in a numeric value.
A second hidden value (i.e. #form/preliminary_diagnosis) could help to interpret these values by using nested if statements to set the value of preliminary_diagnosis. For example, the calculation could read:
if(#form/total_score > 14, "high_risk", if(#form/total_score > 9, "monitor", "okay"))
You could also use the output of either total_score or preliminary_diagnosis to determine the display condition for relevant questions or labels.
This becomes more complicated if a question has to be coded with something other than numbers for item values, or if there are two possible answers that have the same numeric value. In these cases, the checklist and weighted checklist functions are really useful.
If your scoring is too complex even for these functions, you will need to create a hidden value that corresponds to each question and interprets the answers as a numeric value.
Generate a Random Number
With the random() function, you can create randomized numbers from within your CommCare application. This is helpful when usign CommCare for research studies, for example.
Return: Returns a random number between 0.0 (inclusive) and 1.0 (exclusive) (ex. 0.738).
Arguments: None
Usage: random()
Example Usage: When you need to generate a random number.
For example, to generate a number between 5 (inclusive) and 23 (exclusive), you can use (random()*(23 - 5)) + 5. This will be something like 12.43334.
You can convert that to a whole number by using int((random()*(23 - 5)) + 5). You can also reference questions instead of directly typing numbers.
Ex. int(random()*(#form/high_num - #form/low_num) + #form/low_num).
The output varies between low_num (inclusive) and high_num (exclusive)
You can generate either a random decimal number (ex. 8.9) or a random integer (ex. 22) using the random() function. Set the calculate expression of your question to the following:
For a random decimal: start_num + (random() * (end_num - start_num)) For example, to generate random number between 10.3 and 30.9, you can use 10.3 + (random() * (30.9 - 10.3))
For a random integer: start_num + int(random() * (end_num - start_num)) For example, to generate random number between 9 and 22, you can use 9 + int(random() * (22 - 9))
You can also replace start_num and end_num with references to other questions in your form (ex. #form/my_start_range_question). Please make sure that they are required or have value so that the calculate expression works.
Notes on the random() Function:
If random() is used within a calculate or display condition, the value will change and be recalculated many time as the form is filled out
To generate a random number once when the form is opened, add a hidden value and then put the random() logic inside the Default Value logic of the hidden value (its in Advanced Section).
If you're using random() to generate or calculate number that is shown to the user, then its best practice to put random() inside of the default value section as described above.
Generate a Unique ID
Encrypt a string
Sometimes it would be useful to encrypt a value in a CommCare form and store the encrypted result. A typical reason would be that you have a value that is both a key in a database outside of CommCare, such as an external case ID, and one that contains personally identifiable information (PII). You would not want to pass the value itself through a third party, but you might be able to pass an encrypted value. For example, you might pass an encrypted case ID to third party visualization software that is permitted to access your database for approximate location information. The visualization software cannot decrypt the case ID, but your system can perform the decryption, access the database and return associated non-PII data.
CommCare only implements a form of encryption with shared secret keys, so it is important to realize that this does not hide any information from CommCare itself, which will have a copy of the key. If you need to store data in CommCare that even CommCare can't decrypt, then perform the encryption before the data is entered into a form. Please note that this encryption functionality will be available after CommCare's mobile 2.51 release.
Encryption in a form
The CommCare encryption function is called 'encrypt-string' and a call to it looks like this:
|
All three arguments are strings. The first string is a message to be encrypted. The second string is a key suitable for the chosen encryption method. The third string is the name of the encryption method. Currently, only one encryption method named 'AES' is implemented, so it will be assumed for the rest of this documentation.
AES-GCM with 256-bit key
AES is an encryption standard that is implemented in many software libraries. It has several settings that affect how encryption and decryption work. For the encrypt-string function, CommCare uses the following settings:
encryption uses the GCM mode, so decryption must use the same mode
the function requires a 256-bit key, which you should produce with a good random number generator and store so you can decrypt messages later. The key passed to encrypt-string as the second argument should be encoded via Base64, so that it contains no unprintable characters.
encrypt-string chooses an initialization vector to improve security of repeated message encodings. The initialization vector is part of the encrypted output because it is needed for decryption.
Example
We will walk through an example of using encrypt-string on a message and decrypting it. In this example, we show code that you would run outside of CommCare using the Python language, but you can use any language with an implementation of AES.
First, produce a 256-bit AES key
You will need to generate a key that is shared between your system and CommCare. Treat this key like a password because anyone who knows it can read the encrypted values.
The following Python code produces a 256-bit key, encoded in Base64, which is suitable for encrypt-string. There are many ways to produce a random 256-bit value. The following code uses a system-supplied random number seed and produces a large random integer that is converted to bytes and then Base64 encoded.
|
The value of encoded_key is a 'bytes' object with a value like b'VP1m9MQs8UZeaa2h+NkNqqbPkxBSFxYQNe9imEWl7tk='. Copy that value (without the initial b) and use it as the second argument to encrypt-string.
Second, encrypt a value in your form
The actual encryption will happen within your CommCare application. Within a form, you will compute a value using encrypt-string in a calculation like this:
|
Here is a sample form in XML that calls encrypt-string and displays the result. You can upload the form to an application to try it yourself. Here is a picture of it in CommCare Web Apps:
The result of encrypt-string is a Base64 encoded string containing a sequence of N bytes. Your form will probably just store the whole string, but decoding it will require knowing the detailed format of the bytes, as follows:
Byte 0: the first byte is the length of the initialization vector. Call this IV_LEN, which will be between 1 and 255.
Bytes 1 to IV_LEN: the initialization vector, which will be used in the decryption code.
Bytes IV_LEN + 1 to N - 16: the encrypted message bytes.
Bytes N - 16 to N - 1: the last 16 bytes are an AES-GCM message authentication tag.
Different implementations of AES use different default initialization vector lengths, so it is necessary to encode the length as part of the result. Typical vectors are either 12 or 16 bytes long.
Third, decrypt a value outside of CommCare
The software that decrypts values will run in your own system outside of CommCare. For example, it might run in a server that accesses your database in response to requests from a third party. That software will use the shared key to decode the original value. The following Python code demonstrates the steps required.
|
When using the encrypt-string function, it will be very important to test your decryption code with real examples of values encoded by CommCare, including testing the CommCare mobile application and Web Apps if you use both, because the cryptography implementations used in Android and CommCare Web Apps are different.
Incrementing a counter
Overview
Technique for keeping track of the number of times a form has been filled out. This is often used to count the number of visits or other type of interaction.
It may be useful to increment counters in a case values, the typical example being the number of visits or consultations a patient received. The case stores the number of consultations received and each form stores a number of the consultation received at that time. Such counters are implemented with a couple of hidden values in the form and a single case value.
General Approach
We will walk through a very simple example where we count the number of times that a home visit form has been completed.
Set up the Form
The visit form will need to have a hidden value.
Hidden value name | Description | Calculate Condition | Explanation of Calculation Condition |
---|---|---|---|
count | Each time the form is opened, this hidden value will calculate how many times the form has been filled out | coalesce(#case/visit_count, 0) + 1 | This hidden value will use coalesce to determine whether the case property visit_count has any value. If it does, it will add 1 to it, if it does not then it will take 0 and add 1 to that. You can imagine the first time the form is completed it will add 1 to 0 = 1 The second time it will take the case property value (1) and add 1 = 2 And so on... |
CommCare Functions: Coalesce
Icon
This example uses a function called coalesce. You can read more at https://dimagi.atlassian.net/wiki/x/VTLKfw but here is the basic information about how this function works:
coalesce
Behavior: Useful for choosing which of two values to return. Will return the non-empty value. If both are not null, will return the first argument.
Return: One of the values
Arguments: The two values to be coalesced
Syntax: coalesce(value_1, value_2).
Example: This is useful if you want to use a default value when referring to a question which may or may not have been answered. Ex. coalesce(#form/my_question, "my default value").
Set up the Case Management
After you form is set up you will need to link your case properties. You will want to take the value of home_visits each time, add one, and save it to the case again.
Example: Counting ANC Visits
Let's take an example of Ante Natal Care (ANC) visits.
Create a hidden value (
anc_number)
to compute the current ANC numberby
adding 1 to the case propertyanc_number
Since the case property
anc_number
can be blank in the case of a first ANC, we need to use the coalesce function:coalesce(#case/anc_number, 0) + 1
Explained Step by Step
To count or keep track of ANC visits in follow-up forms, do this:
1. Create Hidden Value anc_number
2. Set up your case management to save anc_number to the case.
3. In the form question anc_number, in the calculate condition, type: coalesce(#case/anc_number,0)+1