value string required ISO 8601 duration, at most 64 characters: P[nY][nM][nW][nD][T[nH][nM][nS]], optionally signed. Uppercase designators; a decimal fraction only in the smallest component present.
Parse ISO 8601 durations, add them with documented end-of-month clamping, break down the gap between two dates, and humanize a span in any locale.
Authoritative reference data or standards computation
Validates the P-form grammar and returns every component plus which units are nominal, calendar or exact. Whole-duration second totals are reported only as a labelled approximation.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
value
required
| string | allowed: ≤ 64 example: P1Y2M3DT4H5M6S | ISO 8601 duration, at most 64 characters: P[nY][nM][nW][nD][T[nH][nM][nS]], optionally signed. Uppercase designators; a decimal fraction only in the smallest component present. |
value string required ISO 8601 duration, at most 64 characters: P[nY][nM][nW][nD][T[nH][nM][nS]], optionally signed. Uppercase designators; a decimal fraction only in the smallest component present.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: value,normalized | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: approximate | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
value | string | The validated input duration, unchanged. | P1Y2M3DT4H5M6S |
normalized | string | Canonical re-rendering with zero components dropped and a full stop as decimal sign; an all-zero duration becomes PT0S. | P1Y2M3DT4H5M6S |
negative | boolean | True when the duration carries a leading '-'. A duration whose components are all zero is never reported as negative. | false |
years | float | Unsigned magnitude of the years component; 0 when absent. Nominal unit. | 1 |
months | float | Unsigned magnitude of the months component; 0 when absent. Nominal unit. | 2 |
weeks | float | Unsigned magnitude of the weeks component; 0 when absent. Never combined with other components. | 0 |
days | float | Unsigned magnitude of the days component; 0 when absent. Calendar unit. | 3 |
hours | float | Unsigned magnitude of the hours component; 0 when absent. Exact unit. | 4 |
minutes | float | Unsigned magnitude of the minutes component; 0 when absent. Exact unit. | 5 |
seconds | float | Unsigned magnitude of the seconds component; 0 when absent. Exact unit. | 6 |
classification | object | Which of the PRESENT units are nominal (years, months — no fixed length), calendar (weeks, days — exactly 24h in UTC, wall-clock days in a zone) and exact (hours, minutes, seconds). | {"nominal":["years","months"],"calendar":["days"],"exact":["hours","minutes","seconds"]} |
nominalMonths | float | 12 × years + months. Exact as a count of months; it cannot be converted to a fixed number of days. | 14 |
calendarDays | float | 7 × weeks + days, as calendar days. | 3 |
exactSeconds | float | Seconds contributed by the hours, minutes and seconds components only — exact under every interpretation of the calendar units, and summed on the decimal text rather than as a floating-point product, so PT1.1H is 3960 and not 3960.0000000000005. | 14706 |
approximate | object | Clearly-labelled approximation of the whole duration in seconds and days, plus the assumption used. Never treat it as an exact conversion of years or months. | {"seconds":37090350,"days":429.2864583333333,"assumption":"Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s."} |
value string The validated input duration, unchanged.
example: P1Y2M3DT4H5M6S
normalized string Canonical re-rendering with zero components dropped and a full stop as decimal sign; an all-zero duration becomes PT0S.
example: P1Y2M3DT4H5M6S
negative boolean True when the duration carries a leading '-'. A duration whose components are all zero is never reported as negative.
example: false
years float Unsigned magnitude of the years component; 0 when absent. Nominal unit.
example: 1
months float Unsigned magnitude of the months component; 0 when absent. Nominal unit.
example: 2
weeks float Unsigned magnitude of the weeks component; 0 when absent. Never combined with other components.
example: 0
days float Unsigned magnitude of the days component; 0 when absent. Calendar unit.
example: 3
hours float Unsigned magnitude of the hours component; 0 when absent. Exact unit.
example: 4
minutes float Unsigned magnitude of the minutes component; 0 when absent. Exact unit.
example: 5
seconds float Unsigned magnitude of the seconds component; 0 when absent. Exact unit.
example: 6
classification object Which of the PRESENT units are nominal (years, months — no fixed length), calendar (weeks, days — exactly 24h in UTC, wall-clock days in a zone) and exact (hours, minutes, seconds).
example: {"nominal":["years","months"],"calendar":["days"],"exact":["hours","minutes","seconds"]}
nominalMonths float 12 × years + months. Exact as a count of months; it cannot be converted to a fixed number of days.
example: 14
calendarDays float 7 × weeks + days, as calendar days.
example: 3
exactSeconds float Seconds contributed by the hours, minutes and seconds components only — exact under every interpretation of the calendar units, and summed on the decimal text rather than as a floating-point product, so PT1.1H is 3960 and not 3960.0000000000005.
example: 14706
approximate object Clearly-labelled approximation of the whole duration in seconds and days, plus the assumption used. Never treat it as an exact conversion of years or months.
example: {"seconds":37090350,"days":429.2864583333333,"assumption":"Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s."}
/api/durations/parse?value=P1Y2M3DT4H5M6S {
"data": {
"value": "P1Y2M3DT4H5M6S",
"normalized": "P1Y2M3DT4H5M6S",
"negative": false,
"years": 1,
"months": 2,
"weeks": 0,
"days": 3,
"hours": 4,
"minutes": 5,
"seconds": 6,
"classification": {
"nominal": [
"years",
"months"
],
"calendar": [
"days"
],
"exact": [
"hours",
"minutes",
"seconds"
]
},
"nominalMonths": 14,
"calendarDays": 3,
"exactSeconds": 14706,
"approximate": {
"seconds": 37090350,
"days": 429.2864583333333,
"assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
}
},
"meta": {
"endpoint": "durations",
"route": "/parse",
"params": {
"value": "P1Y2M3DT4H5M6S"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/parse?value=PT1H30M {
"data": {
"value": "PT1H30M",
"normalized": "PT1H30M",
"negative": false,
"years": 0,
"months": 0,
"weeks": 0,
"days": 0,
"hours": 1,
"minutes": 30,
"seconds": 0,
"classification": {
"nominal": [],
"calendar": [],
"exact": [
"hours",
"minutes"
]
},
"nominalMonths": 0,
"calendarDays": 0,
"exactSeconds": 5400,
"approximate": {
"seconds": 5400,
"days": 0.0625,
"assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
}
},
"meta": {
"endpoint": "durations",
"route": "/parse",
"params": {
"value": "PT1H30M"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/parse?value=P2W {
"data": {
"value": "P2W",
"normalized": "P2W",
"negative": false,
"years": 0,
"months": 0,
"weeks": 2,
"days": 0,
"hours": 0,
"minutes": 0,
"seconds": 0,
"classification": {
"nominal": [],
"calendar": [
"weeks"
],
"exact": []
},
"nominalMonths": 0,
"calendarDays": 14,
"exactSeconds": 0,
"approximate": {
"seconds": 1209600,
"days": 14,
"assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
}
},
"meta": {
"endpoint": "durations",
"route": "/parse",
"params": {
"value": "P2W"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/parse?value=-PT1.5S {
"data": {
"value": "-PT1.5S",
"normalized": "-PT1.5S",
"negative": true,
"years": 0,
"months": 0,
"weeks": 0,
"days": 0,
"hours": 0,
"minutes": 0,
"seconds": 1.5,
"classification": {
"nominal": [],
"calendar": [],
"exact": [
"seconds"
]
},
"nominalMonths": 0,
"calendarDays": 0,
"exactSeconds": 1.5,
"approximate": {
"seconds": 1.5,
"days": 0.00001736111111111111,
"assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
}
},
"meta": {
"endpoint": "durations",
"route": "/parse",
"params": {
"value": "-PT1.5S"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} Returns the years/months/days/hours/minutes/seconds breakdown in the same order /add applies, plus exact elapsed totals. Omitting 'to' uses the request time and echoes the resolved instant.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
from
required
| date | example: 2026-01-31T00:00:00Z | Start instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset. |
to | date | example: 2026-03-01T00:00:00Z | End instant, same formats. Defaults to the request time, in which case the resolved instant is echoed in meta.params. May be earlier than 'from'. |
tz | string | default: UTC allowed: 1 – 64 example: Europe/Copenhagen | IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long. |
maxUnits | int | default: 6 allowed: 1 – 6 example: 3 | Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately. |
from date required Start instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.
to date End instant, same formats. Defaults to the request time, in which case the resolved instant is echoed in meta.params. May be earlier than 'from'.
tz string IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.
maxUnits int Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: from,to | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: totalDays | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
from | string (ISO 8601 UTC) | Resolved start instant. | 2026-01-31T00:00:00.000Z |
to | string (ISO 8601 UTC) | Resolved end instant; the request time when 'to' was omitted. | 2026-03-01T00:00:00.000Z |
timeZone | string | Canonical IANA zone the calendar breakdown was computed in. | Europe/Copenhagen |
negative | boolean | True when 'to' is earlier than 'from'; the breakdown is then how far back you must go from 'from'. | false |
years | integer | Whole calendar years from 'from', applied first. | 0 |
months | integer | Whole calendar months after the years step, with end-of-month clamping. | 1 |
days | integer | Whole wall-clock days after the years and months steps. | 1 |
hours | integer | Whole hours left over. Can reach 24 inside a 25-hour fall-back day. | 0 |
minutes | integer | Whole minutes left over. | 0 |
seconds | float | Seconds left over, carrying the millisecond fraction of the inputs — the nearest JSON number to that exact decimal, so it never disagrees with 'iso' or 'totalSeconds'. | 0 |
iso | string | The breakdown as an ISO 8601 duration, signed. Feeding it to /add with the same 'from' and 'tz' reproduces 'to' when truncated is false. | P1M1D |
truncated | boolean | True when maxUnits dropped a non-zero unit. | false |
remainderSeconds | float | Exact seconds still between /add(from, iso) and 'to' — always 0 unless maxUnits truncated the breakdown. | 0 |
totalSeconds | float | Exact elapsed SI seconds between the two instants, signed. Unaffected by the zone. | 2505600 |
totalDays | float | totalSeconds ÷ 86400 as an IEEE-754 double — elapsed days, not calendar days. | 29 |
from string (ISO 8601 UTC) Resolved start instant.
example: 2026-01-31T00:00:00.000Z
to string (ISO 8601 UTC) Resolved end instant; the request time when 'to' was omitted.
example: 2026-03-01T00:00:00.000Z
timeZone string Canonical IANA zone the calendar breakdown was computed in.
example: Europe/Copenhagen
negative boolean True when 'to' is earlier than 'from'; the breakdown is then how far back you must go from 'from'.
example: false
years integer Whole calendar years from 'from', applied first.
example: 0
months integer Whole calendar months after the years step, with end-of-month clamping.
example: 1
days integer Whole wall-clock days after the years and months steps.
example: 1
hours integer Whole hours left over. Can reach 24 inside a 25-hour fall-back day.
example: 0
minutes integer Whole minutes left over.
example: 0
seconds float Seconds left over, carrying the millisecond fraction of the inputs — the nearest JSON number to that exact decimal, so it never disagrees with 'iso' or 'totalSeconds'.
example: 0
iso string The breakdown as an ISO 8601 duration, signed. Feeding it to /add with the same 'from' and 'tz' reproduces 'to' when truncated is false.
example: P1M1D
truncated boolean True when maxUnits dropped a non-zero unit.
example: false
remainderSeconds float Exact seconds still between /add(from, iso) and 'to' — always 0 unless maxUnits truncated the breakdown.
example: 0
totalSeconds float Exact elapsed SI seconds between the two instants, signed. Unaffected by the zone.
example: 2505600
totalDays float totalSeconds ÷ 86400 as an IEEE-754 double — elapsed days, not calendar days.
example: 29
/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen {
"data": {
"from": "2026-01-31T00:00:00.000Z",
"to": "2026-03-01T00:00:00.000Z",
"timeZone": "Europe/Copenhagen",
"negative": false,
"years": 0,
"months": 1,
"days": 1,
"hours": 0,
"minutes": 0,
"seconds": 0,
"iso": "P1M1D",
"truncated": false,
"remainderSeconds": 0,
"totalSeconds": 2505600,
"totalDays": 29
},
"meta": {
"endpoint": "durations",
"route": "/between",
"params": {
"from": "2026-01-31",
"to": "2026-03-01",
"tz": "Europe/Copenhagen",
"maxUnits": 6
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/between?from=2026-01-01T00%3A00%3A00Z&to=2027-03-15T06%3A30%3A00Z&maxUnits=2 {
"data": {
"from": "2026-01-01T00:00:00.000Z",
"to": "2027-03-15T06:30:00.000Z",
"timeZone": "UTC",
"negative": false,
"years": 1,
"months": 2,
"days": 0,
"hours": 0,
"minutes": 0,
"seconds": 0,
"iso": "P1Y2M",
"truncated": true,
"remainderSeconds": 1233000,
"totalSeconds": 37866600,
"totalDays": 438.2708333333333
},
"meta": {
"endpoint": "durations",
"route": "/between",
"params": {
"from": "2026-01-01",
"to": "2027-03-15T06:30:00.000Z",
"tz": "UTC",
"maxUnits": 2
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/between?from=2026-03-31&to=2026-02-28 {
"data": {
"from": "2026-03-31T00:00:00.000Z",
"to": "2026-02-28T00:00:00.000Z",
"timeZone": "UTC",
"negative": true,
"years": 0,
"months": 1,
"days": 0,
"hours": 0,
"minutes": 0,
"seconds": 0,
"iso": "-P1M",
"truncated": false,
"remainderSeconds": 0,
"totalSeconds": -2678400,
"totalDays": -31
},
"meta": {
"endpoint": "durations",
"route": "/between",
"params": {
"from": "2026-03-31",
"to": "2026-02-28",
"tz": "UTC",
"maxUnits": 6
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/between?from=2026-01-01&maxUnits=2 {
"data": {
"from": "2026-01-01T00:00:00.000Z",
"to": "2026-07-29T08:13:22.000Z",
"timeZone": "UTC",
"negative": false,
"years": 0,
"months": 6,
"days": 28,
"hours": 0,
"minutes": 0,
"seconds": 0,
"iso": "P6M28D",
"truncated": true,
"remainderSeconds": 29602,
"totalSeconds": 18087202,
"totalDays": 209.34261574074074
},
"meta": {
"endpoint": "durations",
"route": "/between",
"params": {
"from": "2026-01-01",
"tz": "UTC",
"maxUnits": 2,
"to": "2026-07-29T08:13:22.000Z"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} Applies years, then months, then weeks and days on the wall clock, resolves that value in the zone once, then adds the exact time components. Clamping and any DST adjustment are reported explicitly.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
start
required
| date | example: 2026-01-31 | Starting instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset. |
duration
required
| string | allowed: ≤ 64 example: P1M | ISO 8601 duration to add; a leading '-' subtracts. Years, months, weeks and days must be whole numbers — only H, M and S may carry a fraction. |
tz | string | default: UTC allowed: 1 – 64 example: Europe/Copenhagen | IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long. |
start date required Starting instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.
duration string required ISO 8601 duration to add; a leading '-' subtracts. Years, months, weeks and days must be whole numbers — only H, M and S may carry a fraction.
tz string IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: start,startLocal | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: calendarDaysApplied | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
start | string (ISO 8601 UTC) | Resolved starting instant. | 2026-01-31T00:00:00.000Z |
startLocal | string (local wall clock) | Wall-clock value of 'start' in the requested zone. | 2026-01-31T00:00:00.000 |
startOffset | string (±HH:MM) | UTC offset in effect at 'start'. | +00:00 |
timeZone | string | Canonical IANA zone the calendar part was applied in. | UTC |
duration | string | Normalized form of the duration that was applied. | P1M |
steps | object[] | The four ordered steps (years, months, days, time) with the signed amount, the wall clock after the step and whether an end-of-month clamp fired. The calendar steps run on the wall clock; the zone is applied once afterwards. | [{"unit":"years","amount":0,"local":"2026-01-31T00:00:00.000","clamped":false},{"unit":"months","amount":1,"local":"2026-02-28T00:00:00.000","clamped":true},{"unit":"days","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false},{"unit":"time","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false}] |
clamped | boolean | True when the years or months step clamped the day of month to the end of a shorter month. | true |
dstAdjustment nullable | object | Null when the duration has no calendar part (nothing is resolved through the wall clock) or when the calendar result was an ordinary wall-clock value. Otherwise names the gap (shifted forward by the missing interval) or fold (earlier instant chosen) and by how much. | null |
result | string (ISO 8601 UTC) | The resulting instant. | 2026-02-28T00:00:00.000Z |
resultLocal | string (local wall clock) | Wall-clock value of the result in the requested zone. | 2026-02-28T00:00:00.000 |
resultOffset | string (±HH:MM) | UTC offset in effect at the result. | +00:00 |
exactSecondsApplied | float | Signed seconds added by the exact time components (H, M, S) after the calendar steps. | 0 |
calendarDaysApplied | integer | Signed calendar days added by the day step, counting each week as 7 days. | 0 |
start string (ISO 8601 UTC) Resolved starting instant.
example: 2026-01-31T00:00:00.000Z
startLocal string (local wall clock) Wall-clock value of 'start' in the requested zone.
example: 2026-01-31T00:00:00.000
startOffset string (±HH:MM) UTC offset in effect at 'start'.
example: +00:00
timeZone string Canonical IANA zone the calendar part was applied in.
example: UTC
duration string Normalized form of the duration that was applied.
example: P1M
steps object[] The four ordered steps (years, months, days, time) with the signed amount, the wall clock after the step and whether an end-of-month clamp fired. The calendar steps run on the wall clock; the zone is applied once afterwards.
example: [{"unit":"years","amount":0,"local":"2026-01-31T00:00:00.000","clamped":false},{"unit":"months","amount":1,"local":"2026-02-28T00:00:00.000","clamped":true},{"unit":"days","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false},{"unit":"time","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false}]
clamped boolean True when the years or months step clamped the day of month to the end of a shorter month.
example: true
dstAdjustment object nullable Null when the duration has no calendar part (nothing is resolved through the wall clock) or when the calendar result was an ordinary wall-clock value. Otherwise names the gap (shifted forward by the missing interval) or fold (earlier instant chosen) and by how much.
example: null
result string (ISO 8601 UTC) The resulting instant.
example: 2026-02-28T00:00:00.000Z
resultLocal string (local wall clock) Wall-clock value of the result in the requested zone.
example: 2026-02-28T00:00:00.000
resultOffset string (±HH:MM) UTC offset in effect at the result.
example: +00:00
exactSecondsApplied float Signed seconds added by the exact time components (H, M, S) after the calendar steps.
example: 0
calendarDaysApplied integer Signed calendar days added by the day step, counting each week as 7 days.
example: 0
/api/durations/add?start=2026-01-31&duration=P1M {
"data": {
"start": "2026-01-31T00:00:00.000Z",
"startLocal": "2026-01-31T00:00:00.000",
"startOffset": "+00:00",
"timeZone": "UTC",
"duration": "P1M",
"steps": [
{
"unit": "years",
"amount": 0,
"local": "2026-01-31T00:00:00.000",
"clamped": false
},
{
"unit": "months",
"amount": 1,
"local": "2026-02-28T00:00:00.000",
"clamped": true
},
{
"unit": "days",
"amount": 0,
"local": "2026-02-28T00:00:00.000",
"clamped": false
},
{
"unit": "time",
"amount": 0,
"local": "2026-02-28T00:00:00.000",
"clamped": false
}
],
"clamped": true,
"dstAdjustment": null,
"result": "2026-02-28T00:00:00.000Z",
"resultLocal": "2026-02-28T00:00:00.000",
"resultOffset": "+00:00",
"exactSecondsApplied": 0,
"calendarDaysApplied": 0
…
]
}
} /api/durations/add?start=2024-01-31&duration=P1M {
"data": {
"start": "2024-01-31T00:00:00.000Z",
"startLocal": "2024-01-31T00:00:00.000",
"startOffset": "+00:00",
"timeZone": "UTC",
"duration": "P1M",
"steps": [
{
"unit": "years",
"amount": 0,
"local": "2024-01-31T00:00:00.000",
"clamped": false
},
{
"unit": "months",
"amount": 1,
"local": "2024-02-29T00:00:00.000",
"clamped": true
},
{
"unit": "days",
"amount": 0,
"local": "2024-02-29T00:00:00.000",
"clamped": false
},
{
"unit": "time",
"amount": 0,
"local": "2024-02-29T00:00:00.000",
"clamped": false
}
],
"clamped": true,
"dstAdjustment": null,
"result": "2024-02-29T00:00:00.000Z",
"resultLocal": "2024-02-29T00:00:00.000",
"resultOffset": "+00:00",
"exactSecondsApplied": 0,
"calendarDaysApplied": 0
…
]
}
} /api/durations/add?start=2026-03-28T23%3A00%3A00Z&duration=P1D&tz=Europe%2FCopenhagen {
"data": {
"start": "2026-03-28T23:00:00.000Z",
"startLocal": "2026-03-29T00:00:00.000",
"startOffset": "+01:00",
"timeZone": "Europe/Copenhagen",
"duration": "P1D",
"steps": [
{
"unit": "years",
"amount": 0,
"local": "2026-03-29T00:00:00.000",
"clamped": false
},
{
"unit": "months",
"amount": 0,
"local": "2026-03-29T00:00:00.000",
"clamped": false
},
{
"unit": "days",
"amount": 1,
"local": "2026-03-30T00:00:00.000",
"clamped": false
},
{
"unit": "time",
"amount": 0,
"local": "2026-03-30T00:00:00.000",
"clamped": false
}
],
"clamped": false,
"dstAdjustment": null,
"result": "2026-03-29T22:00:00.000Z",
"resultLocal": "2026-03-30T00:00:00.000",
"resultOffset": "+02:00",
"exactSecondsApplied": 0,
"calendarDaysApplied": 1
…
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/add?start=2026-03-28T23%3A00%3A00Z&duration=PT24H&tz=Europe%2FCopenhagen {
"data": {
"start": "2026-03-28T23:00:00.000Z",
"startLocal": "2026-03-29T00:00:00.000",
"startOffset": "+01:00",
"timeZone": "Europe/Copenhagen",
"duration": "PT24H",
"steps": [
{
"unit": "years",
"amount": 0,
"local": "2026-03-29T00:00:00.000",
"clamped": false
},
{
"unit": "months",
"amount": 0,
"local": "2026-03-29T00:00:00.000",
"clamped": false
},
{
"unit": "days",
"amount": 0,
"local": "2026-03-29T00:00:00.000",
"clamped": false
},
{
"unit": "time",
"amount": 86400,
"local": "2026-03-30T01:00:00.000",
"clamped": false
}
],
"clamped": false,
"dstAdjustment": null,
"result": "2026-03-29T23:00:00.000Z",
"resultLocal": "2026-03-30T01:00:00.000",
"resultOffset": "+02:00",
"exactSecondsApplied": 86400,
"calendarDaysApplied": 0
…
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/add?start=2026-05-31&duration=-P3M {
"data": {
"start": "2026-05-31T00:00:00.000Z",
"startLocal": "2026-05-31T00:00:00.000",
"startOffset": "+00:00",
"timeZone": "UTC",
"duration": "-P3M",
"steps": [
{
"unit": "years",
"amount": 0,
"local": "2026-05-31T00:00:00.000",
"clamped": false
},
{
"unit": "months",
"amount": -3,
"local": "2026-02-28T00:00:00.000",
"clamped": true
},
{
"unit": "days",
"amount": 0,
"local": "2026-02-28T00:00:00.000",
"clamped": false
},
{
"unit": "time",
"amount": 0,
"local": "2026-02-28T00:00:00.000",
"clamped": false
}
],
"clamped": true,
"dstAdjustment": null,
"result": "2026-02-28T00:00:00.000Z",
"resultLocal": "2026-02-28T00:00:00.000",
"resultOffset": "+00:00",
"exactSecondsApplied": 0,
"calendarDaysApplied": 0
…
]
}
} Formats the duration with Intl.DurationFormat and echoes the resolved locale. Units are never re-balanced; maxUnits truncates without rounding and returns the dropped part.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
value
required
| string | allowed: ≤ 64 example: PT1H30M | ISO 8601 duration to render. A fraction in the smallest exact component is carried into the next smaller unit; a fractional year, month, week or day is rejected. |
style | enum | default: long allowed: long | short | digital example: short | CLDR duration style: long ('1 hour, 30 minutes'), short ('1 hr, 30 min') or digital ('1:30:00', which always shows the full H:MM:SS clock). |
maxUnits | int | default: 6 allowed: 1 – 6 example: 3 | Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately. |
locale | string | default: en allowed: 2 – 35 example: de | BCP 47 language tag for the wording and digits, e.g. de, ja or ar-EG-u-nu-arab. Malformed tags are a 400; an unmatched but well-formed tag falls back, which resolvedLocale makes visible. |
value string required ISO 8601 duration to render. A fraction in the smallest exact component is carried into the next smaller unit; a fractional year, month, week or day is rejected.
style enum CLDR duration style: long ('1 hour, 30 minutes'), short ('1 hr, 30 min') or digital ('1:30:00', which always shows the full H:MM:SS clock).
maxUnits int Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.
locale string BCP 47 language tag for the wording and digits, e.g. de, ja or ar-EG-u-nu-arab. Malformed tags are a 400; an unmatched but well-formed tag falls back, which resolvedLocale makes visible.
| Parameter | Type | Default & allowed | Description |
|---|---|---|---|
fields | list | example: value,normalized | Return only these fields (comma-separated). Mutually exclusive with 'exclude'. |
exclude | list | example: negative | Return all fields except these (comma-separated). |
pretty | boolean | default: false example: true | Pretty-print the JSON response. |
unwrap | boolean | default: false example: true | Drop the envelope: return the raw array/object without data/meta wrapper. |
fields list Return only these fields (comma-separated). Mutually exclusive with 'exclude'.
exclude list Return all fields except these (comma-separated).
pretty boolean Pretty-print the JSON response.
unwrap boolean Drop the envelope: return the raw array/object without data/meta wrapper.
| Field | Type | Description | Example |
|---|---|---|---|
value | string | The validated input duration, unchanged. | PT1H30M |
normalized | string | What was actually formatted: the input with any fraction carried exactly into the next smaller exact unit, and truncated to maxUnits. | PT1H30M |
text | string | The rendered duration, produced from the runtime's CLDR data: Intl.DurationFormat where the runtime provides it, otherwise the same unit and list patterns composed from Intl.NumberFormat and Intl.ListFormat. | 1 hour, 30 minutes |
style | string | Requested style: long, short or digital. | long |
locale | string | The BCP 47 tag exactly as requested. | en |
resolvedLocale | string | resolvedOptions().locale of the formatter that ran — the locale the runtime actually matched, which reveals any fallback. | en |
numberingSystem | string | resolvedOptions().numberingSystem of the formatter that ran, used for the digits. | latn |
units | string[] | Units with a non-zero value that were handed to the formatter, largest first. | ["hours","minutes"] |
truncated | boolean | True when maxUnits dropped a non-zero unit. | false |
remainder nullable | string | The dropped part as an ISO 8601 duration, or null when nothing was dropped. Nothing is rounded away silently. | null |
negative | boolean | True when the duration carries a leading '-'. | false |
value string The validated input duration, unchanged.
example: PT1H30M
normalized string What was actually formatted: the input with any fraction carried exactly into the next smaller exact unit, and truncated to maxUnits.
example: PT1H30M
text string The rendered duration, produced from the runtime's CLDR data: Intl.DurationFormat where the runtime provides it, otherwise the same unit and list patterns composed from Intl.NumberFormat and Intl.ListFormat.
example: 1 hour, 30 minutes
style string Requested style: long, short or digital.
example: long
locale string The BCP 47 tag exactly as requested.
example: en
resolvedLocale string resolvedOptions().locale of the formatter that ran — the locale the runtime actually matched, which reveals any fallback.
example: en
numberingSystem string resolvedOptions().numberingSystem of the formatter that ran, used for the digits.
example: latn
units string[] Units with a non-zero value that were handed to the formatter, largest first.
example: ["hours","minutes"]
truncated boolean True when maxUnits dropped a non-zero unit.
example: false
remainder string nullable The dropped part as an ISO 8601 duration, or null when nothing was dropped. Nothing is rounded away silently.
example: null
negative boolean True when the duration carries a leading '-'.
example: false
/api/durations/humanize?value=PT1H30M {
"data": {
"value": "PT1H30M",
"normalized": "PT1H30M",
"text": "1 hour, 30 minutes",
"style": "long",
"locale": "en",
"resolvedLocale": "en",
"numberingSystem": "latn",
"units": [
"hours",
"minutes"
],
"truncated": false,
"remainder": null,
"negative": false
},
"meta": {
"endpoint": "durations",
"route": "/humanize",
"params": {
"value": "PT1H30M",
"style": "long",
"maxUnits": 6,
"locale": "en"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/humanize?value=P1DT2H30M15S&style=short&maxUnits=3 {
"data": {
"value": "P1DT2H30M15S",
"normalized": "P1DT2H30M",
"text": "1 day, 2 hr, 30 min",
"style": "short",
"locale": "en",
"resolvedLocale": "en",
"numberingSystem": "latn",
"units": [
"days",
"hours",
"minutes"
],
"truncated": true,
"remainder": "PT15S",
"negative": false
},
"meta": {
"endpoint": "durations",
"route": "/humanize",
"params": {
"value": "P1DT2H30M15S",
"style": "short",
"maxUnits": 3,
"locale": "en"
},
"generatedAt": "2026-07-29T08:13:22.000Z",
"warnings": [
"maxUnits=3 dropped seconds; the omitted part is returned in 'remainder' and nothing was rounded."
]
}
} /api/durations/humanize?value=PT1H30M5S&style=digital {
"data": {
"value": "PT1H30M5S",
"normalized": "PT1H30M5S",
"text": "1:30:05",
"style": "digital",
"locale": "en",
"resolvedLocale": "en",
"numberingSystem": "latn",
"units": [
"hours",
"minutes",
"seconds"
],
"truncated": false,
"remainder": null,
"negative": false
},
"meta": {
"endpoint": "durations",
"route": "/humanize",
"params": {
"value": "PT1H30M5S",
"style": "digital",
"maxUnits": 6,
"locale": "en"
},
"generatedAt": "2026-07-29T08:13:22.000Z"
}
} /api/durations/humanize?value=P2DT4H30M&locale=de&maxUnits=2 {
"data": {
"value": "P2DT4H30M",
"normalized": "P2DT4H",
"text": "2 Tage, 4 Stunden",
"style": "long",
"locale": "de",
"resolvedLocale": "de",
"numberingSystem": "latn",
"units": [
"days",
"hours"
],
"truncated": true,
"remainder": "PT30M",
"negative": false
},
"meta": {
"endpoint": "durations",
"route": "/humanize",
"params": {
"value": "P2DT4H30M",
"style": "long",
"maxUnits": 2,
"locale": "de"
},
"generatedAt": "2026-07-29T08:13:22.000Z",
"warnings": [
"maxUnits=2 dropped minutes; the omitted part is returned in 'remainder' and nothing was rounded."
]
}
} Everything about the ISO 8601 duration form P[nY][nM][nW][nD][T[nH][nM][nS]] — the string the YouTube Data API returns as PT1H30M, OpenAPI and JSON Schema validate as format: duration, and iCalendar carries as DURATION. Pure computation: no dataset, no clock except where documented, no network.
Grammar. Components must appear in the order above, each at most once, digits before every designator, designators uppercase. The week form is exclusive — P2W or the date/time form, never both — in the ABNF of RFC 5545 §3.3.6 and of RFC 3339 Appendix A alike, so P1W2D is a 400 (write P9D). The Y and M designators come from RFC 3339 Appendix A (dur-year, dur-month) and xs:duration, not from RFC 5545: iCalendar's dur-value has no year or month production at all and says so in its own prose, so it is cited here only for week-vs-day exclusivity and the leading sign. A decimal fraction (full stop or comma) is accepted only in the smallest component present, ISO 8601's lowest-order-component rule; both public ABNFs allow whole numbers only, and XML Schema restates the same idea for seconds alone, so a fractional input is an extension of those grammars. A leading - (or +) is also accepted: ISO 8601-1 durations are unsigned, but RFC 5545's dur-value admits + or - and xs:duration admits -, and that is the form used here. P alone, a T with no time component, and unknown designators are all explicit 400s that name the problem. Once leading zeroes are dropped, a component value may carry at most 9 digits either side of the decimal sign and must be exactly representable as a JSON number — anything else is rejected, never rounded.
Nominal vs exact. Years and months are nominal: they have no fixed length, and no conversion of P1Y to seconds is a fact. /parse therefore reports exactSeconds for the H/M/S part and puts any whole-duration total inside approximate, together with the mean-Gregorian assumption it used. Weeks and days are calendar units: exactly 24 hours in UTC, but wall-clock days in a real zone, so they can be 23 or 25 hours long. Hours, minutes and seconds are always exact SI seconds, and every total labelled exact (exactSeconds, nominalMonths, calendarDays) is summed on the decimal text with integer arithmetic rather than as a floating-point product — PT1.1H is 3960 seconds, not 3960.0000000000005.
/add — order, clamping and DST. The calendar part is applied to the wall clock in a fixed, documented order: years, then months, then weeks and days; the zone then resolves that wall-clock value to an instant exactly once; finally the exact time components are added as milliseconds. Order changes the answer, so it is pinned: 2024-02-29 +P1Y1M is 2025-03-28 while +P13M is 2025-03-29. Adding a month clamps to the last day of the shorter month — 2026-01-31 +P1M is 2026-02-28, and 2024-01-31 +P1M is 2024-02-29 — which is a documented choice, not a universal rule: because of it, P1M twice (2026-03-28) is not the same as P2M (2026-03-31). P1D means the same wall time on the next calendar day; PT24H means exactly 86400 seconds, so across a daylight-saving change the two differ by the size of the change. If the calendar result lands in a DST gap it is shifted forward by the missing interval; if it lands in a fold the earlier of the two instants is taken. Both cases are reported in dstAdjustment and as a warning — never silently. A duration with no calendar part never touches the wall clock at all, so PT1H moves the instant by exactly 3600 seconds even when the start sits in a repeated hour, where reading the local time back would have collapsed it onto the earlier occurrence. /add requires whole years, months, weeks and days: a fractional calendar unit is a 400 rather than a guess, and exact components are applied to millisecond precision, so PT0.0005S is a 400 instead of a rounded answer.
/between. Returns both the calendar breakdown (years, months, days, hours, minutes, seconds, in the same order /add applies) and the exact totals (totalSeconds, totalDays = elapsed SI time, unaffected by the zone). The breakdown is built by the same arithmetic /add uses, so /add(from, iso, tz) reproduces to exactly whenever truncated is false; maxUnits truncates toward zero and reports the exact leftover in remainderSeconds. Calendar differences are not symmetric under clamping: 2026-02-28 → 2026-03-31 is P1M3D, while 2026-03-31 → 2026-02-28 is -P1M. Inside a 25-hour fall-back day the hours component can legitimately reach 24, because the calendar day it sits in really was 25 hours long. Omitting to uses the request time and echoes the resolved instant.
/humanize. Wording and unit names come from the runtime's CLDR data through Intl.DurationFormat (resolvedLocale and numberingSystem echo what the runtime matched, so a locale fallback is visible). Intl.DurationFormat only landed in Node 23, so on an older runtime — including a static build importing this module — the same CLDR data is composed instead from Intl.NumberFormat's style: "unit" patterns and Intl.ListFormat's unit list, which is how CLDR builds a duration in the first place. The test suite compares the two paths on every locale its runtime resolves, in all three styles: they agree byte-for-byte, the one known exception being the digital clock separator in kl and ars, whose CLDR duration pattern no other Intl API exposes. The choices around the formatter are ours, not a specification: units are never re-balanced (PT90M is "90 minutes", not "1 hour, 30 minutes"), a fraction is carried exactly into the next smaller exact unit (PT1.5H → PT1H30M), maxUnits truncates without rounding and returns the dropped part in remainder, and a fractional year, month, week or day is a 400 because it cannot be carried exactly.
Scope. This endpoint owns the duration grammar only. ISO 8601 date, time and interval strings (2026-07-10, 2026-07-10T12:00:00Z/PT1H) are deliberately out of scope; use /api/date-info and /api/timezones for those. There is no repeating-interval (R/…) support. ISO 8601-1:2019 is paywalled, so the grammar implemented here follows its public restatements — RFC 5545 §3.3.6, RFC 3339 Appendix A and XML Schema's xs:duration — and this API makes no claim of certified ISO conformance.
It is the ISO 8601 duration for one hour and thirty minutes: P opens the duration, T separates the time part, and H/M are hours and minutes. /humanize?value=PT1H30M renders it as 1 hour, 30 minutes.
/parse returns exactSeconds for the hours, minutes and seconds part, which is exact. Years and months have no fixed length, so any whole-duration total appears inside approximate with the mean-Gregorian assumption stated — it is never presented as a fact.
No. /add clamps to the last day of a shorter month, so 2026-01-31 +P1M is 2026-02-28 and 2024-01-31 +P1M is 2024-02-29. Because clamping is not reversible, applying P1M twice gives 2026-03-28 while P2M gives 2026-03-31.
Only when the zone has no offset change in between (tz=UTC always). P1D keeps the same wall-clock time on the next calendar day, while PT24H adds exactly 86400 seconds, so across a spring-forward in Europe/Copenhagen the two results differ by an hour.
The ABNF in RFC 5545 §3.3.6 and in RFC 3339 Appendix A alike makes the week form an alternative to the date form, so weeks cannot be combined with other components. Write P9D instead; P2W on its own is fine.
Yes, in the smallest component present — ISO 8601's lowest-order-component rule, with a full stop or a comma. PT1.5H parses, and /humanize carries the fraction exactly into PT1H30M. A fraction in a larger component such as P1.5DT2H is a 400, and /add rejects fractional years, months, weeks and days outright.
Real calendar facts for any date — weekday, ISO week, day of year, quarter, leap year, days in month, weekend flag and Unix timestamp. Defaults to today.
Add, count or inspect business days using verified national public-holiday rules for 61 countries, including observed substitute days.
Uniformly random timestamps from any date range — ISO instant, Unix seconds, date, time and weekday — with an exact weekdays-only mode and seeded results.
Current time in any IANA timezone — UTC instant, unix seconds, local date & time, UTC offset, ISO week, day of year and a live DST flag.
Generate coherent reservation lifecycles and deterministic IANA-timezone availability slots with exact capacity and DST behavior.
Generate coherent fake calendar events with bounded durations, attendees, IANA timezones and correct timed or all-day date shapes.