# Migrate from Luxon to Temporal with GMT

Checked against Luxon 3.7.2 on 2026-10-27.

[Migrate to Temporal with GMT](/gmt/migrate/) explains the one change that applies to every call below: GMT takes strings in and gives strings out.

## Call by call

| Luxon | GMT | Note |
| --- | --- | --- |
| `DateTime.fromISO(s)` | [`isValidDate`](/gmt/reference/plain/validate/isValidDate), [`isValidDateTime`](/gmt/reference/plain/validate/isValidDateTime), [`isValidZonedDateTime`](/gmt/reference/zoned/validate/isValidZonedDateTime) | The string is the value; check it with a validator. Luxon reads a `[zone]` only with `{ setZone: true }`. Temporal reads the basic format `20240212`; GMT rejects it on purpose and follows the extended format of RFC 3339 §5.6. |
| `DateTime.fromFormat(s, "dd/MM/yyyy")` | [`parseDateWithPattern`](/gmt/reference/plain/parse/parseDateWithPattern) |  |
| `DateTime.fromFormat(s, "yyyy MMMM")` | [`parseYearMonthWithPattern`](/gmt/reference/plain/parse/parseYearMonthWithPattern) | A year and a month with no day, returned as `YYYY-MM`. |
| `dt.isValid`, `dt.invalidReason` | [`isValidDate`](/gmt/reference/plain/validate/isValidDate) | GMT gives no reason string. |
| `dt.toLocaleString(DateTime.DATE_FULL)`, `dt.toFormat(f)` | [`formatDate`](/gmt/reference/plain/format/formatDate) | GMT has no token formatter; pass a locale and `dateStyle`. |
| `dt.toISO()`, `dt.toISODate()` | none | None; the value is already the string. `toISO()` omits the `[zone]`. |
| `DateTime.now().setZone(z)` | [`getZonedNow`](/gmt/reference/zoned/get/getZonedNow) |  |
| `DateTime.now().setZone(z).toISODate()` | [`getZonedToday`](/gmt/reference/zoned/get/getZonedToday) |  |
| `dt.plus({ days: 1 })` | [`addDate`](/gmt/reference/plain/calculate/addDate), [`addZoned`](/gmt/reference/zoned/calculate/addZoned) | The result across the Berlin gap is the same. |
| `dt.plus({ months: 1 })` | [`addDate`](/gmt/reference/plain/calculate/addDate) | The day is clamped to the month length. |
| `dt.minus({ … })` | [`subtractDate`](/gmt/reference/plain/calculate/subtractDate), [`subtractZoned`](/gmt/reference/zoned/calculate/subtractZoned) |  |
| `dt.plus({ hours: 1 })` | [`addZoned`](/gmt/reference/zoned/calculate/addZoned) |  |
| `b.diff(a, "days").days` | [`diffDate`](/gmt/reference/plain/calculate/diffDate), [`diffZoned`](/gmt/reference/zoned/calculate/diffZoned) | The arguments are in the other order: earlier first. |
| `b.diff(a, ["months", "days"])` | [`diffDate`](/gmt/reference/plain/calculate/diffDate) | From 2024-01-31 to 2024-02-29 Luxon gives 1 month and 0 days; GMT gives 0 months and 29 days. |
| `dt.startOf("month")` | [`startOfDate`](/gmt/reference/plain/calculate/startOfDate) |  |
| `dt.endOf("month")` | [`endOfDate`](/gmt/reference/plain/calculate/endOfDate) |  |
| `dt.setZone(z).startOf("day")` | [`startOfZoned`](/gmt/reference/zoned/calculate/startOfZoned) | The Santiago result is the same. |
| `a < b` | [`isBeforeDate`](/gmt/reference/plain/compare/isBeforeDate), [`isBeforeZoned`](/gmt/reference/zoned/compare/isBeforeZoned) |  |
| `a.equals(b)` | [`areZonedEqual`](/gmt/reference/zoned/compare/areZonedEqual) | `areZonedEqual` follows `ZonedDateTime.prototype.equals`. Luxon treats `UTC` and `Etc/UTC` as different zones. |
| `a.hasSame(b, "day")` | [`areZonedEqualBy`](/gmt/reference/zoned/compare/areZonedEqualBy), [`areDatesEqualBy`](/gmt/reference/plain/compare/areDatesEqualBy) |  |
| `Interval.fromDateTimes(s, e).contains(d)` | [`isBetweenDate`](/gmt/reference/plain/compare/isBetweenDate) | Luxon intervals are half-open. GMT includes both ends by default; pass `{ inclusiveEnd: false }`. |
| `i.overlaps(j)` | [`intervalsOverlapDate`](/gmt/reference/plain/interval/intervalsOverlapDate), [`intervalsOverlapZoned`](/gmt/reference/zoned/interval/intervalsOverlapZoned) |  |
| `DateTime.min(…)`, `DateTime.max(…)` | [`minDate`](/gmt/reference/plain/calculate/minDate), [`maxDate`](/gmt/reference/plain/calculate/maxDate), [`minZoned`](/gmt/reference/zoned/calculate/minZoned) |  |
| `dt.daysInMonth`, `dt.isInLeapYear` | [`getDaysInMonth`](/gmt/reference/plain/calculate/getDaysInMonth), [`getDaysInYearMonth`](/gmt/reference/plain/calculate/getDaysInYearMonth), [`isLeapYear`](/gmt/reference/plain/validate/isLeapYear) |  |
| `dt.weekYear`, `dt.weekNumber`, `dt.weeksInWeekYear` | [`getIsoWeekDate`](/gmt/reference/calendar/calculate/getIsoWeekDate), [`getWeeksInYear`](/gmt/reference/plain/calculate/getWeeksInYear) | `localWeekNumber` is a locale week. |
| `dt.isWeekend` | [`isWeekend`](/gmt/reference/plain/compare/isWeekend) | Both read the weekend from the locale. |
| `dt.setZone(z)` | [`convertZonedToZoned`](/gmt/reference/zoned/convert/convertZonedToZoned), [`convertUtcToZoned`](/gmt/reference/utc/convert/convertUtcToZoned) |  |
| `DateTime.fromISO(local, { zone: z })` | [`convertPlainDateTimeToZoned`](/gmt/reference/zoned/convert/convertPlainDateTimeToZoned) | Luxon has no disambiguation option; GMT takes `disambiguation`, with the four Temporal values. |
| `dt.toRelative()`, `Duration.fromISO(s).toHuman()` | [`formatRelativeZoned`](/gmt/reference/zoned/format/formatRelativeZoned), [`formatDuration`](/gmt/reference/duration/format/formatDuration) |  |

