# Date and Time Reference {#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 {#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 {#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 {#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)` {#date-diffunit-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 {#completed-months}


Months are counted as:

```text
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):

| 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 {#completed-years}


```text
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 {#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` {#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 exactly `date-add(date, −amount, unit)`.
- `amount` may be negative (equivalent to subtracting).
- A non-numeric amount yields `null` (see
  [Blank and Invalid Values](/user-manual/blank-and-invalid-values.md)).

---


## `time-diff(unit, start, end)` {#time-diffunit-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` {#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).

```text
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 {#gestational-functions}


Based on **LMP → reference date** (not due date), clamped to `0`:

```text
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` {#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-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) {#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 {#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](/user-manual/blank-and-invalid-values.md) for what happens
when a date is missing, malformed, or out of order.
