Luxon Manual Reference

Luxon Documentation

This is the manual for Luxon. You'll find guides below and an API doc reference here.

Install guide

Luxon provides different builds for different JS environments. See below for a link to the right one and instructions on how to use it. Luxon supports all modern platforms, but see the support matrix for additional details.

Basic browser setup

You can also load the files from a CDN.

Just include Luxon in a script tag. You can access its various classes through the luxon global.

<script src="luxon.js"></script>

You may wish to alias the classes you use:

var DateTime = luxon.DateTime;

Internet Explorer

If you're supporting IE 10 or 11, you need some polyfills to get Luxon to work. Use

<script src=",String.prototype.repeat,Array.prototype.find,Array.prototype.findIndex,Math.trunc,Math.sign,"></script>

See the support matrix for more information on what works and what doesn't in IE.


Supports Node 6+. Install via NPM:

npm install --save luxon
const { DateTime } = require("luxon");

If you want to work with locales, you'll need to have full-icu support installed in Node. You can build Node with it, use an NPM module to provide it, or find it prepackaged for your platform, like brew install node --with-full-icu. If you skip this step, Luxon still works but methods like setLocale() will do nothing.

The instructions for using full-icu as a package are a little confusing. Node can't automatically discover that you've installed the it, so you need to tell it where to find the data, like this:

npm install full-icu
node --icu-data-dir=./node_modules/full-icu

You can also point to the data with an environment var, like this:

NODE_ICU_DATA="$(pwd)/node_modules/full-icu" node

AMD (System.js, RequireJS, etc)

requirejs(["luxon"], function(luxon) {


import { DateTime } from "luxon";


npm install --save luxon
import { DateTime } from "luxon";


There are third-party typing files for Flow (via flow-typed) and TypeScript (via DefinitelyTyped).

For Flow, use:

flow-typed install luxon

For TypeScript, use:

npm install --save-dev @types/luxon

React Native

React Native works just fine, but React Native for Android doesn't ship with Intl support, which you need for a lot of Luxon's functionality. Use jsc-android-buildscripts to fix it.

A quick tour

Luxon is a library that makes it easier to work with dates and times in JavaScript. If you want, add and subtract them, format and parse them, ask them hard questions, and so on, Luxon provides a much easier and comprehensive interface than the native types it wraps. We're going to talk about the most immediately useful subset of that interface.

This is going to be a bit brisk, but keep in mind that the API docs are comprehensive, so if you want to know more, feel free to dive into them.

Your first DateTime

The most important class in Luxon is DateTime. A DateTime represents a specific millisecond in time, along with a time zone and a locale. Here's one that represents May 15, 2017 at 8:30 in the morning in the local time zone:

var dt = DateTime.local(2017, 5, 15, 8, 30);

DateTime.local takes any number of arguments, all the way out to milliseconds. Underneath, this is similar to a JavaScript Date object. But we've decorated it with lots of useful methods.

Creating a DateTime

There are lots of ways to create a DateTime by parsing strings or constructing them out of parts. You've already seen one, DateTime.local(), but let's talk about three more.

Get the current date and time

To get the current time, just do this:

var now =;

Calling is equivalent to calling local() with no parameter.

Create from an object

The most powerful way to create a DateTime instance is to provide an object containing all the information:

dt = DateTime.fromObject({day: 22, hour: 12, zone: 'America/Los_Angeles', numberingSystem: 'beng'})

Don't worry too much about the properties you don't understand yet; the point is that you can set every attribute of a DateTime when you create it. One thing to notice from the example is that we just set the day and hour; the year and month get defaulted to the current one and the minutes, seconds, and milliseconds get defaulted to 0. So DateTime.fromObject is sort of the power user interface.

Parse from ISO 8601

Luxon has lots of parsing capabilities, but the most important one is parsing ISO 8601 strings, because they're more-or-less the standard wire format for dates and times. Use DateTime.fromISO.

DateTime.fromISO("2017-05-15")          //=> May 15, 2017 at midnight
DateTime.fromISO("2017-05-15T08:30:00") //=> May 15, 2017 at 8:30

You can parse a bunch of other formats, including your own custom ones.

Getting to know your DateTime instance

Now that we've made some DateTimes, let's see what we can ask of it.


The first thing we want to see is the DateTime as a string. Luxon returns ISO 8601 strings:; //=> '2017-09-14T03:20:34.091-04:00'

Getting at components

We can get at the components of the time individually through getters. For example:

dt =;
dt.year     //=> 2017
dt.month    //=> 9      //=> 14
dt.second   //=> 47
dt.weekday  //=> 4

Other fun accessors

dt.zoneName     //=> 'America/New_York'
dt.offset       //=> -240
dt.daysInMonth  //=> 30

There are lots more!

Formatting your DateTime

You may want to output your DateTime to a string for a machine or a human to read. Luxon has lots of tools for this, but two of them are most important. If you want to format a human-readable string, use toLocaleString:

dt.toLocaleString()      //=> '9/14/2017'
dt.toLocaleString(DateTime.DATETIME_MED) //=> 'September 14, 3:21 AM'

This works well across different locales (languages) by letting the browser figure out what order the different parts go in and how to punctuate them.

If you want the string read by another program, you almost certainly want to use toISO:

dt.toISO() //=> '2017-09-14T03:21:47.070-04:00'

Custom formats are also supported. See formatting.

Transforming your DateTime


Luxon objects are immutable. That means that you can't alter them in place, just create altered copies. Throughout the documentation, we use terms like "alter", "change", and "set" loosely, but rest assured we mean "create a new instance with different properties".


This is easier to show than to tell. All of these calls return new DateTime instances:

var dt =;{ hours: 3, minutes: 2 });
dt.minus({ days: 7 });


You can create new instances by overriding specific properties:

var dt =;
dt.set({hour: 3}).hour   //=> 3


Luxon provides several different Intl capabilities, but the most important one is in formatting:

var dt =;
var f = {month: 'long', day: 'numeric'};
dt.setLocale('fr').toLocaleString(f)      //=> '14 septembre'
dt.setLocale('en-GB').toLocaleString(f)   //=> '14 September'
dt.setLocale('en-US').toLocaleString(f)  //=> 'September 14'

Luxon's Info class can also list months or weekdays for different locales:

Info.months('long', {locale: 'fr'}) //=> [ 'janvier', 'février', 'mars', 'avril', ... ]

Time zones

Luxon supports time zones. There's a whole big section about it. But briefly, you can create DateTimes in specific zones and change their zones:

DateTime.fromObject({zone: 'America/Los_Angeles'}) // now, but expressed in LA's local time"America/Los_Angeles"); // same

Luxon also supports UTC directly:

DateTime.utc(2017, 5, 15);
DateTime.utc(); // now, in UTC time zone;


The Duration class represents a quantity of time such as "2 hours and 7 minutes". You create them like this:

var dur = Duration.fromObject({ hours: 2, minutes: 7 });

They can be add or subtracted from DateTimes like this:;

They have getters just like DateTime:

dur.hours   //=> 2
dur.minutes //=> 7
dur.seconds //=> 0

And some other useful stuff:'seconds') //=> 7620
dur.toObject()    //=> { hours: 2, minutes: 7 }
dur.toISO()       //=> 'PT2H7M'

You can also format, negate, and normalize them. See it all in the Duration API docs.


Intervals are a specific period of time, such as "between now and midnight". They're really a wrapper for two DateTimes that form its endpoints. Here's what you can do with them:

now =;
later = DateTime.local(2020, 10, 12);
i = Interval.fromDateTimes(now, later);

i.length()                             //=> 97098768468
i.length('years')                //=> 3.0762420239726027
i.contains(DateTime.local(2019))       //=> true

i.toISO()       //=> '2017-09-14T04:07:11.532-04:00/2020-10-12T00:00:00.000-04:00'
i.toString()    //=> '[2017-09-14T04:07:11.532-04:00 – 2020-10-12T00:00:00.000-04:00)

Intervals can be split up into smaller intervals, perform set-like operations with other intervals, and few other handy features. See the Interval API docs.


Luxon uses the native Intl API to provide easy-to-use internationalization. A quick example:
  .toLocaleString(DateTime.DATE_FULL); //=>  '24 Σεπτεμβρίου 2017'

Making sure you have access to other locales

Please see the install guide for instructions on making sure your platform has access to the Intl APIs and the ICU data to power it. This especially important for Node, which doesn't ship with ICU data by default.

How locales work

Luxon DateTimes can be configured using BCP 47 locale strings specifying the language to use generating or interpreting strings. The native Intl API provides the actual internationalized strings; Luxon just wraps it with a nice layer of convenience and integrates the localization functionality into the rest of Luxon. The Mozilla MDN Intl docs have a good description of how the locale argument works. In Luxon, the methods are different but the semantics are the same, except in that Luxon allows you to specify a numbering system and output calendar independently of the locale string.

The rest of this document will concentrate on what Luxon does when provided with locale information.

Setting the locale

locale is a property of Luxon object. Thus, locale is a sort of setting on the DateTime object, as opposed to an argument you provide the different methods that need internationalized.

You can generally set it at construction time:

var dt = DateTime.fromISO("2017-09-24", { locale: "fr" });
dt.locale; //=> 'fr'

In this case, the specified locale didn't change the how the parsing worked (there's nothing localized about it), but it did set the locale property in the resulting instance. For other factory methods, such as fromFormat, the locale argument does affect how the string is parsed. See further down for more.

You can change the locale of a DateTime instance (meaning, create a clone DateTime with a different locale) using setLocale:"fr").locale; //=> 'fr'

setLocale is just a convenience for reconfigure:{ locale: "fr" }).locale; //=> 'fr'

Default locale

Out-of-the-box behavior

By default the locale property of a new DateTime or Duration is the system locale. On a browser, that means whatever the user has their browser or OS language set to. On Node, that usually means en-US.

As a result, DateTime#toLocaleString, DateTime#toLocaleParts, and other human-readable-string methods like Info.months will by default generate strings in the user's locale.

However, note that DateTime.fromFormat and DateTime#toFormat fall back on en-US. That's because these methods are often used to parse or format strings for consumption by APIs that don't care about the user's locale. So we need to pick a locale and stick with it, or the code will break depending on whose browser it runs in. There's an exception, though: DateTime#toFormat can take "macro" formats like "D" that produces localized strings as part of a larger string. These do default to the system locale because their entire purpose is to be provide localized strings.

Setting the default

You can set a default locale so that new instances will always be created with the specified locale:

Settings.defaultLocale = "fr";; //=> 'fr'

Note that this also alters the behavior of DateTime#toFormat and DateTime#fromFormat.

Using the system locale in string parsing

You generally don't want DateTime#fromFormat and DateTime#toFormat to use the system's locale, since your format won't be sensitive to the locale's string ordering. That's why Luxon doesn't behave that way by default. But if you really want that behavior, you can always do this:

Settings.defaultLocale =;

Checking what you got

The local environment may not support the exact locale you asked for. The native Intl API will try to find the best match. If you want to know what that match was, use resolvedLocaleOpts:

DateTime.fromObject({ locale: "fr-co" }).resolvedLocaleOpts(); //=> { locale: 'fr',
//     numberingSystem: 'latn',
//     outputCalendar: 'gregory' }

Methods affected by the locale


The most important method affected by the locale setting is toLocaleString, which allows you to produce internationalized, human-readable strings.

dt.setLocale("fr").toLocaleString(DateTime.DATE_FULL); //=> '25 septembre 2017'

That's the normal way to do it: set the locale as property of the DateTime itself and let the toLocaleString inherit it. But you can specify the locale directly to toLocaleString too:

dt.toLocaleString(Object.assign({ locale: "es" }, DateTime.DATE_FULL)); //=> '25 de septiembre de 2017'

Ad-hoc formatting also respects the locale:

dt.setLocale("fr").toFormat("MMMM dd, yyyy GG"); //=> 'septembre 25, 2017 après Jésus-Christ'


You can parse localized strings:

DateTime.fromFormat("septembre 25, 2017 après Jésus-Christ", "MMMM dd, yyyy GG", { locale: "fr" });


