Date and Time Reference
Survey users frequently make mistakes with dates. This page documents exactly how MQUAD parses and calculates dates, datetimes, and plain times — based on the frozen calculation-function contract.
Formats
| Kind | Canonical format | Example |
|---|---|---|
| Date | YYYY-MM-DD | 2026-08-14 |
| Datetime | ISO 8601 | 2026-08-14T09:00:00Z |
| Plain time | HH:MM or HH:MM:SS | 09:30, 09:30:45 |
Accepted date formats
Expressions accept:
- ISO:
2026-08-14 - Slashed/dashed, day-first:
14/08/2026,14-08-2026are treated as 14 August, not 14 August US-style. MQUAD reads03/08/2026as 3 August.
⚠ Always use
YYYY-MM-DDin your own expressions. It is the only format that is unambiguous on every platform.
Date values are calendar dates
A date-only value is a calendar date, not a moment in time. MQUAD performs
date arithmetic in calendar space so results never shift across time zones or
daylight-saving boundaries. A date in the survey is stored and compared as
YYYY-MM-DD.
date-diff(unit, start, end)
Completed calendar units between two dates. Signed — reversing the arguments negates the result.
| Unit | Meaning |
|---|---|
'days' | calendar days (end − start) |
'weeks' | completed weeks = floor(days / 7) |
'months' | completed months |
'years' | completed years |
Singular and plural forms both work ('day' / 'days').
Completed months
Months are counted as:
months = (endY − startY) × 12 + (endM − startM)
if end day < start day, subtract 1The comparison uses the start date's day-of-month. Examples (from the frozen contract):
| Expression | Result | Why |
|---|---|---|
date-diff('months', '2026-01-31', '2026-02-28') | 0 | Feb 28 < Jan 31, so not one full month |
date-diff('months', '2026-01-31', '2026-03-31') | 2 | two full months |
date-diff('months', '2024-02-29', '2024-03-31') | 1 | 31 ≥ 29 |
date-diff('months', '2026-02-28', '2026-01-31') | -1 | reversed, signed |
Completed years
years = endY − startY
if endM < startM, or (endM = startM and endD < startD), subtract 1| Expression | Result |
|---|---|
date-diff('years', '2025-01-01', '2026-08-14') | 1 |
date-diff('years', '2025-08-20', '2026-08-14') | 0 (birthday not yet reached) |
date-diff('years', '2000-08-20', '2026-08-20') | 26 |
A leap-day birthday: born 2000-02-29, reference 2026-02-28 gives 25
(Feb 28 is before Feb 29); reference 2026-03-01 gives 26.
Weeks and days
date-diff('days', '2026-08-01', '2026-08-14') = 13.
date-diff('weeks', '2026-01-01', '2026-08-14') = 32
(225 days ÷ 7 = 32.14 → 32 completed weeks).
date-add / date-subtract
Calendar arithmetic with end-of-month clamping: if the resulting month has fewer days than the original day, the result clamps to the last day.
| Expression | Result | Why |
|---|---|---|
date-add('2026-01-31', 1, 'months') | 2026-02-28 | Feb has no 31st |
date-add('2024-01-31', 1, 'months') | 2024-02-29 | leap year |
date-add('2026-08-31', 1, 'months') | 2026-09-30 | Sep has no 31st |
date-add('2024-02-29', 1, 'years') | 2025-02-28 | leap-day clamped |
date-subtract('2026-03-31', 1, 'months') | 2026-02-28 | same clamping on subtract |
date-add('2026-08-14', 3, 'days') | 2026-08-17 | |
date-add('2026-08-14', 1, 'weeks') | 2026-08-21 |
date-subtract(date, amount, unit)is exactlydate-add(date, −amount, unit).amountmay be negative (equivalent to subtracting).- A non-numeric amount yields
null(see Blank and Invalid Values).
time-diff(unit, start, end)
Three cases, frozen:
| Inputs | Behaviour | Example |
|---|---|---|
Both plain times (HH:MM) | Clock arithmetic, end − start. No midnight rollover | time-diff('hours', '09:00', '10:30') = 1.5; time-diff('hours', '23:00', '01:00') = -22 |
| Both datetimes (ISO) | Instant arithmetic, UTC-normalized | time-diff('hours', '2026-08-14T09:00:00Z', '2026-08-14T10:30:00Z') = 1.5 |
Both date-only (YYYY-MM-DD) | Treated as datetimes at UTC midnight | time-diff('days', '2026-08-13', '2026-08-14') = 1 |
| Mixed plain-time + datetime/date | null | time-diff('hours', '09:00', '2026-08-14T10:00:00Z') = null |
Units: 'seconds', 'minutes', 'hours', 'days' (singulars and s/m/h/d
also accepted). Seconds and minutes are integers; hours and days are floats.
The "no midnight rollover" rule is important: 23:00 → 01:00 is -22 hours,
never +2. If you need elapsed time across midnight, supply datetimes
(with a date) instead of plain times.
age / age-months / age-days
Completed calendar units from a date of birth to a reference date, clamped to
0 when the reference is on or before the DOB (future birthdate).
age('2000-08-20', '2026-08-14') → 25
age('2000-08-20', '2026-08-20') → 26 (birthday reached)
age('2000-02-29', '2026-02-28') → 25 (Feb 28 < Feb 29)
age('2027-01-01', '2026-08-14') → 0 (future DOB → clamped)
age-months('2000-08-20', '2026-08-14') → 311
age-days('2000-08-20', '2026-08-14') → 9490The reference date can be an ordinary survey date question (${interview_date})
or today().
Gestational functions
Based on LMP → reference date (not due date), clamped to 0:
gestational-weeks('2026-04-20', '2026-08-14') → 16 (116 days ÷ 7)
gestational-days('2026-04-20', '2026-08-14') → 4 (116 mod 7)
gestational-total-days('2026-04-20', '2026-08-14') → 116gestational-weeks= whole weeks since LMP.gestational-days= the 0–6 remainder days ("16 weeks + 4 days").gestational-total-days= the raw calendar-day count.
day / month / year / day-of-week
Component extraction from a date:
| Expression | Result |
|---|---|
day('2026-08-14') | 14 |
month('2026-08-14') | 8 |
year('2026-08-14') | 2026 |
day-of-week('2026-08-14') | 5 (Friday) |
day-of-week('2026-08-10') | 1 (Monday) |
day-of-week('2026-08-16') | 7 (Sunday) |
day-of-week follows ISO 8601: 1 = Monday … 7 = Sunday — it is not the
JavaScript convention (where Sunday is 0).
today() and now()
today()returns the device's local calendar date (YYYY-MM-DD).now()returns the current datetime as ISO 8601.
today() is a live value — a calculation using today() re-evaluates when
dependencies change, but otherwise reflects when it was evaluated.
days-between and duration (legacy)
These older functions are unchanged for backward compatibility, with two cautions:
days-between(d1, d2)— argument order differs between the web runtime (d2 − d1) and mobile/backend (d1 − d2). Preferdate-diff('days', start, end), which is consistent and signed everywhere.duration(end, start, [unit])— computesend − start; default unit'hours'. Units'seconds'/'minutes'/'hours'/'days'. This one is consistent across platforms.
Time zone summary
| Value type | Semantics |
|---|---|
Date-only (YYYY-MM-DD) | Calendar date; arithmetic in calendar space; never shifts across DST |
| Datetime (ISO) | Instant; normalized to UTC for time-diff arithmetic |
Plain time (HH:MM) | Clock arithmetic; no date implied, no rollover |
today() | Device-local calendar day |
now() | Instant, ISO 8601 |
See Blank and Invalid Values for what happens when a date is missing, malformed, or out of order.
