Auto-Populate and Calculations
Calculation expressions derive a value from other survey answers. They are re-evaluated whenever any referenced answer changes, so the derived value stays current even if the respondent goes back and edits an earlier answer.
There are two ways to use a calculation:
| Mode | Question type | Behaviour |
|---|---|---|
| Calculated Value | calculate | A read-only computed result. Displayed as text; respondents cannot edit it. |
| Auto-Populate | any other type (text, integer, decimal, select_one, date, ...) | A pre-filled value that the respondent can override. Re-computed whenever dependencies change. |
Both modes use the exact same expression syntax and function set.
Not the same as Default Values. A default value is evaluated once, at form start, and never changes. A calculation re-runs on every dependency change. See Default Values and the comparison table below.
Where to configure it
- Open your survey in the Survey Designer.
- Select the question in the canvas.
- In the right-hand Properties panel, scroll to the Advanced section.
- Click Calculated Value (for a
calculatequestion) or Auto-Populate (for any other type).
The Calculation Editor provides:
- Available Variables — questions before the current one. Click to insert
a
${variable}reference. - Functions — the built-in function catalog. Click to insert the function name and an opening parenthesis.
- Formula textarea — type or paste your expression. It validates as you type (unknown variables and unbalanced parentheses are flagged).
- Quick Insert — operator buttons for
+ - * / ( ) = > < >= <= and or.
The editor only offers earlier questions as variables — a calculation cannot reference a question that appears later in the form.
Referencing other questions
Use ${question_name} to reference another question's answer:
${quantity} * ${unit_price}multiplies the quantity by the unit price. The reference uses the question's variable name — never its label.
Arithmetic operators
| Operator | Meaning | Example |
|---|---|---|
+ | addition | ${q1} + ${q2} |
- | subtraction | ${total} - ${discount} |
* | multiplication | ${qty} * ${price} |
div | division | ${total} div ${count} |
/ | division | ${total} / ${count} |
mod | remainder | ${items} mod 2 |
div and / both divide; mod gives the remainder.
Comparison and logical operators
Calculations can produce boolean values too:
| Operator | Meaning | Example |
|---|---|---|
=, == | equals | ${pregnant} = 'yes' |
!= | not equals | ${pregnant} != 'yes' |
>, >=, <, <= | comparisons | ${age} >= 18 |
and | logical AND | ${age} >= 15 and ${age} <= 49 |
or | logical OR | ${district} = 'A' or ${district} = 'B' |
not(...) | logical NOT | not(${pregnant} = 'yes') |
Use parentheses to group: (${a} = 1 or ${b} = 1) and ${c} = 1.
The calculation function set
MQUAD implements a frozen set of calculation functions identically across the web designer/runtime, the Flutter mobile app, and the form-service backend. Each function's exact semantics (argument order, units, null behaviour) are defined in the Date and Time Reference and the Expression Reference. Below is the summary of every public calculation function, grouped by purpose.
Date calculations
| Function | Purpose | Example |
|---|---|---|
date-diff(unit, start, end) | Completed calendar units between two dates (signed) | date-diff('months', ${enrollment}, ${visit}) |
date-add(date, amount, unit) | Add calendar units with month-end clamping | date-add(${visit_date}, 14, 'days') |
date-subtract(date, amount, unit) | Subtract calendar units with month-end clamping | date-subtract(${visit_date}, 14, 'days') |
day(date) | Day of month, 1–31 | day(${visit_date}) |
month(date) | Month, 1–12 | month(${visit_date}) |
year(date) | Four-digit year | year(${visit_date}) |
day-of-week(date) | ISO day of week, 1=Mon…7=Sun | day-of-week(${visit_date}) |
Units for date-diff / date-add / date-subtract are string literals:
'days', 'weeks', 'months', 'years' (singular forms also accepted).
Time calculations
| Function | Purpose | Example |
|---|---|---|
time-diff(unit, start, end) | Difference between two plain times or two datetimes | time-diff('hours', ${start_time}, ${end_time}) |
Units: 'seconds', 'minutes', 'hours', 'days'.
Age
| Function | Purpose | Example |
|---|---|---|
age(dob, ref) | Completed years at the reference date | age(${dob}, ${interview_date}) |
age-months(dob, ref) | Completed months | age-months(${dob}, ${visit_date}) |
age-days(dob, ref) | Total calendar days | age-days(${dob}, ${visit_date}) |
All age functions return 0 (never negative) when the reference date is on or
before the date of birth.
Pregnancy / gestational age
| Function | Purpose | Example |
|---|---|---|
gestational-weeks(lmp, ref) | Whole weeks since LMP | gestational-weeks(${lmp}, ${visit_date}) |
gestational-days(lmp, ref) | Remaining days (0–6) | gestational-days(${lmp}, ${visit_date}) |
gestational-total-days(lmp, ref) | Total days since LMP | gestational-total-days(${lmp}, ${visit_date}) |
The reference point is the LMP (last menstrual period) → reference date —
not a due date. Values are clamped to 0 if the reference is before LMP.
Health
| Function | Purpose | Example |
|---|---|---|
bmi(weightKg, heightCm) | Body mass index | bmi(${weight_kg}, ${height_cm}) |
Height is always in centimetres under the frozen contract:
bmi(70, 175) = 70 / (175/100)² ≈ 22.86. Do not pass metres
(bmi(70, 1.75) is wrong — it returns a huge value).
Existing date/time functions (unchanged, legacy)
| Function | Purpose | Example |
|---|---|---|
today() | Current calendar date (device-local) | today() |
now() | Current datetime, ISO 8601 | now() |
date(value) / date(y, m, d) | Parse or construct a date | date('2026-08-14') |
days-between(d1, d2) | Days between two dates | days-between(${visit_1}, ${visit_2}) |
duration(end, start, [unit]) | Time difference, default hours | duration(${end}, ${start}, 'minutes') |
today-offset(days) | Date days from today | today-offset(-10) |
format-date(value, fmt) | Format a date with a pattern | format-date(${visit_date}, '%d/%m/%Y') |
days-betweenargument order — cross-platform caution. The legacydays-between(d1, d2)computesd2 − d1on the web runtime, butd1 − d2on the mobile app and form-service backend. Usedate-diff('days', start, end)instead — it is frozen, consistent everywhere, and signed (date-diff('days', ${end}, ${start})is the negation ofdate-diff('days', ${start}, ${end})).
durationargument order — end first!duration(end, start, [unit])computesend − starton all platforms. It is notduration(start, end).duration(${end_time}, ${start_time})gives a positive duration.
Numeric functions
| Function | Purpose | Example |
|---|---|---|
sum(a, b, ...) | Sum of values | sum(${q1}, ${q2}, ${q3}) |
min(a, b, ...) | Minimum | min(${q1}, ${q2}) |
max(a, b, ...) | Maximum | max(${q1}, ${q2}) |
round(v, n) | Round to n decimals | round(${result}, 2) |
abs(v) | Absolute value | abs(${change}) |
int(v) | Truncate to integer | int(${value}) |
number(v) | Convert to number | number(${text_value}) |
pow(base, exp) | Power | pow(${radius}, 2) |
String functions
| Function | Purpose | Example |
|---|---|---|
concat(a, b, ...) | Join text | concat(${first_name}, ' ', ${last_name}) |
join(sep, a, b, ...) | Join with separator | join('-', ${state}, ${district}) |
string(v) | Convert to string | string(${age}) |
string-length(v) | Length of string | string-length(${name}) |
substring(s, start, end?) | Extract substring (web) | substring(${code}, 0, 3) |
contains(s, sub) | Contains substring | contains(${notes}, 'urgent') |
starts-with(s, prefix) | Starts with prefix | starts-with(${id}, 'MH-') |
ends-with(s, suffix) | Ends with suffix | ends-with(${email}, '@example.com') |
replace(s, search, repl) | Replace all occurrences | replace(${text}, '-', '') |
regex(v, pattern) | Test regex match | regex(${phone}, '^[0-9]{10}$') |
substring/substr— cross-platform caution. On the web runtime,substring(s, start, end?)uses JavaScript semantics (0-basedstart, andendis an end index, not a length). The mobile runtime instead exposessubstr(s, start, length?)(0-basedstart, third argument is a length), and the form-service backend implementssubstringwith XPath-style 1-based indexing. Prefercontains(),starts-with(), andstring-length()for cross-platform logic, or testsubstringon every platform you deploy to.
Selection functions
| Function | Purpose | Example |
|---|---|---|
selected(value, choice) | Is the choice selected? | selected(${symptoms}, 'fever') |
count-selected(value) | Number of selected options | count-selected(${symptoms}) |
selected-at(value, index) | Choice at 0-based index | selected-at(${symptoms}, 0) |
count(value) | Count items | count(${multi}) |
Conditional functions
| Function | Purpose | Example |
|---|---|---|
if(cond, then, else) | Conditional value | if(${age} >= 18, 'adult', 'child') |
coalesce(a, b, ...) | First non-empty value | coalesce(${phone_2}, ${phone_1}, 'none') |
true() / false() | Boolean constants | if(${pregnant} = 'yes', true(), false()) |
Use
if(), not?:. Theif(condition, then, else)function is the supported conditional in calculations on every platform. The? :ternary syntax exists only in the Flutter expression engine and will not parse on the web runtime — do not use it in calculations.
Utility functions
| Function | Purpose | Example |
|---|---|---|
uuid() | Random UUID v4 | uuid() |
random() | Random number 0–1 | random() |
once(value) | First computed value only (page lifetime) | once(now()) |
Common calculation patterns
Total score:
${q1} + ${q2} + ${q3} + ${q4}Weighted score:
(${q1} * 2) + (${q2} * 3) + (${q3} * 1)Age from DOB (recommended):
age(${dob}, ${interview_date})interview_date in this example is an ordinary date question you create in
the survey (for example "Interview date"). MQUAD does not create or infer an
interview date automatically — you must supply it as a question and reference
it explicitly.
Age with today's date:
age(${dob}, today())today() is evaluated when the calculation runs, so this age updates daily.
The difference from the previous example: interview_date is fixed once the
respondent answers it; today() always reflects the current date.
Conditional discount:
if(${member_status} = 'Premium', ${total} * 0.2, 0)Composite ID:
concat(${site_code}, '-', ${participant_num})Estimated due date (a generic date calculation — not medical advice):
date-add(${lmp}, 280, 'days')Calculation vs Default Value
| Auto-Populate (calculation) | Default Value | |
|---|---|---|
| When evaluated | Every time dependencies change | Once, at form start |
| Reactive | Yes — updates when referenced values change | No — static after form start |
| User override | Yes — if the field is editable | Yes — if the field is editable |
| Reference syntax | ${question_name} | #question_name |
| Can call functions | Yes | No — default grammar has no function calls |
| Best for | Dynamic computed values | Initial static values |
Calculations in XLSForm
Use the calculation column:
| type | name | label | calculation |
|---|---|---|---|
calculate | total_score | Total Score | ${q1} + ${q2} + ${q3} |
integer | estimated_total | Estimated Total | ${q1} * ${q2} |
When importing, all calculations are preserved and visible in the survey designer. See XLSForm Mapping for details.
Tips
- Use Calculated Value (
type = calculate) for read-only results. - Use Auto-Populate on normal question types for editable pre-filled values that should stay current.
- For static initial values that never need to update, use Default Values instead.
- A calculation that references an unanswered question yields
null/blank — see Blank and Invalid Values. - Calculations run identically on web forms, the mobile field app, and the
backend for the frozen function set above. Platform-specific differences are
flagged inline (
days-between,substring, ternary).