Some of the methods in the Info class let you list strings like months, weekdays, and eras, and they can be localized:

Info.months("long", { locale: "fr" }); //=> [ 'janvier', 'février', ...
Info.weekdays("long", { locale: "fr" }); //=> [ 'lundi', 'mardi', ...
Info.eras("long", { locale: "fr" }); //=> [ 'avant Jésus-Christ', 'après Jésus-Christ' ]


DateTimes also have a numberingSystem setting that lets you control what system of numerals is used in formatting. In general, you shouldn't override the numbering system provided by the locale. For example, no extra work is needed to get Arabic numbers to show up in Arabic-speaking locales:

var dt ="ar");

dt.resolvedLocaleOpts(); //=> { locale: 'ar',
//     numberingSystem: 'arab',
//     outputCalendar: 'gregory' }

dt.toLocaleString(); //=> '٢٤‏/٩‏/٢٠١٧'

For this reason, Luxon defaults its own numberingSystem property to null, by which it means "let the Intl API decide". However, you can override it if you want. This example is admittedly ridiculous:

var dt ={ locale: "it", numberingSystem: "beng" });
dt.toLocaleString(DateTime.DATE_FULL); //=> '২৪ settembre ২০১৭'

Similar to locale, you can set the default numbering system for new instances:

Settings.defaultNumberingSystem = "beng";

Time zones and offsets

Luxon has support for time zones. This page explains how to use them.

Don't worry!

