Date and Time Reference

How MQUAD handles dates, datetimes, and plain times in expressions — formats, completed units, month-end clamping, and time-diff rules

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

KindCanonical formatExample
DateYYYY-MM-DD2026-08-14
DatetimeISO 86012026-08-14T09:00:00Z
Plain timeHH:MM or HH:MM:SS09:30, 09:30:45

Accepted date formats

Expressions accept:

  • ISO: 2026-08-14
  • Slashed/dashed, day-first: 14/08/2026, 14-08-2026 are treated as 14 August, not 14 August US-style. MQUAD reads 03/08/2026 as 3 August.

⚠ Always use YYYY-MM-DD in 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.

UnitMeaning
'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 1

The comparison uses the start date's day-of-month. Examples (from the frozen contract):

ExpressionResultWhy
date-diff('months', '2026-01-31', '2026-02-28')0Feb 28 < Jan 31, so not one full month
date-diff('months', '2026-01-31', '2026-03-31')2two full months
date-diff('months', '2024-02-29', '2024-03-31')131 ≥ 29
date-diff('months', '2026-02-28', '2026-01-31')-1reversed, signed

Completed years

years = endY − startY
if endM < startM, or (endM = startM and endD < startD), subtract 1
ExpressionResult
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.

ExpressionResultWhy
date-add('2026-01-31', 1, 'months')2026-02-28Feb has no 31st
date-add('2024-01-31', 1, 'months')2024-02-29leap year
date-add('2026-08-31', 1, 'months')2026-09-30Sep has no 31st
date-add('2024-02-29', 1, 'years')2025-02-28leap-day clamped
date-subtract('2026-03-31', 1, 'months')2026-02-28same 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 exactly date-add(date, −amount, unit).
  • amount may be negative (equivalent to subtracting).
  • A non-numeric amount yields null (see Blank and Invalid Values).

time-diff(unit, start, end)

Three cases, frozen:

InputsBehaviourExample
Both plain times (HH:MM)Clock arithmetic, end − start. No midnight rollovertime-diff('hours', '09:00', '10:30') = 1.5; time-diff('hours', '23:00', '01:00') = -22
Both datetimes (ISO)Instant arithmetic, UTC-normalizedtime-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 midnighttime-diff('days', '2026-08-13', '2026-08-14') = 1
Mixed plain-time + datetime/datenulltime-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')     → 9490

The 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')   → 116
  • gestational-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:

ExpressionResult
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). Prefer date-diff('days', start, end), which is consistent and signed everywhere.
  • duration(end, start, [unit]) — computes end − start; default unit 'hours'. Units 'seconds'/'minutes'/'hours'/'days'. This one is consistent across platforms.

Time zone summary

Value typeSemantics
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.