# Behaviour of date libraries against Temporal

Each case below gives one input to Temporal, to GMT and to five libraries: date-fns, Luxon, Day.js, Moment.js and @internationalized/date. A library's result is reduced to the form named in the case, then compared with Temporal's result. The verdict is "Same" when the two are equal on every host, "Differs" when they are not, "Depends on the host zone" when the result changes with the host, and "Not supported" when the library has no such operation. A case Temporal has no call for is compared with a stated result, and the case says so.

## How it was measured

A person runs `pnpm matrix` in `scripts/behaviour-matrix/`. Some libraries read the time zone of the machine they run on, so a result can depend on where the code runs. The runner therefore repeats every library call once under each of these host time zones: Asia/Kolkata, Europe/London, Australia/Sydney, America/Santiago. One has no daylight saving time, and the others observe it in the northern and southern hemispheres. This run used Node v26.12.1 on 2026-10-27.

A library whose result is the same on every host gets the verdict "Same" or "Differs". A library whose result changes with the host zone gets the verdict "Depends on the host zone", and its row lists the result of each host. It is never "Same", because it would give a different answer on another machine. Temporal and GMT read no host zone, and the runner stops if either gives a different result on any host.

Every Temporal call ran twice, once on the polyfill and once on native Temporal, and the runner stops when the two disagree. The versions of the five libraries, and of the companion packages date-fns and Moment.js use for time zones, are named at the top of each library's page under [Migrate to Temporal with GMT](/gmt/migrate/).

GMT's result is run from the source of this repository. Where GMT's result differs from Temporal's, the case says why.

## M1. Add one calendar day across a DST gap