You usually don't need to worry about time zones. Your code runs on a computer with a particular time zone and everything will work consistently in that zone without you doing anything. It's when you want to do complicated stuff across zones that you have to think about it. Even then, here are some pointers to help you avoid situations where you have to think carefully about time zones:

  1. Don't make servers think about local times. Configure them to use UTC and write your server's code to work in UTC. Times can often be thought of as a simple count of epoch milliseconds; what you would call that time (e.g. 9:30) in what zone doesn't (again, often) matter.
  2. Communicate times between systems in ISO 8601, like "2017-05-15T13:30:34Z" where possible (it doesn't matter if you use Z or some local offset; the point is that it precisely identifies the millisecond on the global timeline).
  3. Where possible, only think of time zones as a formatting concern; your application ideally never knows that the time it's working with is called "9:00" until it's being rendered to the user.
  4. Barring 3, do as much manipulation of the time (say, adding an hour to the current time) in the client code that's already running in the time zone where the results will matter.

All those things will make it less likely you ever need to work explicitly with time zones and may also save you plenty of other headaches. But those aren't possible for some applications; you might need to work with times in zones other than the one the program is running in, for any number of reasons. And that's where Luxon's time zone support comes in.


Bear with me here. Time zones are pain in the ass. Luxon has lots of tools to deal with them, but there's no getting around the fact that they're complicated. The terminology for time zones and offsets isn't well-established. But let's try to impose some order:

  1. An offset is a difference between the local time and the UTC time, such as +5 (hours) or -12:30. They may be expressed directly in minutes, or in hours, or in a combination of minutes and hours. Here we'll use hours.
  2. A time zone is a set of rules, associated with a geographical location, that determines the local offset from UTC at any given time. The best way to identify a zone is by its IANA string, such as "America/New_York". That zone says something to the effect of "The offset is -4, except between March and November, when it's -5".
  3. A fixed-offset time zone is any time zone that never changes offsets, such as UTC. Luxon supports fixed-offset zones directly; they're specified like UTC+7, which you can interpret as "always with an offset of +7".
  4. A named offset is a time zone-specific name for an offset, such as Eastern Daylight Time. It expresses both the zone (America's EST roughly implies America/New_York) and the current offset (EST means -4). They are also confusing in that they overspecify the offset (e.g. for any given time it is unnecessary to specify EST vs EDT; it's always whichever one is right). They are also ambiguous (BST is both British Summer Time and Bangladesh Standard Time), unstandardized, and internationalized (what would a Frenchman call the US's EST?). For all these reasons, you should avoid them when specifying times programmatically. Luxon only supports their use in formatting.

Some subtleties:

  1. Multiple zones can have the same offset (think about the US's zones and their Canadian equivalents), though they might not have the same offset all the time, depending on when their DSTs are. Thus zones and offsets have a many-to-many relationship.
  2. Just because a time zone doesn't have a DST now doesn't mean it's fixed. Perhaps it had one in the past. Regardless, Luxon does not have first-class access to the list of rules, so it assumes any IANA-specified zone is not fixed and checks for its current offset programmatically.

If all this seems too terse, check out these articles. The terminology in them is subtly different but the concepts are the same:

Luxon works with time zones

Luxon's DateTime class supports zones directly. By default, a date created in Luxon is "in" the local time zone of the machine it's running on. By "in" we mean that the DateTime has, as one of its properties, an associated zone.

It's important to remember that a DateTime represents a specific instant in time and that instant has an unambiguous meaning independent of what time zone you're in; the zone is really piece of social metadata that affects how humans interact with the time, rather than a fact about the passing of time itself. Of course, Luxon is a library for humans, so that social metadata affects Luxon's behavior too. It just doesn't change what time it is.

Specifically, a DateTime's zone affects its behavior in these ways:

  1. Times will be formatted as they would be in that zone.
  2. Transformations to the DateTime (such as plus or startOf) will obey any DSTs in that zone that affect the calculation (see "Math across DSTs" below)

Generally speaking, Luxon does not support changing a DateTime's offset, just its zone. That allows it to enforce the behaviors in the list above. The offset for that DateTime is just whatever the zone says it is. If you are unconcerned with the effects above, then you can always give your DateTime a fixed-offset zone.

Specifying a zone

Luxon's API methods that take a zone as an argument all let you specify the zone in a few ways.

Type Example Description
IANA 'America/New_York' that zone
local 'local' the system's local zone
UTC 'utc' Universal Coordinated Time
fixed offset 'UTC+7' a fixed offset zone
Zone new YourZone() A custom implementation of Luxon's Zone interface (advanced only)

IANA support

IANA-specified zones are string identifiers like "America/New_York" or "Asia/Tokyo". Luxon gains direct support for them by abusing built-in Intl APIs. However, your environment may not support them, in which case, you can't fiddle with the zones directly. You can always use the local zone your system is in, UTC, and any fixed-offset zone like UTC+7. You can check if your runtime environment supports IANA zones with our handy utility:

Info.features().zones; //=> true

If you're unsure if all your target environments (browser versions and Node versions) support this, check out the Support Matrix. You can generally count on modern browsers to have this feature, except IE (it is supported in Edge). You may also polyfill your environment.

If you specify a zone and your environment doesn't support that zone, you'll get an invalid DateTime. That could be because the environment doesn't support zones at all, because for whatever reason it doesn't support that particular zone, or because the zone is just bogus. Like this:

bogus = DateTime.local().setZone("America/Bogus");

bogus.isValid; //=> false
bogus.invalidReason; //=> 'unsupported zone'

Creating DateTimes

Local by default

By default, DateTime instances are created in the system's local zone and parsed strings are interpreted as specifying times in the system's local zone. For example, my computer is configured to use America/New_York, which has an offset of -4 in May:

var local = DateTime.local(2017, 05, 15, 09, 10, 23);

local.zoneName; //=> 'America/New_York'
local.toString(); //=> '2017-05-15T09:10:23.000-04:00'

var iso = DateTime.fromISO("2017-05-15T09:10:23");

iso.zoneName; //=> 'America/New_York'
iso.toString(); //=> '2017-05-15T09:10:23.000-04:00'

Creating DateTimes in a zone

Many of Luxon's factory methods allow you to tell it specifically what zone to create the DateTime in:

var overrideZone = DateTime.fromISO("2017-05-15T09:10:23", { zone: "Europe/Paris" });

overrideZone.zoneName; //=> 'Europe/Paris'
overrideZone.toString(); //=> '2017-05-15T09:10:23.000+02:00'

Note two things:

  1. The date and time specified in the string was interpreted as a Parisian local time (i.e. it's the time that corresponds to what would be called 9:10 there).
  2. The resulting DateTime object is in Europe/Paris.

Those are conceptually independent (i.e. Luxon could have converted the time to the local zone), but it practice it's more convenient for the same option to govern both.

In addition, one static method, utc(), specifically interprets the input as being specified in UTC. It also returns a DateTime in UTC:

var utc = DateTime.utc(2017, 05, 15, 09, 10, 23);

utc.zoneName; //=> 'UTC'
utc.toString(); //=> '2017-05-15T09:10:23.000Z'

Strings that specify an offset

Some input strings may specify an offset as part of the string itself. In these cases, Luxon interprets the time as being specified with that offset, but converts the resulting DateTime into the system's local zone:

var specifyOffset = DateTime.fromISO("2017-05-15T09:10:23-09:00");

specifyOffset.zoneName; //=> 'America/New_York'
specifyOffset.toString(); //=> '2017-05-15T14:10:23.000-04:00'

var specifyZone = DateTime.fromFormat(
  "2017-05-15T09:10:23 Europe/Paris",
  "yyyy-MM-dd'T'HH:mm:ss z"

specifyZone.zoneName; //=> 'America/New_York'
specifyZone.toString(); //=> '2017-05-15T03:10:23.000-04:00'

...unless a zone is specified as an option (see previous section), in which case the DateTime gets converted to that zone:

var specifyOffsetAndOverrideZone = DateTime.fromISO("2017-05-15T09:10:23-09:00", {
  zone: "Europe/Paris"

specifyOffsetAndOverrideZone.zoneName; //=> 'Europe/Paris'
specifyOffsetAndOverrideZone.toString(); //=> '2017-05-15T20:10:23.000+02:00'


Finally, some parsing functions allow you to "keep" the zone in the string as the DateTime's zone. Note that if only an offset is provided by the string, the zone will be a fixed-offset one, since Luxon doesn't know which zone is meant, even if you do.

var keepOffset = DateTime.fromISO("2017-05-15T09:10:23-09:00", { setZone: true });

keepOffset.zoneName; //=> 'UTC-9'
keepOffset.toString(); //=> '2017-05-15T09:10:23.000-09:00'

var keepZone = DateTime.fromFormat("2017-05-15T09:10:23 Europe/Paris", "yyyy-MM-dd'T'HH:mm:ss z", {
  setZone: true

keepZone.zoneName; //=> 'Europe/Paris'
keepZone.toString(); //=> '2017-05-15T09:10:23.000+02:00'

Changing zones


Luxon objects are immutable, so when we say "changing zones" we really mean "creating a new instance with a different zone". Changing zone generally means "change the zone in which this DateTime is expressed (and according to which rules it is manipulated), but don't change the underlying timestamp." For example:

var local = DateTime.local();
var rezoned = local.setZone("America/Los_Angeles");

// different local times with different offsets
local.toString(); //=> '2017-09-13T18:30:51.141-04:00'
rezoned.toString(); //=> '2017-09-13T15:30:51.141-07:00'

// but actually the same time
local.valueOf() === rezoned.valueOf(); //=> true


Generally, it's best to think of the zone as a sort of metadata that you slide around independent of the underlying count of milliseconds. However, sometimes that's not what you want. Sometimes you want to change zones while keeping the local time fixed and instead altering the timestamp. Luxon supports this:

var local = DateTime.local();
var rezoned = local.setZone("America/Los_Angeles", { keepLocalTime: true });

local.toString(); //=> '2017-09-13T18:36:23.187-04:00'
rezoned.toString(); //=> '2017-09-13T18:36:23.187-07:00'

local.valueOf() === rezoned.valueOf(); //=> false

If you find that confusing, I recommend just not using it. On the other hand, if you find yourself using this all the time, you are probably doing something wrong.


Luxon DateTimes have a few different accessors that let you find out about the zone and offset:

var dt = DateTime.local();

dt.zoneName; //=> 'America/New_York'
dt.offset; //=> -240
dt.offsetNameShort; //=> 'EDT'
dt.offsetNameLong; //=> 'Eastern Daylight Time'
dt.isOffsetFixed; //=> false
dt.isInDST; //=> true

Those are all documented in the DateTime API docs.

DST weirdness

Because our ancestors were morons, they opted for a system wherein many governments shift around the local time twice a year for no good reason. And it's not like they do it in a neat, coordinated fashion. No, they do it whimsically, varying the shifts' timing from country to country (or region to region!) and from year to year. And of course, they do it the opposite way south of the Equator. This is all a tremendous waste of everyone's energy and, er, time, but it is how the world works and a date and a time library has to deal with it.

Most of the time, DST shifts will happen without you having to do anything about it and everything will just work. Luxon goes to some pains to make DSTs as unweird as possible. But there are exceptions. This section covers them.

Invalid times

Some local times simply don't exist. The Spring Forward DST shift involves shifting the local time forward by (usually) one hour. In my zone, America/New_York, on March 12, 2017 the millisecond after 1:59:59.999 is 3:00:00.000. Thus the times between 2:00:00.000 and 2:59:59.000, inclusive, don't exist in that zone. But of course, nothing stops a user from constructing a DateTime out of that local time.

If you create such a DateTime from scratch, the missing time will be advanced by an hour:

DateTime.local(2017, 3, 12, 2, 30).toString(); //=> '2017-03-12T03:30:00.000-04:00'

You can also do date math that lands you in the middle of the shift. These also push forward:

DateTime.local(2017, 3, 11, 2, 30)
  .plus({ days: 1 })
  .toString(); //=> '2017-03-12T03:30:00.000-04:00'
DateTime.local(2017, 3, 13, 2, 30)
  .minus({ days: 1 })
  .toString(); //=> '2017-03-12T03:30:00.000-04:00'

Ambiguous times

Harder to handle are ambiguous times. During Fall Back, some local times happen twice. In my zone, America/New_York, on November 5, 2017 the millisecond after 1:59:59.000 became 1:00:00.000. But of course there was already a 1:00 that day, one hour before before this one. So if you create a DateTime with a local time of 1:30, which time do you mean? It's an important question, because they correspond to different moments in time.

However, Luxon's behavior here is undefined. It makes no promises about which of the two possible timestamps the instance will represent. Currently, its specific behavior is like this:

DateTime.local(2017, 11, 5, 1, 30).offset / 60; //=> -4
DateTime.local(2017, 11, 4, 1, 30).plus({ days: 1 }).offset / 60; //=> -4
DateTime.local(2017, 11, 6, 1, 30).minus({ days: 1 }).offset / 60; //=> -5

In other words, sometimes it picks one and sometimes the other. Luxon doesn't guarantee the specific behavior above. That's just what it happens to do.

If you're curious, this lack of definition is because Luxon doesn't actually know that any particular DateTime is an ambiguous time. It doesn't know the time zones rules at all. It just knows the local time does not contradict the offset and leaves it at that. To find out the time is ambiguous and define exact rules for how to resolve it, Luxon would have to test nearby times to see if it can find duplicate local time, and it would have to do that on every creation of a DateTime, regardless of whether it was anywhere near a real DST shift. Because that's onerous, Luxon doesn't bother.

Math across DSTs

There's a whole section about date and time math, but it's worth highlighting one thing here: when Luxon does math across DSTs, it adjusts for them when working with higher-order, variable-length units like days, weeks, months, and years. When working with lower-order, exact units like hours, minutes, and seconds, it does not. For example, DSTs mean that days are not always the same length: one day a year is (usually) 23 hours long and another is 25 hours long. Luxon makes sure that adding days takes that into account. On the other hand, an hour is always 3,600,000 milliseconds.

An easy way to think of it is that if you add a day to a DateTime, you should always get the same time the next day, regardless of any intervening DSTs. On the other hand, adding 24 hours will result in DateTime that is 24 hours later, which may or may not be the same time the next day. In this example, my zone is America/New_York, which had a Spring Forward DST in the early hours of March 12.

var start = DateTime.local(2017, 3, 11, 10);
start.hour; //=> 10, just for comparison{ days: 1 }).hour; //=> 10, stayed the same{ hours: 24 }).hour; //=> 11, DST pushed forward an hour

Changing the default zone

By default, Luxon creates DateTimes in the system's local zone. However, you can override this behavior globally:

Settings.defaultZoneName = "Asia/Tokyo";
DateTime.local().zoneName; //=> 'Asia/Tokyo'

Settings.defaultZoneName = "utc";
DateTime.local().zoneName; //=> 'UTC'

// you can reset by setting to 'local'

Settings.defaultZoneName = "local";
DateTime.local().zoneName; //=> 'America/New_York'


This covers Luxon's support for various calendar systems. If you don't need to use non-standard calendars, you don't need to read any of this.

Fully supported calendars

Luxon has full support for Gregorian and ISO Week calendars. What I mean by that is that Luxon can parse dates specified in those calendars, format dates into strings using those calendars, and transform dates using the units of those calendars. For example, here is Luxon working directly with an ISO calendar:

DateTime.fromISO('2017-W23-3').plus({ weeks: 1, days: 2 }).toISOWeekDate(); //=>  '2017-W24-5'

The main reason I bring all this is up is to contrast it with the capabilities for other calendars described below.

Output calendars

Luxon has limited support for other calendaring systems. Which calendars are supported at all is a platform-dependent, but can generally be expected to be these: Buddhist, Chinese, Coptic, Ethioaa, Ethiopic, Hebrew, Indian, Islamic, Islamicc, Japanese, Persian, and ROC. Support is limited to formatting strings with them, hence the qualified name "output calendar".

In practice this is pretty useful; you can show users the date in their preferred calendaring system while the software works with dates using Gregorian units or Epoch milliseconds. But the limitations are real enough; Luxon doesn't know how to do things like "add one Islamic month".

The output calendar is a property of the DateTime itself. For example:

var dtHebrew ={ outputCalendar: "hebrew" });
dtHebrew.outputCalendar; //=> 'hebrew'
dtHebrew.toLocaleString() //=> '4 Tishri 5778'

You can modulate the structure of that string with arguments to toLocaleString (see the docs on that), but the point here is just that you got the alternative calendar.

Generally supported calendars

Here's a table of the different calendars with examples generated formatting the same date generated like this:

DateTime.fromObject({ outputCalendar: c }).toLocaleString(DateTime.DATE_FULL);
Calendar Example
buddhist September 24, 2560 BE
chinese Eighth Month 5, 2017
coptic Tout 14, 1734 ERA1
ethioaa Meskerem 14, 7510 ERA0
ethiopic Meskerem 14, 2010 ERA1
hebrew 4 Tishri 5778
indian Asvina 2, 1939 Saka
islamic Muharram 4, 1439 AH
islamicc Muharram 3, 1439 AH
japanese September 24, 29 Heisei
persian Mehr 2, 1396 AP
roc September 24, 106 Minguo

Default output calendar

You can set the default output calendar for new DateTime instances like this:

Settings.defaultOuputCalendar = 'persian';


This section covers creating strings to represent a DateTime. There are three types of formatting capabilities:

  1. Technical formats like ISO 8601 and RFC 2822
  2. Internationalizable human-readable formats
  3. Token-based formatting

Technical formats (strings for computers)

ISO 8601

ISO 8601 is the most widely used set of string formats for dates and times. Luxon can parse a wide range of them, but provides direct support for formatting only a few of them:

dt.toISO(); //=> '2017-04-20T11:32:00.000-04:00'
dt.toISODate(); //=> '2017-04-20'
dt.toISOWeekDate(); //=> '2017-W17-7'
dt.toISOTime(); //=> '11:32:00.000-04:00'

Generally, you'll want the first one. Use it by default when building or interacting with APIs, communicating times over a wire, etc.

HTTP and RFC 2822

There are a number of legacy standard date and time formats out there, and Luxon supports some of them. You shouldn't use them unless you have a specific reason to.

dt.toRFC2822(); //=> 'Thu, 20 Apr 2017 11:32:00 -0400'
dt.toHTTP(); //=> 'Thu, 20 Apr 2017 03:32:00 GMT'

Unix timestamps

DateTime objects can also be converted to numerical Unix timestamps:

dt.toMillis(); //=> 1492702320000
dt.toSeconds(); //=> 1492702320
dt.valueOf(); //=> 1492702320000, same as .toMillis()

toLocaleString (strings for humans)

The basics

Modern browsers (and other JS environments) provide support for human-readable, internationalized strings. Luxon provides convenient support for them, and you should use them anytime you want to display a time to a user. Use toLocaleString to do it:

dt.toLocaleString(); //=> '4/20/2017'
dt.toLocaleString(DateTime.DATETIME_FULL); //=> 'April 20, 2017, 11:32 AM EDT'
dt.setLocale('fr').toLocaleString(DateTime.DATETIME_FULL); //=> '20 avril 2017 à 11:32 UTC−4'


In the example above, DateTime.DATETIME_FULL is one of several convenience formats provided by Luxon. But the arguments are really any object of options that can be provided to Intl.DateTimeFormat. For example:

dt.toLocaleString({ month: 'long', day: 'numeric' }); //=> 'April 20'

And that's all the preset is:

DateTime.DATETIME_FULL;  //=> {
                         //     year: 'numeric',
                         //     month: 'long',
                         //     day: 'numeric',
                         //     hour: 'numeric',
                         //     minute: '2-digit',
                         //     timeZoneName: 'short'
                         //   }

This also means you can modify the presets as you choose:

dt.toLocaleString(DateTime.DATE_SHORT); //=>  '4/20/2017'
var newFormat = Object.assign(DateTime.DATE_SHORT, { weekday: 'long' });
dt.toLocaleString(newFormat); //=>  'Thursday, 4/20/2017'


Here's the full set of provided presets using the October 14, 1983 at 13:30:23 as an example.

Name Description Example in en_US Example in fr
DATE_SHORT short date 10/14/1983 14/10/1983
DATE_MED abbreviated date Oct 14, 1983 14 oct. 1983
DATE_MED_WITH_WEEKDAY abbreviated date with weekday Fri, Oct 14, 1983 ven. 14 oct. 1983
DATE_FULL full date October 14, 1983 14 octobre 1983
DATE_HUGE full date with weekday Tuesday, October 14, 1983 vendredi 14 octobre 1983
TIME_SIMPLE time 1:30 PM 13:30
TIME_WITH_SECONDS time with seconds 1:30:23 PM 13:30:23
TIME_WITH_SHORT_OFFSET time with seconds and abbreviated named offset 1:30:23 PM EDT 13:30:23 UTC−4
TIME_WITH_LONG_OFFSET time with seconds and full named offset 1:30:23 PM Eastern Daylight Time 13:30:23 heure d’été de l’Est
TIME_24_SIMPLE 24-hour time 13:30 13:30
TIME_24_WITH_SECONDS 24-hour time with seconds 13:30:23 13:30:23
TIME_24_WITH_SHORT_OFFSET 24-hour time with seconds and abbreviated named offset 13:30:23 EDT 13:30:23 UTC−4
TIME_24_WITH_LONG_OFFSET 24-hour time with seconds and full named offset 13:30:23 Eastern Daylight Time 13:30:23 heure d’été de l’Est
DATETIME_SHORT short date & time 10/14/1983, 1:30 PM 14/10/1983 à 13:30
DATETIME_MED abbreviated date & time Oct 14, 1983, 1:30 PM 14 oct. 1983 à 13:30
DATETIME_FULL full date and time with abbreviated named offset October 14, 1983, 1:30 PM EDT 14 octobre 1983 à 13:30 UTC−4
DATETIME_HUGE full date and time with weekday and full named offset Friday, October 14, 1983, 1:30 PM Eastern Daylight Time vendredi 14 octobre 1983 à 13:30 heure d’été de l’Est
DATETIME_SHORT_WITH_SECONDS short date & time with seconds 10/14/1983, 1:30:23 PM 14/10/1983 à 13:30:23
DATETIME_MED_WITH_SECONDS abbreviated date & time with seconds Oct 14, 1983, 1:30:23 PM 14 oct. 1983 à 13:30:23
DATETIME_FULL_WITH_SECONDS full date and time with abbreviated named offset with seconds October 14, 1983, 1:30:23 PM EDT 14 octobre 1983 à 13:30:23 UTC−4
DATETIME_HUGE_WITH_SECONDS full date and time with weekday and full named offset with seconds Friday, October 14, 1983, 1:30:23 PM Eastern Daylight Time vendredi 14 octobre 1983 à 13:30:23 heure d’été de l’Est


toLocaleString's behavior is affected by the DateTime's locale, numberingSystem, and outputCalendar properties. See the Intl section for more.

Formatting with tokens (strings for Cthulhu)

This section covers generating strings from DateTimes with programmer-specified formats.

Consider alternatives

You shouldn't create ad-hoc string formats if you can avoid it. If you intend for a computer to read the string, prefer ISO 8601. If a human will read it, prefer toLocaleString. Both are covered above. However, if you have some esoteric need where you need some specific format (e.g. because some other software expects it), then toFormat is how you do it.


See DateTime#toFormat for the API signature. As a brief motivating example:

DateTime.fromISO('2014-08-06T13:07:04.054').toFormat('yyyy LLL dd'); //=> '2014 Aug 06'

The supported tokens are described in the table below.


All of the strings (e.g. month names and weekday names) are internationalized by introspecting strings generated by the Intl API. Thus the exact strings you get are implementation-specific.

  .toFormat('yyyy LLL dd'); //=> '2014 août 06'


You may escape strings using single quotes:"HH 'hours and' mm 'minutes'"); //=> '20 hours and 55 minutes'

Standalone vs format tokens

Some tokens have a "standalone" and "format" version. Some languages require different forms of a word based on whether it is part of a longer phrase or just by itself (e.g. "Monday the 22nd" vs "Monday"). Use them accordingly.

var d = DateTime.fromISO('2014-08-06T13:07:04.054').setLocale('ru');
d.toFormat('LLLL'); //=> 'август' (standalone)
d.toFormat('MMMM'); //=> 'августа' (format)

Macro tokens

Some of the formats are "macros", meaning they correspond to multiple components. These use the native Intl API and will order their constituent parts in a locale-friendly way.

DateTime.fromISO('2014-08-06T13:07:04.054').toFormat('ff'); //=> 'Aug 6, 2014, 1:07 PM'

The macro options available correspond one-to-one with the preset formats defined for toLocaleString.

Table of tokens

(Examples below given for 2014-08-06T13:07:04.054 considered as a local time in America/New_York).

Standalone token Format token Description Example
S millisecond, no padding 54
SSS millisecond, padded to 3 054
u fractional seconds, functionally identical to SSS 054
s second, no padding 4
ss second, padded to 2 padding 04
m minute, no padding 7
mm minute, padded to 2 07
h hour in 12-hour time, no padding 1
hh hour in 12-hour time, padded to 2 01
H hour in 24-hour time, no padding 9
HH hour in 24-hour time, padded to 2 13
Z narrow offset +5
ZZ short offset +05:00
ZZZ techie offset +0500
ZZZZ abbreviated named offset EST
ZZZZZ unabbreviated named offset Eastern Standard Time
z IANA zone America/New_York
a meridiem AM
d day of the month, no padding 6
dd day of the month, padded to 2 06
c E day of the week, as number from 1-7 (Monday is 1, Sunday is 7) 3
ccc EEE day of the week, as an abbreviate localized string Wed
cccc EEEE day of the week, as an unabbreviated localized string Wednesday
ccccc EEEEE day of the week, as a single localized letter W
L M month as an unpadded number 8
LL MM month as an padded number 08
LLL MMM month as an abbreviated localized string Aug
LLLL MMMM month as an unabbreviated localized string August
LLLLL MMMMM month as a single localized letter A
y year, unpadded 2014
yy two-digit year 14
yyyy four- to six- digit year, pads to 4 2014
G abbreviated localized era AD
GG unabbreviated localized era Anno Domini
GGGGG one-letter localized era A
kk ISO week year, unpadded 14
kkkk ISO week year, padded to 4 2014
W ISO week number, unpadded 32
WW ISO week number, padded to 2 32
o ordinal (day of year), unpadded 218
ooo ordinal (day of year), padded to 3 218
q quarter, no padding 3
qq quarter, padded to 2 03
D localized numeric date 9/4/2017
DD localized date with abbreviated month Aug 6, 2014
DDD localized date with full month August 6, 2014
DDDD localized date with full month and weekday Wednesday, August 6, 2014
t localized time 9:07 AM
tt localized time with seconds 1:07:04 PM
ttt localized time with seconds and abbreviated offset 1:07:04 PM EDT
tttt localized time with seconds and full offset 1:07:04 PM Eastern Daylight Time
T localized 24-hour time 13:07
TT localized 24-hour time with seconds 13:07:04
TTT localized 24-hour time with seconds and abbreviated offset 13:07:04 EDT
TTTT localized 24-hour time with seconds and full offset 13:07:04 Eastern Daylight Time
f short localized date and time 8/6/2014, 1:07 PM
ff less short localized date and time Aug 6, 2014, 1:07 PM
fff verbose localized date and time August 6, 2014, 1:07 PM EDT
ffff extra verbose localized date and time Wednesday, August 6, 2014, 1:07 PM Eastern Daylight Time
F short localized date and time with seconds 8/6/2014, 1:07:04 PM
FF less short localized date and time with seconds Aug 6, 2014, 1:07:04 PM
FFF verbose localized date and time with seconds August 6, 2014, 1:07:04 PM EDT
FFFF extra verbose localized date and time with seconds Wednesday, August 6, 2014, 1:07:04 PM Eastern Daylight Time
X unix timestamp in seconds 1407287224
x unix timestamp in milliseconds 1407287224054


Luxon is not an NLP tool and isn't suitable for all date parsing jobs. But it can do some parsing:

  1. Direct support for several well-known formats, including most valid ISO 8601 formats
  2. An ad-hoc parser for parsing specific formats

Parsing technical formats

ISO 8601

Luxon supports a wide range of valid ISO 8601 formats through the fromISO method.


All of these are parsable by fromISO:

  • In addition, all the times support offset arguments like "Z" and "+06:00".
  • Missing lower-order values are always set to the minimum possible value; i.e. it always parses to a full DateTime. For example, "2016-05-25" parses to midnight of that day. "2016-05" parses to the first of the month, etc.
  • The time is parsed as a local time if no offset is specified, but see the method docs to see your options, and also check out time zone docs for more details.

HTTP and RFC2822

Luxon also provides parsing for strings formatted according to RFC 2822 and the HTTP header specs (RFC 850 and 1123):

DateTime.fromRFC2822("Tue, 01 Nov 2016 13:23:12 +0630");
DateTime.fromHTTP("Sunday, 06-Nov-94 08:49:37 GMT");
DateTime.fromHTTP("Sun, 06 Nov 1994 08:49:37 GMT");


Luxon accepts SQL dates, times, and datetimes, via fromSQL:

DateTime.fromSQL("2017-05-15 09:24:15");

It works similarly to fromISO, so see above for additional notes.

Unix timestamps

Luxon can parse numerical Unix timestamps:


Both methods accept the same options, which allow you to specify a timezone, calendar, and/or numbering system.

JS Date Object

A native JS Date object can be converted into a DateTime using fromJSDate.

An optional zone parameter can be provided to set the zone on the resulting object.

Ad-hoc parsing

Consider alternatives

You generally shouldn't use Luxon to parse arbitrarily formatted date strings:

  1. If the string was generated by a computer for programmatic access, use a standard format like ISO 8601. Then you can parse it using DateTime.fromISO.
  2. If the string is typed out by a human, it may not conform to the format you specify when asking Luxon to parse it. Luxon is quite strict about the format matching the string exactly.

Sometimes, though, you get a string from some legacy system in some terrible ad-hoc format and you need to parse it.


See DateTime.fromFormat for the method signature. A brief example:

DateTime.fromFormat("May 25 1982", "LLLL dd yyyy");


Luxon supports parsing internationalized strings:

DateTime.fromFormat("mai 25 1982", "LLLL dd yyyy", { locale: "fr" });

Note, however, that Luxon derives the list of strings that can match, say, "LLLL" (and their meaning) by introspecting the environment's Intl implementation. Thus the exact strings may in some cases be environment-specific. You also need the Intl API available on the target platform (see the support matrix).


Not every token supported by DateTime#toFormat is supported in the parser. For example, there's no ZZZZ or ZZZZZ tokens. This is for a few reasons:

  • Luxon relies on natively-available functionality that only provides the mapping in one direction. We can ask what the named offset is and get "Eastern Standard Time" but not ask what "Eastern Standard Time" is most likely to mean.
  • Some things are ambiguous. There are several Eastern Standard Times in different countries and Luxon has no way to know which one you mean without additional information (such as that the zone is America/New_York) that would make EST superfluous anyway. Similarly, the single-letter month and weekday formats (EEEEE) that are useful in displaying calendars graphically can't be parsed because of their ambiguity.
  • Because of the limitations above, Luxon also doesn't support the "macro" tokens that include offset names, such ass "ttt" and "FFFF".


There are two kinds of things that can go wrong when parsing a string: a) you make a mistake with the tokens or b) the information parsed from the string does not correspond to a valid date. To help you sort that out, Luxon provides a method called fromFormatExplain. It takes the same arguments as fromFormat but returns a map of information about the parse that can be useful in debugging.

For example, here the code is using "MMMM" where "MMM" was needed. You can see the regex Luxon uses and see that it didn't match anything:

> DateTime.fromFormatExplain("Aug 6 1982", "MMMM d yyyy")

{ input: 'Aug 6 1982',
   [ { literal: false, val: 'MMMM' },
     { literal: false, val: ' ' },
     { literal: false, val: 'd' },
     { literal: false, val: ' ' },
     { literal: false, val: 'yyyy' } ],
  regex: '(January|February|March|April|May|June|July|August|September|October|November|December)( )(\\d\\d?)( )(\\d{4})',
  matches: {},
  result: {},
  zone: null }

If you parse something and get an invalid date, the debugging steps are slightly different. Here, we're attempting to parse August 32nd, which doesn't exist:

var d = DateTime.fromFormat("August 32 1982", "MMMM d yyyy");
d.isValid; //=> false
d.invalidReason; //=> 'day out of range'

For more on validity and how to debug it, see validity. You may find more comprehensive tips there. But as it applies specifically to fromFormat, again try fromFormatExplain:

> DateTime.fromFormatExplain("August 32 1982", "MMMM d yyyy")

{ input: 'August 32 1982',
   [ { literal: false, val: 'MMMM' },
     { literal: false, val: ' ' },
     { literal: false, val: 'd' },
     { literal: false, val: ' ' },
     { literal: false, val: 'yyyy' } ],
  regex: '(January|February|March|April|May|June|July|August|September|October|November|December)( )(\\d\\d?)( )(\\d{4})',
  matches: { M: 8, d: 32, y: 1982 },
  result: { month: 8, day: 32, year: 1982 },
  zone: null }

Because Luxon was able to parse the string without difficulty, the output is a lot richer. And you can see that the "day" field is set to 32. Combined with the "out of range" explanation above, that should clear up the situation.

Table of tokens

(Examples below given for 2014-08-06T13:07:04.054 considered as a local time in America/New_York). Note that many tokens supported by the formatter are not supported by the parser. That includes all the "macro" formats like "D" for "localized numeric date".

Standalone token Format token Description Example
S millisecond, no padding 54
SSS millisecond, padded to 3 054
u fractional seconds, (5 is a half second, 54 is slightly more) 54
s second, no padding 4
ss second, padded to 2 padding 04
m minute, no padding 7
mm minute, padded to 2 07
h hour in 12-hour time, no padding 1
hh hour in 12-hour time, padded to 2 01
H hour in 24-hour time, no padding 9
HH hour in 24-hour time, padded to 2 13
Z narrow offset +5
ZZ short offset +05:00
ZZZ techie offset +0500
z IANA zone America/New_York
a meridiem AM
d day of the month, no padding 6
dd day of the month, padded to 2 06
E c day of the week, as number from 1-7 (Monday is 1, Sunday is 7) 3
EEE ccc day of the week, as an abbreviate localized string Wed
EEEE cccc day of the week, as an unabbreviated localized string Wednesday
M L month as an unpadded number 8
MM LL month as an padded number 08
MMM LLL month as an abbreviated localized string Aug
MMMM LLLL month as an unabbreviated localized string August
y year, 1-6 digits, very literally 2014
yy two-digit year, interpreted as > 1960 (also accepts 4) 14
yyyy four-digit year 2014
yyyyy four- to six-digit years 10340
yyyyyy six-digit years 010340
G abbreviated localized era AD
GG unabbreviated localized era Anno Domini
GGGGG one-letter localized era A
kk ISO week year, unpadded 17
kkkk ISO week year, padded to 4 2014
W ISO week number, unpadded 32
WW ISO week number, padded to 2 32
o ordinal (day of year), unpadded 218
ooo ordinal (day of year), padded to 3 218
q quarter, no padding 3
D localized numeric date 9/4/2017
DD localized date with abbreviated month Aug 6, 2014
DDD localized date with full month August 6, 2014
DDDD localized date with full month and weekday Wednesday, August 6, 2014
t localized time 9:07 AM
tt localized time with seconds 1:07:04 PM
T localized 24-hour time 13:07
TT localized 24-hour time with seconds 13:07:04
TTT localized 24-hour time with seconds and abbreviated offset 13:07:04 EDT
f short localized date and time 8/6/2014, 1:07 PM
ff less short localized date and time Aug 6, 2014, 1:07 PM
F short localized date and time with seconds 8/6/2014, 1:07:04 PM
FF less short localized date and time with seconds Aug 6, 2014, 1:07:04 PM


This page covers some oddball topics related to date and time math, which has some quirky corner cases.

Calendar math vs time math

The basics

Math with dates and times can be unintuitive to programmers. If it's Feb 13, 2017 and I say "in exactly one month", you know I mean March 13. Exactly one month after that is April 13. But because February is a shorter month than March, that means we added a different amount of time in each case. On the other hand, if I said "30 days from February 13", you'd try to figure out what day that landed on in March. Here it is in Luxon:

DateTime.local(2017, 2, 13).plus({ months: 1 }).toISODate() //=> '2017-03-13'

DateTime.local(2017, 2, 13).plus({ days: 30 }).toISODate() //=> '2017-03-15'

More generally we can differentiate two modes of math:

  • Calendar math works with higher-order, variable-length units like years and months
  • Time math works with lower-order, constant-length units such as hours, minutes, and seconds.

Which units use which math?

These units use calendar math:

  • Years vary because of leap years.
  • Months vary because they're just different lengths.
  • Days vary because DST transitions mean some days are 23 or 25 hours long.
  • Quarters are always three months, but months vary in length so quarters do too.
  • Weeks are always the same number of days, but days vary so weeks do too.

These units use time math:

  • Hours are always 60 minutes
  • Minutes are always 60 seconds
  • Seconds are always 1000 milliseconds

Don't worry about leap seconds. JavaScript and most other programming environments don't account for them; they just happen as abrupt, invisible changes to the underlying system's time.

How to think about calendar math

It's best not to think of calendar math as requiring arcane checks on the lengths of intervening periods. Instead, think of them as adjusting that unit directly and keeping lower order date components constant. Let's go back to the Feb 13 + 1 month example. If you didn't have Luxon, you would do something like this to accomplish that:

var d = new Date('2017-02-13')
d.setMonth(d.getMonth() + 1)
d.toLocaleString() //=> '3/13/2017, 12:00:00 AM'

And under the covers, that's more or less what Luxon does too. It doesn't boil the operation down to a milliseconds delta because that's not what's being asked. Instead, it fiddles with what it thinks the date should be and then uses the built-in Gregorian calendar to compute the new timestamp.


There's a whole section about this in the time zones documentation. But here's a quick example (Spring Forward is early on March 12 in my time zone):

var start = DateTime.local(2017, 3, 11, 10);
start.hour                          //=> 10, just for comparison{days: 1}).hour          //=> 10, stayed the same{hours: 24}).hour        //=> 11, DST pushed forward an hour

So in adding a day, we kept the hour at 10, even though that's only 23 hours later.

Time math

Time math is different. In time math, we're just adjusting the clock, adding or subtracting from the epoch timestamp. Adding 63 hours is really the same as adding 63 hours' worth of milliseconds. Under the covers, Luxon does this exactly the opposite of how it does calendar math; it boils the operation down to milliseconds, computes the new timestamp, and then computes the date out of that.

Math with multiple units

It's possible to do math with multiple units:

DateTime.fromISO('2017-05-15').plus({months: 2, days: 6}).toISODate(); //=> '2017-07-21'

This isn't as simple as it looks. For example, what should you expect this to do?

DateTime.fromISO('2017-04-30').plus({months: 1, days: 1}).toISODate();

If the day is added first, we'll get an intermediate value of May 1. Adding a month to that gives us June 1. But if the month is added first, we'll an intermediate value of May 30 and day after that is May 31. (See "Calendar math vs time math above if this is confusing.) So the order matters.

Luxon has a simple rule for this: math is done from highest order to lowest order. So the result of the example above is May 31. This rule isn't logically necessary, but it does seem reflect what people mean. Of course, Luxon can't enforce this rule if you do the math in separate operations:

DateTime.fromISO('2017-04-30').plus({days: 1}).plus({months: 1}).toISODate() //=> '2017-06-01'

It's not a coincidence that Luxon's interface makes it awkward to do this wrong.

Comparing DateTimes

DateTime implements #valueOf to return the epoch timestamp, so you can compare DateTimes with <, >, <=, and >=. That lets you find out if one DateTime is after or before another DateTime.

d1 < d2 // is d1 before d2?

However, be aware that === compares object identity, which is not a useful concept in a library with immutable types. Use #equals to compare both the time and additional metadata, such as the locale and time zone. If you're only interested in checking the equality of the timestamps, you can use:

d1.toMillis() === d2.toMillis() // are d1 and d2 the same instant in time?
+d1 === +d2 // same test, using object coercion

You may also use #hasSame to make more subtle comparisons:

d1.hasSame(d2, 'year');   // both DateTimes have the same calendar year
d1.hasSame(d2, 'day');    // both DateTimes have the same calendar day (which implies they also have the same calendar year and month)

Note that these are checking against the calendar. For example, if d1 is in 2017, calling hasSame with "year" asks if d2 is also in 2017, not whether the DateTimes within a year of each other. For that, you'd need diff (see below).

If you'd like to compare using a specific unit, you can achieve this by combining #startOf and the #valueOf comparisons above.

var d1 = DateTime.fromISO('2017-04-30');
var d2 = DateTime.fromISO('2017-04-01');

d2 < d1                                   //=> true
d2.startOf('year') < d1.startOf('year')   //=> false
d2.startOf('month') < d1.startOf('month') //=> false
d2.startOf('day') < d1.startOf('day')     //=> true

Duration math


Durations are quantities of time, like "3 days and 6 hours". Luxon has no idea which 3 days and 6 hours they represent; it's just how Luxon represents those quantities in abstract, unmoored from the timeline. This is both tremendously useful and occasionally confusing. I'm not going to give a detailed tour of their capabilities here (see the API docs for that), but I do want to clear up some of those confusions.

Here's some very basic stuff to get us going:

var dur = Duration.fromObject({ days: 3, hours: 6})

// examine it
dur.toObject()          //=> { days: 3, hours: 6 }

// express in minutes'minutes')       //=> 4680

// convert to minutes
dur.shiftTo('minutes').toObject() //=> { minutes: 4680 }

// add to a DateTime
DateTime.fromISO("2017-05-15").plus(dur).toISO() //=> '2017-05-18T06:00:00.000-04:00'


You can subtract one time from another to find out how much time there is between them. Luxon's diff method does this and it returns a Duration. For example:

var end = DateTime.fromISO('2017-03-13');
var start = DateTime.fromISO('2017-02-13');

var diffInMonths = end.diff(start, 'months');
diffInMonths.toObject(); //=> { months: 1 }

Notice we had to pick the unit to keep track of the diff in. The default is milliseconds:

var diff = end.diff(start);
diff.toObject() //=> { milliseconds: 2415600000 }

Finally, you can diff using multiple units:

var end = DateTime.fromISO('2017-03-13');
var start = DateTime.fromISO('2017-02-15');
end.diff(start, ['months', 'days']) //=> { months: 1, days: 2 }

Casual vs longterm conversion accuracy

Durations represent bundles of time with specific units, but Luxon allows you to convert between them:

  • shiftTo returns a new Duration denominated in the specified units.
  • as converts the duration to just that unit and returns its value
var dur = Duration.fromObject({ months: 4, weeks: 2, days: 6 })'days')                            //=> 140
dur.shiftTo('days').toObject()            //=> { days: 140 }
dur.shiftTo('weeks', 'hours').toObject()  //=> { weeks: 18, hours: 144 }

But how do those conversions actually work? First, uncontroversially:

  • 1 week = 7 days
  • 1 day = 24 hours
  • 1 hour = 60 minutes
  • 1 minute = 60 seconds
  • 1 second = 1000 milliseconds

These are always true and you can roll them up and down with consistency (e.g. 1 hour = 60 * 60 * 1000 milliseconds). However, this isn't really true for the higher order units, which vary in length, even putting DSTs aside. A year is sometimes 365 days long and sometimes 366. Months are 28, 29, 30, or 31 days. By default Luxon converts between these units using what you might call "casual" conversions:

Month Week Day
Year 12 52 365
Quarter 3 13 91
Month 4 30

These should match your intuition and for most purposes they work well. But they're not just wrong; they're not even self-consistent:

dur.shiftTo('months').shiftTo('days').as('years') //=> 0.9863013698630136

This is because 12 * 30 != 365. These errors can be annoying, but they can also cause significant issues if the errors accumulate:

var dur = Duration.fromObject({ years: 50000 });'milliseconds')).year //=> 51984                         //=> 52017

Those are 33 years apart! So Luxon offers an alternative conversion scheme called "longterm", based on the 400-year calendar cycle:

Month Week Day
Year 12 52.1775 365.2425
Quarter 3 13.04435 91.310625
Month 4.348125 30.436875

You can see why these are irritating to work with, which is why they're not the default.

Luxon methods that create Durations de novo accept an option called conversionAccuracy. You can set it to "casual" or "longterm". It's a property of the Duration itself, so any conversions you do use the rule you've picked, and any new Durations you derive from it will retain that property.

Duration.fromObject({ years: 23, conversionAccuracy: 'longterm' });
Duration.fromISO('PY23', { conversionAccuracy: 'longterm' });

end.diff(start, 'days', { conversionAccuracy: 'longterm' })

You can also create an accurate Duration out of an existing one:

var pedanticDuration = casualDuration.reconfigure({ conversionAccuracy: 'longterm' });

These Durations will do their conversions differently.

Losing information

Be careful of converting between units. It's easy to lose information. Let's say we converted a diff into days:

var end = DateTime.fromISO('2017-03-13');
var start = DateTime.fromISO('2017-02-13');

var diffInMonths = end.diff(start, 'months');'days'); //=> 30

That's our conversion between months and days (you could also do a longterm-accurate conversion; it wouldn't fix the issue ahead). But this isn't the number of days between February 15 and March 15!

var diffInDays = end.diff(start, 'days');
diffInDays.toObject(); //=> { days: 28 }

It's important to remember that diffs are Duration objects, and a Duration is just a dumb pile of time units our computation spat out. Unlike an Interval, a Duration doesn't "remember" what the inputs to the diff were. So we lost some information converting between units. This mistake is really common when rolling up:

var diff = end.diff(start); // default unit is milliseconds

// wtf, that's not a month!'months'); //=> 0.9319444 

// it's not even the right number of days! (hint: my time zone has a DST)
diff.shiftTo('hours').as('days'); //=> 27.958333333333332

Normally you won't run into this problem if you think clearly about what you want to do with a diff. Specifically, make sure you diff in the units you actually want to use. Then Luxon knows to answer the question you really want to ask.

var monthsDiff = end.diff(start, "months");
var daysDiff = end.diff(start, "days");

But sometimes you really do want an object that represents the subtraction itself, not the result. Intervals can help. Intervals are mostly used to keep track of ranges of time, but they make for "anchored" diffs too. For example:

var end = DateTime.fromISO('2017-03-13');
var start = DateTime.fromISO('2017-02-13');
var i = Interval.fromDateTimes(start, end);

i.length('days');       //=> 28
i.length('months')      //=> 1

Because the Interval stores its endpoints and computes length on the fly, it retakes the diff each time you query it. Of course, precisely because an Interval isn't an abstract bundle of time, it can't be used in places where Durations can. For example, you can't add them to DateTime via plus() because Luxon wouldn't know what units to do the math in (see "Calendar vs time math" above). But you can convert the interval into a Duration by picking the units:

i.toDuration('months').toObject(); //=> { months: 1 }
i.toDuration('days').toObject(); //=> { days: 28 }

You can even pick multiple units:

end = DateTime.fromISO('2018-05-25');
i = start.until(end);
i.toDuration(['years', 'months', 'days']).toObject(); //=> { years: 1, months: 3, days: 12 }

Of course, once you've converted to a Duration, you're back in the same spot you were with the diff case; further conversions will be lossy. So the point is to think carefully about what information you have when.


Invalid DateTimes

One of the most irritating aspects of programming with time is that it's possible to end up with invalid dates. This is a bit subtle: barring integer overflows, there's no count of milliseconds that don't correspond to a valid DateTime, but when working with calendar units, it's pretty easy to say something like "June 400th". Luxon considers that invalid and will mark it accordingly.

Unless you've asked Luxon to throw an exception when it creates an invalid DateTime (see more on that below), it will fail silently, creating an instance that doesn't know how to do anything. You can check validity with isValid:

> var dt = DateTime.fromObject({ month: 6, day: 400 });
dt.isValid //=> false

All of the methods or getters that return primitives return degenerate ones:

dt.year; //=>  NaN
dt.toString(); //=> 'Invalid DateTime'
dt.toObject(); //=> {}

Methods that return other Luxon objects will return invalid ones:{ days: 4 }).isValid; //=> false