## What changes

- Luxon values are immutable `DateTime` objects. GMT takes and returns strings.
- Luxon counts months from 1 to 12, as GMT does.
- `b.diff(a)` subtracts the earlier value from the later one. `diffDate(a, b, unit)` takes the earlier value first.
- Invalid input gives a `DateTime` with `isValid` set to `false`, and `toISO()` returns `null`. GMT returns `""`, `null`, `false` or `[]`.
- Luxon intervals are half-open. GMT includes both ends in `isBetweenDate` unless you pass `inclusiveEnd: false`.

```js
import { diffDate } from "@northguild/gmt/plain/calculate";

diffDate("2024-03-01", "2024-03-15", "days"); // 14
```

## Basic-format dates

`DateTime.fromISO` reads `20240212`, as Temporal does. GMT's validators and parsers accept only the extended format, `2024-02-12`, which is the format of RFC 3339 §5.6, so `isValidDate("20240212")` returns `false`. This is deliberate. Convert a basic-format string before you pass it on. The behaviour matrix shows the library's result in case [M9c](/gmt/compare/behaviour/#m9c-luxon).

## Where Luxon differs from Temporal

Each item is a case where the library's result differs from Temporal's. The [full matrix](/gmt/compare/behaviour/) also lists the cases where it matches.

- **M8. Whole months between January 31 and February 29** Input: 2024-01-31 until 2024-02-29. The library returns 1; Temporal returns 0. Specification: [CompareSurpasses (3.5.5)](https://tc39.es/proposal-temporal/#sec-temporal-comparesurpasses). [Matrix row](/gmt/compare/behaviour/#m8-luxon).
- **M10c. Read a string with a calendar annotation** Input: 2024-03-31T03:30:00+02:00\[Europe/Berlin\]\[u-ca=japanese\]. The library returns rejected; Temporal returns accepted. Specification: [RFC 9557 section 3.3](https://www.rfc-editor.org/rfc/rfc9557#section-3.3). Upstream: [moment/luxon#1621](https://github.com/moment/luxon/issues/1621). [Matrix row](/gmt/compare/behaviour/#m10c-luxon).
- **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). The library returns 2022-07-07T22:14:07.000Z; Temporal returns rejected. Specification: [RFC 9557 section 3.4](https://www.rfc-editor.org/rfc/rfc9557#section-3.4). [Matrix row](/gmt/compare/behaviour/#m10d-luxon).
- **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\]. The library returns false; Temporal returns true. Specification: [TimeZoneEquals (11.1.15)](https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals). Upstream: [moment/luxon#1358](https://github.com/moment/luxon/issues/1358). [Matrix row](/gmt/compare/behaviour/#m13b-luxon).
- **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\]. The library returns false; Temporal returns true. Specification: [TimeZoneEquals (11.1.15)](https://tc39.es/proposal-temporal/#sec-temporal-timezoneequals). [Matrix row](/gmt/compare/behaviour/#m13c-luxon).
- **M14. Offset written for a UTC instant** Input: 2024-03-31T01:30:00Z, written back as ISO 8601 with each library's default writer. The library returns 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; Temporal returns 2024-03-31T01:30:00Z. Specification: [Temporal.Instant.prototype.toString (8.3.11)](https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring). [Matrix row](/gmt/compare/behaviour/#m14-luxon).

## More calls

| Luxon | GMT | Note |
| --- | --- | --- |
| `dt.quarter` | [`getQuarterForDate`](/gmt/reference/plain/calculate/getQuarterForDate) |  |
| `dt.set({ month })` | [`setDate`](/gmt/reference/plain/calculate/setDate) | Months are 1 to 12 in both. |
| `DateTime.fromSeconds(n)`, `dt.toUnixInteger()` | [`convertUnixToUtc`](/gmt/reference/unix/convert/convertUnixToUtc), [`convertUtcToUnix`](/gmt/reference/utc/convert/convertUtcToUnix) | Pass `{ epochUnit: "seconds" }`. |
| `dt.offset` | [`getZonedOffsetAs`](/gmt/reference/zoned/parse/getZonedOffsetAs) | Pass the unit `minutes`. |
| `dt.isInDST` | [`isInDaylightSaving`](/gmt/reference/zoned/compare/isInDaylightSaving) |  |
| `Info.isValidIANAZone(z)` | [`isValidTimeZone`](/gmt/reference/zoned/validate/isValidTimeZone) |  |
| `dt.ordinal` | [`getDayOfYear`](/gmt/reference/plain/calculate/getDayOfYear) |  |