Input: 2024-03-30T02:30:00+01:00\[Europe/Berlin\], plus 1 day (02:30 does not exist on 2024-03-31). Compared as: local time and offset. Specification: [AddZonedDateTime (6.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime), [DisambiguatePossibleEpochNanoseconds (11.1.12)](https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds), [RFC 5545 section 3.3.6](https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-03-31T03:30:00+02:00\[Europe/Berlin\] | Reference | none |
| GMT | addZoned("2024-03-30T02:30:00+01:00\[Europe/Berlin\]", { days: 1 }) | 2024-03-31T03:30:00+02:00\[Europe/Berlin\] | Same | none |
| date-fns | addDays(new TZDate(2024, 2, 30, 2, 30, "Europe/Berlin"), 1) | Asia/Kolkata host: 2024-03-31T03:30:00.000+02:00, Europe/London host: 2024-03-31T01:30:00.000+01:00, Australia/Sydney host: 2024-03-31T03:30:00.000+02:00, America/Santiago host: 2024-03-31T03:30:00.000+02:00 | Depends on the host zone | none |
| Luxon | DateTime.fromISO("2024-03-30T02:30", { zone: "Europe/Berlin" }).plus({ days: 1 }).toISO() | 2024-03-31T03:30:00.000+02:00 | Same | none |
| Day.js | dayjs.tz("2024-03-30T02:30", "Europe/Berlin").add(1, "day").format() | 2024-03-31T02:30:00+01:00 | Differs | [iamkun/dayjs#1271](https://github.com/iamkun/dayjs/issues/1271), [iamkun/dayjs#1260](https://github.com/iamkun/dayjs/issues/1260), [iamkun/dayjs#1827](https://github.com/iamkun/dayjs/issues/1827) |
| Moment.js | moment.tz("2024-03-30T02:30", "Europe/Berlin").add(1, "day").format() | 2024-03-31T03:30:00+02:00 | Same | none |
| @internationalized/date | parseZonedDateTime("2024-03-30T02:30\[Europe/Berlin\]").add({ days: 1 }).toString() | 2024-03-31T03:30:00+02:00\[Europe/Berlin\] | Same | none |

## M2. Add one calendar day, not 24 hours

Input: 2024-03-30T12:00:00+01:00\[Europe/Berlin\], plus 1 day (the day of 2024-03-31 has 23 hours). Compared as: local time and offset. Specification: [AddZonedDateTime (6.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime), [hoursInDay (6.3.23)](https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.hoursinday), [RFC 5545 section 3.3.6](https://www.rfc-editor.org/rfc/rfc5545#section-3.3.6).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-03-31T12:00:00+02:00\[Europe/Berlin\] | Reference | none |
| GMT | addZoned("2024-03-30T12:00:00+01:00\[Europe/Berlin\]", { days: 1 }) | 2024-03-31T12:00:00+02:00\[Europe/Berlin\] | Same | none |
| date-fns | addDays(new TZDate(2024, 2, 30, 12, 0, "Europe/Berlin"), 1) | 2024-03-31T12:00:00.000+02:00 | Same | none |
| Luxon | DateTime.fromISO("2024-03-30T12:00", { zone: "Europe/Berlin" }).plus({ days: 1 }).toISO() | 2024-03-31T12:00:00.000+02:00 | Same | none |
| Day.js | dayjs.tz("2024-03-30T12:00", "Europe/Berlin").add(1, "day").format() | 2024-03-31T12:00:00+01:00 | Differs | [iamkun/dayjs#1271](https://github.com/iamkun/dayjs/issues/1271) |
| Moment.js | moment.tz("2024-03-30T12:00", "Europe/Berlin").add(1, "day").format() | 2024-03-31T12:00:00+02:00 | Same | none |
| @internationalized/date | parseZonedDateTime("2024-03-30T12:00\[Europe/Berlin\]").add({ days: 1 }).toString() | 2024-03-31T12:00:00+02:00\[Europe/Berlin\] | Same | none |

## M3. Add one day into a repeated hour

Input: 2024-10-26T02:30:00+02:00\[Europe/Berlin\], plus 1 day (02:30 happens twice on 2024-10-27). Compared as: local time and offset. Specification: [DisambiguatePossibleEpochNanoseconds (11.1.12)](https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds), [AddZonedDateTime (6.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-10-27T02:30:00+02:00\[Europe/Berlin\] | Reference | none |
| GMT | addZoned("2024-10-26T02:30:00+02:00\[Europe/Berlin\]", { days: 1 }) | 2024-10-27T02:30:00+02:00\[Europe/Berlin\] | Same | none |
| date-fns | addDays(new TZDate(2024, 9, 26, 2, 30, "Europe/Berlin"), 1) | Asia/Kolkata host: 2024-10-27T02:30:00.000+02:00, Europe/London host: 2024-10-27T02:30:00.000+01:00, Australia/Sydney host: 2024-10-27T02:30:00.000+02:00, America/Santiago host: 2024-10-27T02:30:00.000+01:00 | Depends on the host zone | none |
| Luxon | DateTime.fromISO("2024-10-26T02:30", { zone: "Europe/Berlin" }).plus({ days: 1 }).toISO() | 2024-10-27T02:30:00.000+02:00 | Same | none |
| Day.js | dayjs.tz("2024-10-26T02:30", "Europe/Berlin").add(1, "day").format() | 2024-10-27T02:30:00+02:00 | Same | none |
| Moment.js | moment.tz("2024-10-26T02:30", "Europe/Berlin").add(1, "day").format() | 2024-10-27T02:30:00+02:00 | Same | none |
| @internationalized/date | parseZonedDateTime("2024-10-26T02:30\[Europe/Berlin\]").add({ days: 1 }).toString() | 2024-10-27T02:30:00+02:00\[Europe/Berlin\] | Same | none |

## M3b. Two readings of 02:30 on the repeated-hour day: equal or not

Input: 2024-10-27T02:30:00+02:00\[Europe/Berlin\] and 2024-10-27T02:30:00+01:00\[Europe/Berlin\]. Compared as: equal (true or false). Specification: [ZonedDateTime.prototype.equals (6.3.40)](https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.equals).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | false | Reference | none |
| GMT | areZonedEqual("2024-10-27T02:30:00+02:00\[Europe/Berlin\]", "2024-10-27T02:30:00+01:00\[Europe/Berlin\]") | false | Same | none |
| date-fns | isEqual(parseISO("2024-10-27T02:30:00+02:00"), parseISO("2024-10-27T02:30:00+01:00")) | false | Same | none |
| Luxon | DateTime.fromISO(a, { zone: "Europe/Berlin" }).equals(DateTime.fromISO(b, { zone: "Europe/Berlin" })) | false | Same | none |
| Day.js | dayjs(a).isSame(dayjs(b)) | false | Same | none |
| Moment.js | moment.parseZone(a).isSame(moment.parseZone(b)) | false | Same | none |
| @internationalized/date | parseZonedDateTime(a).compare(parseZonedDateTime(b)) === 0 | false | Same | none |

## M4. Resolve a wall time that does not exist

Input: 2024-03-31T02:30:00 in Europe/Berlin (the clocks skip from 02:00 to 03:00). Compared as: UTC instant. Specification: [GetEpochNanosecondsFor (11.1.11)](https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor), [DisambiguatePossibleEpochNanoseconds (11.1.12)](https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds), [GetPossibleEpochNanoseconds (11.1.13)](https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-03-31T01:30:00Z | Reference | none |
| GMT | resolveLocal("2024-03-31T02:30:00", "Europe/Berlin") | 2024-03-31T01:30:00Z | Same | none |
| date-fns | new TZDate(2024, 2, 31, 2, 30, "Europe/Berlin") | Asia/Kolkata host: 2024-03-31T01:30:00.000+00:00, Europe/London host: 2024-03-31T00:30:00.000+00:00, Australia/Sydney host: 2024-03-31T01:30:00.000+00:00, America/Santiago host: 2024-03-31T01:30:00.000+00:00 | Depends on the host zone | none |
| Luxon | DateTime.fromISO("2024-03-31T02:30", { zone: "Europe/Berlin" }).toUTC().toISO() | 2024-03-31T01:30:00.000Z | Same | none |
| Day.js | dayjs.tz("2024-03-31T02:30", "Europe/Berlin").utc().format() | 2024-03-31T01:30:00Z | Same | none |
| Moment.js | moment.tz("2024-03-31T02:30", "Europe/Berlin").utc().format() | 2024-03-31T01:30:00Z | Same | none |
| @internationalized/date | parseZonedDateTime("2024-03-31T02:30\[Europe/Berlin\]").toAbsoluteString() | 2024-03-31T01:30:00.000Z | Same | none |

## M4b. Reject a wall time that does not exist

Input: 2024-03-31T02:30:00 in Europe/Berlin, asking for an error instead of a shifted time. Compared as: UTC instant, or rejected. Specification: [DisambiguatePossibleEpochNanoseconds (11.1.12)](https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds), [GetTemporalDisambiguationOption (13.7)](https://tc39.es/proposal-temporal/#sec-temporal-gettemporaldisambiguationoption).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | rejected | Reference | none |
| GMT | resolveLocal("2024-03-31T02:30:00", "Europe/Berlin", { disambiguation: "reject" }) | rejected | Same | none |
| date-fns | none | TZDate always resolves a skipped wall time; it has no option to reject it | Not supported | none |
| Luxon | none | fromISO always resolves a skipped wall time; it has no option to reject it | Not supported | none |
| Day.js | none | dayjs.tz always resolves a skipped wall time; it has no option to reject it | Not supported | none |
| Moment.js | none | moment.tz always resolves a skipped wall time; it has no option to reject it | Not supported | none |
| @internationalized/date | parseZonedDateTime("2024-03-31T02:30\[Europe/Berlin\]", "reject") | RangeError: No such absolute time found | Same | none |

## M5. Add one day across a 30-minute DST change

Input: 2024-10-05T02:15:00+10:30\[Australia/Lord\_Howe\], plus 1 day (the clocks move 30 minutes on 2024-10-06). Compared as: local time and offset. Specification: [AddZonedDateTime (6.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-addzoneddatetime), [hoursInDay (6.3.23)](https://tc39.es/proposal-temporal/#sec-get-temporal.zoneddatetime.prototype.hoursinday).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-10-06T02:45:00+11:00\[Australia/Lord\_Howe\] | Reference | none |
| GMT | addZoned("2024-10-05T02:15:00+10:30\[Australia/Lord\_Howe\]", { days: 1 }) | 2024-10-06T02:45:00+11:00\[Australia/Lord\_Howe\] | Same | none |
| date-fns | addDays(new TZDate(2024, 9, 5, 2, 15, "Australia/Lord\_Howe"), 1) | Asia/Kolkata host: 2024-10-06T02:45:00.000+11:00, Europe/London host: 2024-10-06T02:45:00.000+11:00, Australia/Sydney host: 2024-10-06T03:15:00.000+11:00, America/Santiago host: 2024-10-06T02:45:00.000+11:00 | Depends on the host zone | none |
| Luxon | DateTime.fromISO("2024-10-05T02:15", { zone: "Australia/Lord\_Howe" }).plus({ days: 1 }).toISO() | 2024-10-06T02:45:00.000+11:00 | Same | none |
| Day.js | dayjs.tz("2024-10-05T02:15", "Australia/Lord\_Howe").add(1, "day").format() | Asia/Kolkata host: 2024-10-06T02:15:00+10:30, Europe/London host: 2024-10-06T02:15:00+10:30, Australia/Sydney host: 2024-10-06T03:15:00+10:30, America/Santiago host: 2024-10-06T02:15:00+10:30 | Depends on the host zone | none |
| Moment.js | moment.tz("2024-10-05T02:15", "Australia/Lord\_Howe").add(1, "day").format() | 2024-10-06T02:45:00+11:00 | Same | none |
| @internationalized/date | parseZonedDateTime("2024-10-05T02:15\[Australia/Lord\_Howe\]").add({ days: 1 }).toString() | 2024-10-06T02:45:00+11:00\[Australia/Lord\_Howe\] | Same | none |

## M6. Start of a day whose midnight does not exist

Input: 2024-09-08T12:00:00-03:00\[America/Santiago\], start of day (the clocks skip from 00:00 to 01:00). Compared as: local time and offset. Specification: [GetStartOfDay (11.1.14)](https://tc39.es/proposal-temporal/#sec-temporal-getstartofday), [startOfDay (6.3.45)](https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.startofday).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-09-08T01:00:00-03:00\[America/Santiago\] | Reference | none |
| GMT | startOfZoned("2024-09-08T12:00:00-03:00\[America/Santiago\]", "day") | 2024-09-08T01:00:00-03:00\[America/Santiago\] | Same | none |
| date-fns | startOfDay(new TZDate(2024, 8, 8, 12, 0, "America/Santiago")) | 2024-09-08T01:00:00.000-03:00 | Same | none |
| Luxon | DateTime.fromISO("2024-09-08T12:00", { zone: "America/Santiago" }).startOf("day").toISO() | 2024-09-08T01:00:00.000-03:00 | Same | none |
| Day.js | dayjs.tz("2024-09-08T12:00", "America/Santiago").startOf("day").format() | Asia/Kolkata host: 2024-09-08T00:00:00-04:00, Europe/London host: 2024-09-08T00:00:00-04:00, Australia/Sydney host: 2024-09-08T00:00:00-04:00, America/Santiago host: 2024-09-08T01:00:00-03:00 | Depends on the host zone | [iamkun/dayjs#1437](https://github.com/iamkun/dayjs/issues/1437), [iamkun/dayjs#2553](https://github.com/iamkun/dayjs/issues/2553), [iamkun/dayjs#2057](https://github.com/iamkun/dayjs/issues/2057) |
| Moment.js | moment.tz("2024-09-08T12:00", "America/Santiago").startOf("day").format() | 2024-09-08T01:00:00-03:00 | Same | none |
| @internationalized/date | toZoned(toCalendarDate(parseZonedDateTime("2024-09-08T12:00\[America/Santiago\]")), "America/Santiago") | 2024-09-08T01:00:00-03:00\[America/Santiago\] | Same | none |

## M7. Add one month to January 31

Input: 2024-01-31, plus 1 month. Compared as: date. Specification: [RegulateISODate (3.5.7)](https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate), [CalendarDateAdd (12.3.7)](https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-02-29 | Reference | none |
| GMT | addDate("2024-01-31", { months: 1 }) | 2024-02-29 | Same | none |
| date-fns | addMonths(parseISO("2024-01-31"), 1) | 2024-02-29 | Same | none |
| Luxon | DateTime.fromISO("2024-01-31").plus({ months: 1 }).toISODate() | 2024-02-29 | Same | none |
| Day.js | dayjs("2024-01-31").add(1, "month").format("YYYY-MM-DD") | 2024-02-29 | Same | none |
| Moment.js | moment("2024-01-31").add(1, "month").format("YYYY-MM-DD") | 2024-02-29 | Same | none |
| @internationalized/date | parseDate("2024-01-31").add({ months: 1 }).toString() | 2024-02-29 | Same | none |

## M8. Whole months between January 31 and February 29

Input: 2024-01-31 until 2024-02-29. Compared as: whole months. Specification: [CompareSurpasses (3.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-comparesurpasses), [CalendarDateUntil (12.3.9)](https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 0 | Reference | none |
| GMT | diffDate("2024-01-31", "2024-02-29", "months") | 0 | Same | none |
| date-fns | differenceInMonths(parseISO("2024-02-29"), parseISO("2024-01-31")) | 1 | Differs | none |
| Luxon | DateTime.fromISO("2024-02-29").diff(DateTime.fromISO("2024-01-31"), "months").months | 1 | Differs | none |
| Day.js | dayjs("2024-02-29").diff(dayjs("2024-01-31"), "month") | 1 | Differs | none |
| Moment.js | moment("2024-02-29").diff(moment("2024-01-31"), "months") | 1 | Differs | none |
| @internationalized/date | none | it has no month difference; compare() returns a count of days | Not supported | none |

## M9. Parse a date whose year has one digit

Input: "2-12-12" read as an ISO 8601 date. Compared as: date, or rejected. Specification: [ISO 8601 grammar (13.31)](https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar), [RFC 3339 section 5.6](https://www.rfc-editor.org/rfc/rfc3339#section-5.6).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | rejected | Reference | none |
| GMT | isValidDate("2-12-12") | rejected | Same | none |
| date-fns | parseISO("2-12-12") | rejected | Same | none |
| Luxon | DateTime.fromISO("2-12-12") | rejected | Same | none |
| Day.js | dayjs("2-12-12") | 2012-02-12 | Differs | none |
| Moment.js | moment("2-12-12", moment.ISO\_8601, true) | rejected | Same | none |
| @internationalized/date | parseDate("2-12-12") | Error: Invalid ISO 8601 date string: 2-12-12 | Same | none |

## M9d. Read a year of one digit with a pattern

Input: "2-12-12" read with the pattern year-month-day. Compared as: date, or rejected. Specification: [UTS #35 Date Format Patterns](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns), [ISO 8601 grammar (13.31)](https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar).

Temporal has no call for this. Each result is compared with the stated result, rejected.

Temporal reads no patterns, so the stated result is GMT's choice: `yyyy` is four digits, as the year is in the ISO 8601 grammar. UTS #35 recommends lenient pattern parsing, so a library that reads `2` as year 2 follows it. The rows show the difference and make no claim about which is right.

| Library | Call | Returns | Compared with the stated result | Upstream issue |
| --- | --- | --- | --- | --- |
| GMT | parseDateWithPattern("2-12-12", "yyyy-MM-dd") | rejected | Same | none |
| date-fns | parse("2-12-12", "yyyy-MM-dd", parseISO("2000-01-01")) | 0002-12-12 | Differs | [date-fns/date-fns#942](https://github.com/date-fns/date-fns/issues/942), [date-fns/date-fns#1569](https://github.com/date-fns/date-fns/issues/1569) |
| Luxon | DateTime.fromFormat("2-12-12", "yyyy-MM-dd") | rejected | Same | none |
| Day.js | dayjs("2-12-12", "YYYY-MM-DD", true) | rejected | Same | none |
| Moment.js | moment("2-12-12", "YYYY-MM-DD", true) | rejected | Same | none |
| @internationalized/date | none | it has no pattern parser; parseDate reads ISO 8601 only | Not supported | none |

## M9b. Parse a date that is not on the calendar

Input: "2024-02-30". Compared as: date, or rejected. Specification: [IsValidDate (13.31.2)](https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar-static-semantics-isvaliddate), [IsValidISODate (3.5.8)](https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | rejected | Reference | none |
| GMT | isValidDate("2024-02-30") | rejected | Same | none |
| date-fns | parseISO("2024-02-30") | rejected | Same | none |
| Luxon | DateTime.fromISO("2024-02-30") | rejected | Same | none |
| Day.js | dayjs("2024-02-30") | 2024-03-01 | Differs | none |
| Moment.js | moment("2024-02-30", moment.ISO\_8601, true) | rejected | Same | none |
| @internationalized/date | parseDate("2024-02-30") | RangeError: Value out of range: 1 \<= 30 \<= 29 | Same | none |

## M9c. Parse a date in basic format

Input: "20240212". Compared as: date, or rejected. Specification: [RFC 3339 section 5.6](https://www.rfc-editor.org/rfc/rfc3339#section-5.6), [ISO 8601 grammar (13.31)](https://tc39.es/proposal-temporal/#sec-temporal-iso8601grammar).

GMT validators take the extended form of RFC 3339 section 5.6 only, so `20240212` is rejected on purpose. Temporal accepts the basic form.

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-02-12 | Reference | none |
| GMT | isValidDate("20240212") | rejected | Differs | none |
| date-fns | parseISO("20240212") | 2024-02-12 | Same | none |
| Luxon | DateTime.fromISO("20240212") | 2024-02-12 | Same | none |
| Day.js | dayjs("20240212") | 2024-02-12 | Same | none |
| Moment.js | moment("20240212", moment.ISO\_8601, true) | 2024-02-12 | Same | none |
| @internationalized/date | parseDate("20240212") | Error: Invalid ISO 8601 date string: 20240212 | Differs | none |

## M10. Read a string with a bracketed time zone

Input: 2024-03-31T03:30:00+02:00\[Europe/Berlin\]. Compared as: UTC instant. Specification: [RFC 9557 section 4.1](https://www.rfc-editor.org/rfc/rfc9557#section-4.1), [InterpretISODateTimeOffset (6.5.1)](https://tc39.es/proposal-temporal/#sec-temporal-interpretisodatetimeoffset).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-03-31T01:30:00Z | Reference | none |
| GMT | convertZonedToUtc("2024-03-31T03:30:00+02:00\[Europe/Berlin\]") | 2024-03-31T01:30:00Z | Same | none |
| date-fns | parseISO("2024-03-31T03:30:00+02:00\[Europe/Berlin\]").toISOString() | 2024-03-31T03:30:00.000Z | Differs | none |
| Luxon | DateTime.fromISO("2024-03-31T03:30:00+02:00\[Europe/Berlin\]").toUTC().toISO() | 2024-03-31T01:30:00.000Z | Same | none |
| Day.js | dayjs("2024-03-31T03:30:00+02:00\[Europe/Berlin\]").toISOString() | rejected | Differs | none |
| Moment.js | moment("2024-03-31T03:30:00+02:00\[Europe/Berlin\]", moment.ISO\_8601, true) | rejected | Differs | none |
| @internationalized/date | parseZonedDateTime("2024-03-31T03:30:00+02:00\[Europe/Berlin\]").toAbsoluteString() | 2024-03-31T01:30:00.000Z | Same | none |

## M10b. Time zone name after reading a bracketed time zone

Input: 2024-03-31T03:30:00+02:00\[Europe/Berlin\]. Compared as: time zone name. Specification: [RFC 9557 section 4.1](https://www.rfc-editor.org/rfc/rfc9557#section-4.1), [ToTemporalZonedDateTime (6.5.2)](https://tc39.es/proposal-temporal/#sec-temporal-totemporalzoneddatetime).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | Europe/Berlin | Reference | none |
| GMT | parseTimeZoneFromZoned("2024-03-31T03:30:00+02:00\[Europe/Berlin\]") | Europe/Berlin | Same | none |
| date-fns | none | parseISO returns a Date; it keeps no time zone name | Not supported | none |
| Luxon | DateTime.fromISO("2024-03-31T03:30:00+02:00\[Europe/Berlin\]", { setZone: true }).zoneName | Europe/Berlin | Same | none |
| Day.js | none | it rejects the bracketed string, so there is no time zone to read | Not supported | none |
| Moment.js | none | strict ISO 8601 parsing rejects the bracketed string, so there is no time zone to read | Not supported | none |
| @internationalized/date | parseZonedDateTime("2024-03-31T03:30:00+02:00\[Europe/Berlin\]").timeZone | Europe/Berlin | Same | none |

## M10c. Read a string with a calendar annotation

Input: 2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]. Compared as: accepted or rejected, and for an accepted string the UTC instant. Specification: [RFC 9557 section 3.3](https://www.rfc-editor.org/rfc/rfc9557#section-3.3), [RFC 9557 section 4.1](https://www.rfc-editor.org/rfc/rfc9557#section-4.1), [ToTemporalZonedDateTime (6.5.2)](https://tc39.es/proposal-temporal/#sec-temporal-totemporalzoneddatetime), [ParseISODateTime (13.35)](https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime).

GMT's validator reports whether the string is accepted. The run also reads the instant with `convertZonedToUtc` from the string without its annotation, which that function does not take.

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | accepted | Reference | none |
| GMT | isValidCalendarZonedDateTime("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]") | accepted | Same | none |
| date-fns | parseISO("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]").toISOString() | accepted, reads 2024-03-31T03:30:00Z | Differs | none |
| Luxon | DateTime.fromISO("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]").toUTC().toISO() | rejected | Differs | [moment/luxon#1621](https://github.com/moment/luxon/issues/1621) |
| Day.js | dayjs("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]").toISOString() | rejected | Differs | none |
| Moment.js | moment("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]", moment.ISO\_8601, true).toISOString() | rejected | Differs | none |
| @internationalized/date | parseZonedDateTime("2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]").toAbsoluteString() | RangeError: Invalid time zone specified: Europe/Berlin\]\[u-ca=japanese | Differs | none |

## M10d. Read a string whose offset does not match its time zone

Input: 2022-07-08T00:14:07+01:00\[Europe/Paris\] (Paris is at +02:00 on that date). Compared as: UTC instant, or rejected. Specification: [RFC 9557 section 3.4](https://www.rfc-editor.org/rfc/rfc9557#section-3.4), [InterpretISODateTimeOffset (6.5.1)](https://tc39.es/proposal-temporal/#sec-temporal-interpretisodatetimeoffset), [GetTemporalOffsetOption (13.9)](https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloffsetoption).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | rejected | Reference | none |
| GMT | convertZonedToUtc("2022-07-08T00:14:07+01:00\[Europe/Paris\]") | rejected | Same | none |
| date-fns | parseISO("2022-07-08T00:14:07+01:00\[Europe/Paris\]").toISOString() | 2022-07-08T00:14:07.000Z | Differs | none |
| Luxon | DateTime.fromISO("2022-07-08T00:14:07+01:00\[Europe/Paris\]").toUTC().toISO() | 2022-07-07T22:14:07.000Z | Differs | none |
| Day.js | none | it rejects the bracketed string | Not supported | none |
| Moment.js | none | strict ISO 8601 parsing rejects the bracketed string | Not supported | none |
| @internationalized/date | parseZonedDateTime("2022-07-08T00:14:07+01:00\[Europe/Paris\]").toAbsoluteString() | Error: Offset +01:00 is invalid for 2022-07-08T00:14:07 in Europe/Paris | Same | none |

## M11. Read an offset with minutes before UTC

Input: 2024-01-15T12:00:00-03:30. Compared as: UTC instant. Specification: [RFC 3339 section 4.2](https://www.rfc-editor.org/rfc/rfc3339#section-4.2), [ParseDateTimeUTCOffset (14.6.11)](https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-01-15T15:30:00Z | Reference | none |
| GMT | toOffsetInstant("2024-01-15T12:00:00-03:30").instant | 2024-01-15T15:30:00Z | Same | none |
| date-fns | parseISO("2024-01-15T12:00:00-03:30").toISOString() | 2024-01-15T15:30:00.000Z | Same | none |
| Luxon | DateTime.fromISO("2024-01-15T12:00:00-03:30").toUTC().toISO() | 2024-01-15T15:30:00.000Z | Same | none |
| Day.js | dayjs("2024-01-15T12:00:00-03:30").toISOString() | 2024-01-15T15:30:00.000Z | Same | none |
| Moment.js | moment("2024-01-15T12:00:00-03:30").toISOString() | 2024-01-15T15:30:00.000Z | Same | none |
| @internationalized/date | parseAbsolute("2024-01-15T12:00:00-03:30", "UTC").toAbsoluteString() | 2024-01-15T14:30:00.000Z | Differs | [adobe/react-spectrum#10809](https://github.com/adobe/react-spectrum/issues/10809) |

## M12. ISO week and week-year at a year boundary

Input: 2024-12-30. Compared as: ISO week-year and week. Specification: [ISOWeekOfYear (12.3.18)](https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear), [ISO 8601-1 (week calendar)](https://www.iso.org/standard/70907.html).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2025-W01 | Reference | none |
| GMT | getIsoWeekDate("2024-12-30") | 2025-W01 | Same | none |
| date-fns | getISOWeekYear(parseISO("2024-12-30")), getISOWeek(parseISO("2024-12-30")) | 2025-W01 | Same | none |
| Luxon | DateTime.fromISO("2024-12-30").weekYear, .weekNumber | 2025-W01 | Same | none |
| Day.js | dayjs("2024-12-30").isoWeekYear(), .isoWeek() | 2025-W01 | Same | none |
| Moment.js | moment("2024-12-30").isoWeekYear(), .isoWeek() | 2025-W01 | Same | none |
| @internationalized/date | none | it has no ISO week call | Not supported | none |

## M13. Time zone name kept for Etc/UTC

Input: 2024-01-01T00:00:00Z shown in Etc/UTC. Compared as: time zone name. Specification: [Time Zone Identifiers (14.6.2)](https://tc39.es/proposal-temporal/#sec-time-zone-identifiers).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | Etc/UTC | Reference | none |
| GMT | parseTimeZoneFromZoned(convertUtcToZoned("2024-01-01T00:00:00Z", "Etc/UTC")) | Etc/UTC | Same | none |
| date-fns | new TZDate(parseISO("2024-01-01T00:00:00Z").getTime(), "Etc/UTC").timeZone | Etc/UTC | Same | none |
| Luxon | DateTime.fromISO("2024-01-01T00:00:00Z").setZone("Etc/UTC").zoneName | Etc/UTC | Same | none |
| Day.js | none | it has no public accessor for the zone name after tz() | Not supported | none |
| Moment.js | moment.tz("2024-01-01T00:00:00Z", "Etc/UTC").tz() | Etc/UTC | Same | none |
| @internationalized/date | parseAbsolute("2024-01-01T00:00:00Z", "Etc/UTC").timeZone | Etc/UTC | Same | none |

## M13b. UTC and Etc/UTC: same time zone or not

Input: 2024-01-01T00:00:00+00:00\[UTC\] and 2024-01-01T00:00:00+00:00\[Etc/UTC\]. Compared as: equal (true or false). Specification: [TimeZoneEquals (11.1.15)](https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals), [ZonedDateTime.prototype.equals (6.3.40)](https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.equals).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | true | Reference | none |
| GMT | areZonedEqual("2024-01-01T00:00:00+00:00\[UTC\]", "2024-01-01T00:00:00+00:00\[Etc/UTC\]") | true | Same | none |
| date-fns | none | isEqual compares instants; no call compares time zone names | Not supported | none |
| Luxon | DateTime.fromISO(s, { zone: "UTC" }).equals(DateTime.fromISO(s, { zone: "Etc/UTC" })) | false | Differs | [moment/luxon#1358](https://github.com/moment/luxon/issues/1358) |
| Day.js | none | isSame compares instants; no call compares time zone names | Not supported | none |
| Moment.js | none | isSame compares instants; no call compares time zone names | Not supported | none |
| @internationalized/date | none | compare() orders instants; no call compares time zone names | Not supported | none |

## M13c. Europe/Kiev and Europe/Kyiv: same time zone or not

Input: 2024-01-01T00:00:00+02:00\[Europe/Kiev\] and 2024-01-01T00:00:00+02:00\[Europe/Kyiv\]. Compared as: equal (true or false). Specification: [TimeZoneEquals (11.1.15)](https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | true | Reference | none |
| GMT | areZonedEqual("2024-01-01T00:00:00+02:00\[Europe/Kiev\]", "2024-01-01T00:00:00+02:00\[Europe/Kyiv\]") | true | Same | none |
| date-fns | none | isEqual compares instants; no call compares time zone names | Not supported | none |
| Luxon | DateTime.fromISO(s, { zone: "Europe/Kiev" }).equals(DateTime.fromISO(s, { zone: "Europe/Kyiv" })) | false | Differs | none |
| Day.js | none | isSame compares instants; no call compares time zone names | Not supported | none |
| Moment.js | none | isSame compares instants; no call compares time zone names | Not supported | none |
| @internationalized/date | none | compare() orders instants; no call compares time zone names | Not supported | none |

## M14. Offset written for a UTC instant

Input: 2024-03-31T01:30:00Z, written back as ISO 8601 with each library's default writer. Compared as: the text written: the local time and offset, or Z. Specification: [Temporal.Instant.prototype.toString (8.3.11)](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring), [RFC 9557 section 2](https://www.rfc-editor.org/rfc/rfc9557#section-2).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-03-31T01:30:00Z | Reference | none |
| GMT | toOffsetInstant("2024-03-31T01:30:00Z").instant | 2024-03-31T01:30:00Z | Same | none |
| date-fns | formatISO(parseISO("2024-03-31T01:30:00Z")) | Asia/Kolkata host: 2024-03-31T07:00:00+05:30, Europe/London host: 2024-03-31T02:30:00+01:00, Australia/Sydney host: 2024-03-31T12:30:00+11:00, America/Santiago host: 2024-03-30T22:30:00-03:00 | Depends on the host zone | [date-fns/date-fns#2151](https://github.com/date-fns/date-fns/issues/2151) |
| Luxon | DateTime.fromISO("2024-03-31T01:30:00Z").toISO() | Asia/Kolkata host: 2024-03-31T07:00:00.000+05:30, Europe/London host: 2024-03-31T02:30:00.000+01:00, Australia/Sydney host: 2024-03-31T12:30:00.000+11:00, America/Santiago host: 2024-03-30T22:30:00.000-03:00 | Depends on the host zone | none |
| Day.js | dayjs("2024-03-31T01:30:00Z").format() | Asia/Kolkata host: 2024-03-31T07:00:00+05:30, Europe/London host: 2024-03-31T02:30:00+01:00, Australia/Sydney host: 2024-03-31T12:30:00+11:00, America/Santiago host: 2024-03-30T22:30:00-03:00 | Depends on the host zone | none |
| Moment.js | moment("2024-03-31T01:30:00Z").format() | Asia/Kolkata host: 2024-03-31T07:00:00+05:30, Europe/London host: 2024-03-31T02:30:00+01:00, Australia/Sydney host: 2024-03-31T12:30:00+11:00, America/Santiago host: 2024-03-30T22:30:00-03:00 | Depends on the host zone | none |
| @internationalized/date | parseAbsolute("2024-03-31T01:30:00Z", "UTC").toAbsoluteString() | 2024-03-31T01:30:00.000Z | Same | none |

## M15. Add one business day with a Friday and Saturday weekend

Input: 2024-07-04 (a Thursday), plus 1 business day, weekend on Friday and Saturday (ISO weekdays 5 and 6). Compared as: date. Specification: [ISO 8601-1 (week calendar)](https://www.iso.org/standard/70907.html), [UTS #35 Week Data](https://www.unicode.org/reports/tr35/tr35-dates.html#Week_Data).

Temporal has no call for this. Each result is compared with the stated result, 2024-07-07.

| Library | Call | Returns | Compared with the stated result | Upstream issue |
| --- | --- | --- | --- | --- |
| GMT | addBusinessDays("2024-07-04", 1, { weekend: \[5, 6\], holidays: \[\], timeZone: "Asia/Riyadh" }) | 2024-07-07 | Same | none |
| date-fns | none | the weekend is fixed to Saturday and Sunday; addBusinessDays has no weekend option | Not supported | none |
| Luxon | none | it has no business-day call | Not supported | none |
| Day.js | none | it has no business-day call | Not supported | none |
| Moment.js | none | it has no business-day call | Not supported | none |
| @internationalized/date | none | it has no business-day call | Not supported | none |

## M16. Show the same instant in another time zone

Input: 2024-07-15T12:00:00+02:00\[Europe/Berlin\], shown in America/St\_Johns. Compared as: local time and offset. Specification: [withTimeZone (6.3.33)](https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.withtimezone).

| Library | Call | Returns | Compared with Temporal | Upstream issue |
| --- | --- | --- | --- | --- |
| Temporal | the proposal's own call | 2024-07-15T07:30:00-02:30\[America/St\_Johns\] | Reference | none |
| GMT | convertZonedToZoned("2024-07-15T12:00:00+02:00\[Europe/Berlin\]", "America/St\_Johns") | 2024-07-15T07:30:00-02:30\[America/St\_Johns\] | Same | none |
| date-fns | new TZDate(parseISO("2024-07-15T12:00:00+02:00").getTime(), "America/St\_Johns").toISOString() | 2024-07-15T07:30:00.000-02:30 | Same | none |
| Luxon | DateTime.fromISO("2024-07-15T12:00:00+02:00").setZone("America/St\_Johns").toISO() | 2024-07-15T07:30:00.000-02:30 | Same | none |
| Day.js | dayjs("2024-07-15T12:00:00+02:00").tz("America/St\_Johns").format() | 2024-07-15T07:30:00-02:30 | Same | none |
| Moment.js | moment.parseZone("2024-07-15T12:00:00+02:00").tz("America/St\_Johns").format() | 2024-07-15T07:30:00-02:30 | Same | none |
| @internationalized/date | toTimeZone(parseZonedDateTime("2024-07-15T12:00:00+02:00\[Europe/Berlin\]"), "America/St\_Johns") | 2024-07-15T07:30:00-02:30\[America/St\_Johns\] | Same | none |