Reasons a DateTimes can be invalid

The most common way to do that is to over- or underflow some unit:

  • February 40th
  • 28:00
  • -4 pm
  • etc

But there are other ways to do it:

// specify a time zone that doesn't exist"America/Blorp").isValid; //=> false

// provide contradictory information (here, this date is not a Wednesday)
DateTime.fromObject({ year: 2017, month: 5, day: 25, weekday: 3 }).isValid; //=> false

Note that some other kinds of mistakes throw, based on our judgment that they are more likely programmer errors than data issues:{ blorp: 7 }); //=> kerplosion

Debugging invalid DateTimes

Because DateTimes fail silently, they can be a pain to debug. Luxon has some features that can help.

invalidReason and invalidExplanation

Invalid DateTime objects are happy to tell you why they're invalid. invalidReason will give you a consistent error code you can use, whereas invalidExplanation will spell it out

var dt ="America/Blorp");
dt.invalidReason; //=>  'unsupported zone'
dt.invalidExplanation; //=> 'the zone "America/Blorp" is not supported'


You can make Luxon throw whenever it creates an invalid DateTime. The message will combine invalidReason and invalidExplanation:

Settings.throwOnInvalid = true;"America/Blorp"); //=> Error: Invalid DateTime: unsupported zone: the zone "America/Blorp" is not supported

You can of course leave this on in production too, but be sure to try/catch it appropriately.

Invalid Durations

Durations can be invalid too. The easiest way to get one is to diff an invalid DateTime.

DateTime.local(2017, 28).diffNow().isValid; //=> false

Invalid Intervals

Intervals can be invalid. This can happen a few different ways:

  • The end time is before the start time
  • It was created from invalid DateTime or Duration

Support matrix

This page covers what platforms are supported by Luxon and what caveats apply to them.

Official support

Luxon officially supports the last two versions of the major browsers, with some caveats. The table below shows which of the not-universally-supported features are available in what environments.

Browser Versions Zones Intl basics Intl tokens Intl relative time formatting
Chrome >= 71
>= 54
Firefox >= 65
Edge 18
IE 11
Safari 11
iOS Safari (iOS version numbers) >= 11
Node w/ICU >= 12
>= 8
Node w/o ICU >= 8
  • Those capabilities are explained in the next sections, along with possible polyfill options
  • "w/ICU" refers to providing Node with ICU data. See the install for instructions

Internet Explorer and platform polyfills

If you're supporting IE 10 or 11, you need some polyfills just to make Luxon work at all.

With IE 11, you can just add a polyfill like this to get the JS features you need:

<script src=",String.prototype.repeat,Array.prototype.find,Array.prototype.findIndex,Math.trunc,Math.sign"></script>

That hasn't checked off the other boxes in the chart above though, so keep reading for those.

With IE 10, you have the same problems as IE 11, except that you don't even get basic Intl support. You'll need to tack on the languages you wish to support. See the Basic Internationalization polyfill section below.

Alternatively, you can use a polyfilled build of Luxon, which you can find here:

These use global polyfills, though, which means newer browsers will be running the injected code too. And the same doesn't-include-intl-and-zone-support caveats apply to it too.

Effects of missing features

If the platforms you're targeting has all its boxes above check off, ignore this section.

In the support table above, you can see that some environments are missing capabilities. They affect a subset of Luxon's features that depend on specific APIs that some older browsers don't support.

  1. Basic internationalization. Luxon doesn't have internationalized strings in its code; instead it relies on the hosts implementation of the Intl API. This includes the very handy toLocaleString. Most browsers and recent versions of Node support this.
  2. Internationalized tokens. Listing the months or weekdays of a locale and outputting or parsing ad-hoc formats in non-English locales requires that Luxon be able to programmatically introspect the results of an Intl call. It does this using Intl's formatToParts method, which is a relatively recent addition in most browsers. So you could have the Intl API without having that.
  3. Zones. Luxon's support of IANA zones works by abusing the Intl API. That means you have to have that API and that the API must support a reasonable list of time zones. Zones are a recent addition to some platforms.
  4. Relative time formatting. Luxon's support for relative time formatting (e.g. DateTime#toRelative and DateTime#toRelativeCalendar) depends on Intl.RelativeTimeFormat, which is currently only available in Chrome and Firefox. Luxon will fall back to using English if that capability is missing.

If the browser lacks these capabilities, Luxon tries its best:

Feature Full support No Intl at all Intl but no formatToParts No IANA zone support No relative time format
Most things OK OK OK OK OK
Using explicit time zones OK Invalid DateTime OK Invalid DateTime OK
DateTime#toLocaleString OK Uses English with caveats† OK OK OK
DateTime#toLocaleParts OK Empty array Empty array OK OK
DateTime#toFormat in en-US OK OK OK OK OK
DateTime#toFormat in other locales OK Uses English Uses English if format contains localized strings‡ OK OK
DateTime#fromFormat in en-US OK OK OK OK OK
DateTime#toRelative in en-US OK OK OK OK OK
DateTime#toRelative in other locales Uses English OK OK OK Uses English
DateTime#offsetNameShort, etc OK Returns null OK in most locales§ OK OK
fromFormat in other locales OK Invalid DateTime if uses localized strings‡ Uses English if format contains localized strings‡ OK OK
Info.months, etc in en-US OK OK OK OK OK
Info.months, etc in other locales OK Uses English Uses English OK OK

† Specifically, the caveat here is that this English fallback only works as you might expect for Luxon-provided preset arguments, like DateTime.DATETIME_MED. If you provide your own, modify the presets, or even clone them, it will use DateTime.DATETIME_HUGE. If you don't provide any arguments at all, it defaults to DateTime.DATE_SHORT.

‡ This means that Luxon can't parse anything with a word in it like localized versions of "January" or "Tuesday". It's fine with numbers, as long as they're Western numbers.

§ This fallback uses a hack that is not guaranteed to work in every locale in every browser. It's worked where I tested it, though. It will fall back to returning null if it fails.



If your platform doesn't have any kind of Intl support (such as IE 10), you need to load them individually through a polyfill. The easiest way to that is like this:

<script src=","></script>

If you're on a platform that already needs other polyfills, just tack those features to the end of your polyfill list.

Intl tokens

Polyfilling Intl token support is a bit painful. This limitation applies to Edge < 18 and all the IEs. Fortunately, you probably don't need Intl token support!

First, if you don't have Intl at all (e.g. as in IE 10), you are in luck. The polyfills in the previous section will give you Intl token support too!

But more likely, you have basic Intl support but not formatToParts (e.g. IE 11 or Edge 16). The problem here is that the polyfill service will ignore the Intl polyfills, so you won't get the support you need. Instead, you need to override all of Intl with the Intl polyfill directly. [help wanted: instructions on exactly how to do that]


If you have an Intl API (either natively or through the Intl polyfill above) but no zone support, you can add it via the very nice DateTime format polyfill.

Older platforms

  • Older versions of both Chrome and Firefox will most likely work. It's just that I only officially support the last two versions. As you get to older versions of these browsers, the missing capabilities listed above begin to apply to them. (e.g. FF started supporting formatToParts in 51 and time zones in 52). I haven't broken that out because it's complicated, Luxon doesn't officially support them, and no one runs them anyway.
  • Older versions of IE probably won't work at all.
  • Older versions of Node probably won't work without recompiling Luxon with a different Node target. In which case they'll work with some features missing.

Other platforms

If the platform you're targeting isn't on the list and you're unsure what caveats apply, you can check which pieces are supported:

Info.features(); //=> { intl: true, intlTokens: true, zones: true, relative: false }

Specific notes on other platforms:

  • React Native on (specifically) Android doesn't come with Intl support, so all the possible-to-be-missing capabilities above are unavailable. Use jsc-android-buildscripts to fix it.

For Moment users

Luxon borrows lots of ideas from Moment.js, but there are a lot of differences too. This document clarifies what they are.


Luxon's objects are immutable, whereas Moment's are mutable. For example, in Moment:

var m1 = moment();
var m2 = m1.add(1, 'hours');
m1.valueOf() === m2.valueOf(); //=> true

This happens because m1 and m2 are really the same object; add() mutated the object to be an hour later. Compare that to Luxon:

var d1 =;
var d2 ={ hours: 1 });
d1.valueOf() === d2.valueOf(); //=> false

This happens because the plus method returns a new instance, leaving d1 unmodified. It also means that Luxon doesn't require copy constructors or clone methods.

Major functional differences

  1. Months in Luxon are 1-indexed instead of 0-indexed like in Moment and the native Date type.
  2. Localizations and time zones are implemented by the native Intl API (or a polyfill of it), instead of by the library itself.
  3. Luxon has both a Duration type and an Interval type. The Interval type is like Twix.
  4. Luxon lacks the relative time features of Moment and won't support it until the required facilities are provided by the browser.

Other API style differences

  1. Luxon methods often take option objects as their last parameter
  2. Luxon has different static methods for object creation (e.g. fromISO), as opposed to Moment's one function that dispatches based on the input
  3. Luxon parsers are very strict, whereas Moment's are more lenient.
  4. Luxon uses getters instead of accessor methods, so dateTime.year instead of dateTime.year()
  5. Luxon centralizes its "setters", like dateTime.set({year: 2016, month: 4}) instead of dateTime.year(2016).month(4) like in Moment.
  6. Luxon's Durations are a separate top-level class.
  7. Arguments to Luxon's methods are not automatically coerced into Luxon instances. E.g. m.diff('2017-04-01') would be dt.diff(DateTime.fromISO('2017-04-01')).

DateTime method equivalence

Here's a rough mapping of DateTime methods in Moment to ones in Luxon. I haven't comprehensively documented stuff that's in Luxon but not in Moment, just a few odds and ends that seemed obvious for inclusion; there are more. I've probably missed a few things too.


Operation Moment Luxon Notes
Now moment()
From ISO moment(String) DateTime.fromISO(String)
From RFC 2822 moment(String) DateTime.fromRFC2822(String)
From custom format moment(String, String) DateTime.fromFormat(String, String) The format tokens differ between Moment and Luxon, such that the same format string cannot be used between the two.
From object moment(Object) DateTime.fromObject(Object)
From timestamp moment(Number) DateTime.fromMillis(Number)
From JS Date moment(Date) DateTime.fromJSDate(Date)
From civil time moment(Array) DateTime.local(Number...) Like DateTime.local(2016, 12, 25, 10, 30)
From UTC civil time moment.utc(Array) DateTime.utc(Number...) Moment also uses moment.utc() to take other arguments. In Luxon, use the appropriate method and pass in the { zone: 'utc'} option
Clone moment(Moment) N/A Immutability makes this pointless; just reuse the object
Use the string's offset parseZone See note Methods taking strings that can specify offset or zone take a setZone argument

Getters and setters

Basic information getters

Property Moment Luxon Notes
Validity isValid() isValid See also invalidReason
Locale locale() locale
Zone tz() zone Moment requires a plugin for this, but not Luxon

Unit getters

Property Moment Luxon Notes
Year year() year
Month month() month
Day of month date() day
Day of week day(), weekday(), isoWeekday() weekday 1-7, Monday is 1, Sunday is 7, per ISO
Day of year dayOfYear() ordinal
Hour of day hour() hour
Minute of hour minute() minute
Second of minute second() second
Millisecond of seconds millisecond() millisecond
Week of ISO week year weekYear, isoWeekYear weekYear
Quarter quarter None Just divide the months by 4

Programmatic get and set

For programmatic getting and setting, Luxon and Moment are very similar here:

Operation Moment Luxon Notes
get value get(String) get(String)
set value set(String, Number) None
set values set(Object) set(Object) Like dt.set({ year: 2016, month: 3 })


Operation Moment Luxon Notes
Addition add(Number, String) plus(Object) Like{ months: 3, days: 2 })
Subtraction subtract(Number, String) minus(Object) Like dt.minus({ months: 3, days: 2 })
Start of unit startOf(String) startOf(String)
End of unit endOf(String) endOf(String)
Change unit values set(Object) set(Object) Like dt.set({ year: 2016, month: 3 })
Change time zone tz(String) setZone(string) Luxon doesn't require a plugin
Change zone to utc utc() toUTC()
Change local zone local() toLocal()
Change offset utcOffset(Number) None Set the zone instead
Change locale locale(String) setLocale(String)


Question Moment Luxon Notes
Is this time before that time? m1.isBefore(m2) dt1 < dt2 The Moment versions of these take a unit. To do that in Luxon, use startOf on both instances.
Is this time after that time? m1.isAfter(m2) dt1 > dt2
Is this time the same or before that time? m1.isSameOrBefore(m2) dt1 <= dt2
Is this time the same or after that time? m1.isSameOrAfter(m2) dt1 >= dt2
Do these two times have the same [unit]? m1.isSame(m2, unit) dt1.hasSame(dt2, unit)
Is this time's [unit] before that time's? m1.isBefore(m2, unit) dt1.startOf(unit) < dt2.startOf(unit)
Is this time's [unit] after that time's? m1.isAfter(m2, unit) dt1.startOf(unit) > dt2.startOf(unit)
Is this time between these two times? m1.isBetween(m2, m3) Interval.fromDateTimes(dt2, dt3).contains(dt1)
Is this time inside a DST isDST() isInDST
Is this time's year a leap year? isInLeapYear() isInLeapYear
How many days are in this time's month? daysInMonth() daysInMonth
How many days are in this time's year? None daysInYear



See the formatting guide for more about the string-outputting methods.

Output Moment Luxon Notes
simple string toString() toString() Luxon just uses ISO 8601 for this. See Luxon's toLocaleString()
full ISO 8601 iso() toISO()
ISO date only None toISODate()
ISO time only None toISOTime()
custom format format(...) toFormat(...) The format tokens differ between Moment and Luxon, such that the same format string cannot be used between the two.
RFC 2822 toRFC2822()
HTTP date string toHTTP()
JS Date toDate() toJSDate()
Epoch time valueOf() toMillis() or valueOf()
Object toObject() toObject()
Duration diff(Moment) diff(DateTime) Moment's diff returns a count of milliseconds, but Luxon's returns a Duration. To replicate the Moment behavior, use dt1.diff(d2).milliseconds.


Luxon has toRelative and toRelativeCalendar. For internationalization, they use Intl.RelativeTimeFormat (or fall back to English when it is not supported by the browser).

Operation Moment Luxon
Time from now fromNow() toRelative()
Time from other time from(Moment) toRelative({ base: DateTime })
Time to now toNow() DateTime.local().toRelative({ base: this })
Time to other time to(Moment) otherTime.toRelative({ base: this })
"Calendar time" calendar() toRelativeCalendar()


Moment Durations and Luxon Durations are broadly similar in purpose and capabilities. The main differences are:

  1. Luxon durations have more sophisticated conversion capabilities. They can convert from one set of units to another using shiftTo. They can also be configured to use different unit conversions. See Duration Math for more.
  2. Luxon does not (yet) have an equivalent of Moment's Duration humanize method. Luxon will add that when Unified Intl.NumberFormat is supported by browsers.
  3. Like DateTimes, Luxon Durations have separate methods for creating objects from different sources.

See the Duration API docs for more.


Moment doesn't have direct support intervals, which must be provided by plugins like Twix or moment-range. Luxon's Intervals have similar capabilities to theirs, with the exception of the humanization features. See the Interval API docs for more.

Why does Luxon exist?

What's the deal with this whole Luxon thing anyway? Why did I write it? How is it related to the Moment project? What's different about it? This page tries to hash all that out.

A disclaimer

I should clarify here that I'm just one of Moment's maintainers; I'm not in charge and I'm not Moment's creator. The opinions here are solely mine. Finally, none of this is meant to bash Moment, a project I've spent a lot of time on and whose other developers I respect.


Luxon started because I had a bunch of ideas on how to improve Moment but kept finding Moment wasn't a good codebase to explore them with. Namely:

  • I wanted to try out some ideas that I thought would provide a better, more explicit API but didn't want to break everything in Moment.
  • I had an idea on how to provide out-of-the-box, no-data-files-required support for time zones, but Moment's design made that difficult.
  • I wanted to completely rethink how internationalization worked by using the Intl API that comes packaged in browsers.
  • I wanted to use a modern JS toolchain, which would require a major retrofit to Moment.

So I decided to write something from scratch, a sort of modernized Moment. It's a combination of all the things I learned maintaining Moment and Twix, plus a bunch of fresh ideas. I worked on it in little slivers of spare time for about two years. But now it's ready to actually use, and the Moment team likes it enough that we pulled it under the organization's umbrella.

Ideas in Luxon

Luxon is built around a few core ideas:

  1. Keep the basic chainable date wrapper idea from Moment.
  2. Make all the types immutable.
  3. Make the API explicit; different methods do different things and have well-defined options.
  4. Use the Intl API to provide internationalization, including token parsing. Fall back to English if the browser doesn't support those APIs.
  5. Abuse the Intl API horribly to provide time zone support. Only possible for modern browsers.
  6. Provide more comprehensive duration support.
  7. Directly provide interval support.
  8. Write inline docs for everything.

These ideas have some big advantages:

  1. It's much easier to understand and debug code that uses Luxon.
  2. Using native browser capabilities for internationalization leads to a much better behavior and is dramatically easier to maintain.
  3. Luxon has the best time zone support of any JS date library.
  4. Luxon's durations are both flexible and easy to use.
  5. The documentation is very good.

They also have some disadvantages:

  1. Using modern browser capabilities means that the fallback behavior introduces complexity for the programmer.
  2. Never keeping internationalized strings in the code base means that some capabilities have to wait until the browsers provide it.
  3. Some aspects of the Intl API are browser-dependent, which means Luxon's behavior is too.

Place in the Moment project

Luxon lives in the Moment project because, basically, we all really like it, and it represents a huge improvement.

But Luxon doesn't quite fulfill Moment's mandate. Since it sometimes relies on browsers' implementations of the Intl specifications, it doesn't provide some of Moment's most commonly-used features on all browsers. Relative date formatting is for instance not supported in IE11 and Safari (as of August 2020). Luxon's Intl features do not work as expected on sufficiently outdated browsers, whereas Moment's all work everywhere. That represents a good tradeoff, IMO, but it's clearly a different one than Moment makes.

Luxon makes a major break in API conventions. Part of Moment's charm is that you just call moment() on basically anything and you get date, whereas Luxon forces you to decide that you want to call fromISO or whatever. The upshot of all that is that Luxon feels like a different library; that's why it's not Moment 3.0.

So what is it then? We're not really sure. We're calling it a Moment labs project. Will its ideas get backported into Moment 3? Will it gradually siphon users away from Moment and become the focus of the Moment project? Will the march of modern browsers retire the arguments above and cause us to revisit branding Luxon as Moment? We don't know.

There, now you know as much as I do.

Future plans

Luxon is fully usable and I plan to support it indefinitely. It's also largely complete. Luxon will eventually strip out its fallbacks for missing platform features. But overall I expect the core functionality to stay basically as it is, adding mostly minor tweaks and bugfixes.



  • Add fromISOTime, toISOTime and toMillis to Duration (#803)
  • Fix padding of negative years in IsoDate (#871)
  • Fix hasSame unit comparison (#798)
  • Export VERSION information (#794)
  • Durations are considered equal with extra zero units. Fixes #809 (#811)


  • fix fromFormat with Intl formats containing non-breaking spaces
  • Support higher precisision in ISO milliseconds
  • Some fixes for 00:30 timezones
  • Fix some throwOnInvalid for invalid Intervals
  • Various doc fixes
  • Fix Interval#isSame for empty intervals
  • Mark package as side effect-free
  • Add support for intervals with a large number of seconds

1.24.1 (2020-05-04)

  • Remove erroneous console.log call

1.24.0 (2020-05-03)

  • Update polyfills for pollyfilled build

1.23.0 (2020-04-02)

  • Allow minus sign prefix when creating Duration from ISO

1.22.2 (2020-03-25)

  • Added more details to error messages for type errors

1.22.1 (2020-03-19)

  • Added support for ISO basic format to DateTime#toISO

1.22.0 (2020-01-26)

  • Fix setZone's handling of pre-1970 dates with milisecond components
  • Fix keepLocalTime for large jumps near the target zone's DST
  • Fix cache perf for toRelative()

1.21.3 (2019-11-28)

  • Fix parsing of meridiems in macro tokens in newer versions of v8

1.21.2 (2019-11-18)

  • Fix bug in Chrome Canary that threw off time zone calculations

1.21.1 (2019-11-03)

  • Fix for quarter parsing
  • Some documentation updates

1.21.0 (2019-10-30)

  • Added quarter support to the parser
  • Fix some rounding issues in ISO formatting

1.20.0 (2019-10-29)

  • Added Duration#mapUnits
  • added Interval#toISODate and Interval#toISOTime
  • Some documentation fixes


  • Cache offset values
  • Fix handling of negative sub 1-hour offsets


  • Speculative fix for Node 6


  • Fix Intl.DateTimeFormat usage for polyfills


  • Interval#splitAt now ignores input dates outside the interval
  • Don't allow decimals in DateTime creation


  • Fix handling of decimals in DateTime#plus and #minus


  • Fix validity when adding or subtracting time that exceeds Date max/min boundaries


  • Add support for macro tokens in the parser


  • Fix issue with toRelative using style: short with plural days


  • Reject out-of-range numbers in DateTime.fromMillis
  • Reject 0s in ISO date inputs


  • DateTime.min and DateTime.max throw if they get the wrong kind of arguments
  • Fixed throwOnInvalid logic for Interval


  • Catch errors trying to use Intl in weird versions of IE 11


  • Fixed locale default logic for `DateTime#toFormat("ZZZZ")


  • Added formatOffset to Zones


  • Allow the zone argument to Interval.fromISO with duration components
  • Ignore the zone argument to Duration factory methods


  • Fix keepLocalTime calculations that span offset changes


  • Fixed ISO formatting for dates > 999


  • Performance improvements for regex parsing


  • Support numberSystem in fromFormat
  • Fix validity for bad initial zone specifiers


  • Fix cross-month diffs in some scenarios
  • Fix time zone parsing when the time zone isn't at the end
  • Memoize IANA zone creation


  • Add some explicit CDN support to the NPM package
  • Add week token to duration ISO support
  • Lots of cleanup and test coverage changes


  • setZone("local") now returns the defaultZone if it is set
  • Fixes for the polyfilled build


  • Allow 24:00 in ISO (and other) strings
  • Fix some bugs with the typecheck functions like DateTime.isDateTime()


  • Fixed handling of some characters in fromFormat literal sections
  • Hanlde string values in object arguments to DateTime methods
  • Fixed toRelativeCalendar's handling of zones in the base date


  • Fix DateTime#plus() when spanning across AD 100


  • Fix low-year handling for IANA zones
  • DateTime#toLocal() now uses the default locale
  • Fix zero duration formatting
  • Many documentation fixes


  • Fix endOf("day") during DSTs (#399)
  • Add `Interval#mapEndpoints (#400)
  • Add DateTime#zone and Info.normalizeZone (#404)


  • Add DateTime#toRelative and DateTime#toRelativeCalendar


  • Allow "UTC" in the zone position of fromSQL
  • Force isDateTime and isDuration to return booleans in all cases


  • Trim leading \u200e characters from offset names in Edge 16 and 17


  • Add DateTime.fromSeconds and DateTime#toSeconds


  • Floor the seconds instead of rounding them when outputting the 'X' format
  • Change the options to toLocale to override the configuration (the previous options were essentially ignored)


  • Fixing merge error that resulted in bad error messages


  • midly breaking Rework negative durations
  • Fix handling weekdays at the end of leap week years
  • Add isDuration, isDateTime, and isInterval
  • Fix handling of Luxon object arguments passed from other execution contexts


  • Improved error message
  • Added DateTime#invalidExplanation, Duration#invalidExplanation, Interval#invalidExplanation to provide more details on invalid objects


  • Cache Intl objects for an 85x speed up on basic operations using non-en locales


  • Fix minified builds


  • Fix hour formatting in RFC822 strings
  • Interval.fromISO accepts formats with durations


Removal accidentally-introduced runtime dependency


  • Handle locale strings with BCP 47 extensions. Especially helpful for environments with funky default locales
  • Support for [weekYear]-W[weekNumber] ISO 8601 strings


  • Empty diffs now have all the asked-for units in them, set at 0
  • Duration operations perserve the superset of units


  • Add x and X to toFormat for formatting Epoch seconds and Epoch milliseconds
  • Parser allows a wider range of IANA zone specifiers
  • BREAKING: Etc/GMT+10 is now interpreted as UTC-10, per spec


Documentation fixes


  • DateTime.fromMillis will throw if passed a non-number
  • Fixes for type checking across JS contexts


  • Include milliseconds in Duration#toISO
  • Avoid deprecation warning from DateTime#inspect in Node 10


  • mildly breaking change Duration.toFormat now floors its outputs instead of rounding them (see #224)
  • Added 'floor' option to Duration.toFormat and deprecated the 'round' option
  • Added Dateime.toBSON
  • Fixed infinite loop when passing invalid or zero-length durations to Interval#splitBy
  • Added better error handling to Duration.fromObject()


  • 222x speed-up in DateTime creation for non-en locales
  • Added DateTime#toMillis alias for DateTime#valueOf
  • Fixed types on zone exports


  • Export Zone classes
  • Fix endOf and startOf for quarters
  • Change toFormat("Z") to return a number for UTC
  • Allow "GTM" as an argument to setZone


  • Support for zone names with more than two components
  • Fixed long-term-accurate conversions for months
  • Added weeksInWeekYear


  • The big one-oh. No changes from 0.5.8.


  • Large perf improvements for DateTime#toFormat(), when using non-intl numbers


  • Added AMD build to the NPM package
  • Large performance improvements to technical formatting (e.g. DateTime#toISO)


  • Refactor internals
  • Added support for fractional seconds in Duration.fromISO
  • Added browser global to the NPM package


  • Best-we-can-do fix for DateTime#toLocaleString() for fixed-offset zones when showing the zone name in the output
  • Fixed Duration#shiftTo for unormalized Durations that need a rollup cascade


  • Fix default locales in Node
  • Fix prototype to help with React inspection
  • Improve REPL output for Durations in Node


  • Remove errant ICU runtime dep (again)


  • Remove comments from minified builds (introduced by 0.5.1)


  • Fixed minified builds (oops)
  • Fix computation of fractional parts of diffs


  • isBefore() returns true for the end of the interval, consistent with being half-open
  • zoneName now rturns null for invalid DateTimes
  • Added quarter support
  • Adding a month to Jan 31 gives Feb 28/29


  • Always round down to the nearest millisecond when parsing


  • Fixed toLocaleString for fixed-offset zones in the absence of Intl
  • Added Info.isValidIANAZone
  • Made malformed zone specifiers result in invalid DateTime instances


  • Rename DateTime.fromString to DateTime.fromFormat (leaving deprecated DateTime.fromString)
  • Rename DateTime.fromStringExplain to DateTime.fromFormatExplain (leaving deprecated DateTime.fromStringExplain)
  • Support Etc/GMT IANA zones
  • Perf fixes for zones
  • Rework build infrastructure


  • Fix DateTime.fromObject's handling of default zones
  • Change keepCalendarTime to keepLocalTime


  • Handle no arguments in DateTime.min and DateTime.max
  • Documentation fixes


  • Fix bug where Durations could sometimes mutate


  • Fix DateTime.fromMillis(0) more thoroughly


  • Fix sourcemaps


  • Fix DateTime.fromMillis(0)


  • Fix 'h' and 'hh' toFormat tokens for midnight


  • Better shiftTo behavior for durations with floating point components


  • Fix toHTTP to use 24-hour hours
  • Tighten up regular expressions
  • Various documentation fixes


  • Fixes for diff with multiple units


  • Fixes for fromSQL, toSQL, toSQLTime, and toSQLDate
  • Add includeOffset option to toISO and toISOTime


  • Add module field to package.json


  • Remove polyfills from main builds
  • Update compilation toolchain to target builds more exactly
  • Fix IE in polyfill build


  • Add .fromSQL, #toSQL, #toSQLTime, #toSQLDate
  • Fix AM/PM parsing
  • Major perf improvements
  • Default to system locale when using macro formats in #toFormat
  • .fromISO accepts standalone times
  • See for important news concerning field accessibility


  • Add 'u' formatting and parsing
  • Add 'y', 'yyyyy', and 'yyyyyy' parsing tokens
  • Add 'yyyyyy' formatting token
  • Better error messages for missing arguments to DateTime.fromString


  • Fix zones for Edge


  • Fix fromISO to accept various levels of subsecond precision


  • Fixed parsing for ordinals
  • Made parsing stricter


  • Fixed formatting for non-hour aligned fixed-offset zones
  • Fixed longterm conversion accuracy option in diffs
  • Fixed invalid handling in Interval#set


  • Fixing formatting for fixed-offset zones


  • Fixes for IE 9 & 10


  • Fixing busted release 0.0.14


  • toLocaleString() and others default to the system's locale
  • support for ISO week durations in Duration.fromISO


  • Improve non-Intl fallbacks for toLocaleString
  • Fix offsetNameShort and offsetNameLong for non-Intl environments
  • Added weekdayShort, weekdayLong, monthShort, monthLong DateTime getters


  • Only include build dir in NPM module


  • Move to Moment Github org


  • The local zone can now report its IANA name
  • Fixed parsing bug for yy and kk
  • Improved test coverage


  • Added toLocaleParts
  • Slightly more friendly month/weekday parsing
  • Default locale setting


  • Stricter toJSDate
  • fromISO now supports year and year-month formats
  • More graceful degradation in the absence of platform features


Experimental, but now broadly useful.

Contributing to Luxon

General guidelines

Patches are welcome. Luxon is at this point just a baby and it could use lots of help. But before you dive in...Luxon is one of those tightly-scoped libraries where the default answer to "should this library do X?" is likely "no". So ask first! It might save you some time and energy.

Here are some vague notes on Luxon's design philosophy:

  1. We won't accept patches that can't be internationalized using the JS environment's (e.g. the browser's) native capabilities. This means that most convenient humanization features are out of scope.
  2. We try hard to have a clear definition of what Luxon does and doesn't do. With few exceptions, this is not a "do what I mean" library.
  3. Luxon shouldn't contain simple conveniences that bloat the library to save callers a couple lines of code. Write those lines in your own code.
  4. Most of the complexity of JS module loading compatibility is left to the build. If you have a "this can't be loaded in my bespoke JS module loader" problems, this isn't something you should be solving with changes to the src directory. If it's a common use case and is possible to generate with Rollup, it can get its own build command.
  5. We prefer documentation clarifications and gotchas to go in the docstrings, not in the guides on the docs page. Obviously, if the guides are wrong, they should be fixed, but we don't want them to turn into troubleshooting pages. On the other hand, making sure the method-level documentation has ample examples and notes is great.
  6. You'll need to sign a CLA as part of your first pull request to Luxon.

Building and testing

Building and testing is done through npm scripts. The tests run in Node and require Node 10+ with full-icu support. This is because some of the features available in Luxon (like internationalization and time zones) need that stuff and we test it all. On any platform, if you have Node 10 installed with full-icu, you're good to go; just run npm scripts like npm run test. But you probably don't have that, so read on.


Mac is easy: Open the terminal.

brew install node --with-full-icu
npm install

If that's for whatever reason a pain, the Linux instructions should also work, as well as the Docker ones.


There are two ways to get full-icu support in Linux: build it with that support, or provide it as a module. We'll cover the latter. Assuming you've installed Node 10:

npm install
npm install full-icu

Where scripts/test is just NODE_ICU_DATA="$(pwd)/node_modules/full-icu" npm run test, which is required for making Node load the full-icu module you just installed. You can run all the other npm scripts (e.g. npm run docs) directly; they don't require Intl support.


If you have Bash or WSL, the Linux instructions seem to work fine.

I would love to add instructions for a non-WSL install of the dev env!


In case messing with your Node environment just to run Luxon's tests is too much to ask, we've provided a Docker container. You'll need a functioning Docker environment, but the rest is easy:

./docker/npm install
./docker/npm run test

Patch basics

Once you're sure your bugfix or feature makes sense for Luxon, make sure you take these steps:

  1. Be sure to add tests and run them with scripts/test
  2. Be sure you run npm run lint! before you commit. Note this will modify your source files to line up with the style guidelines.
  3. Make sure you add or ESDoc annotations appropriately. You can run npm run docs to generate the HTML for them. They land in the build/docs directory. This also builds the markdown files in /docs into the guide on the Luxon website.
  4. To test Luxon in your browser, run npm run site and then open build/demo/global.html. You can access Luxon classes in the console like window.luxon.DateTime.
  5. To test in Node, run npm run build and then run something like var DateTime = require('./build/cjs/luxon').DateTime.

Luxon uses Husky to run the formatter on your code as a pre-commit hook. You should still run npm run lint! yourself to catch other issues, but this hook will help prevent you from failing the build with a trivial formatting error.

npm script reference

Command Function
npm run build Build all the distributable files
npm run build-node Build just for Node
npm run test Run the test suite, but see notes above
npm run format Run the Prettier formatter
npm run lint! Run the formatter and the linter
npm run docs Build the doc pages
npm run site Build the Luxon website
npm run check-doc-coverage Check whether there's full doc coverage