Install guide
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.
Basic browser setup
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;
Node
Install via NPM:
npm install --save luxon
var luxon = require('luxon');
AMD (System.js, RequireJS, etc)
requirejs(['luxon'], function(luxon) {
//...
});
ES6
import { DateTime } from 'luxon';
Webpack
npm install --save luxon
import { DateTime } from 'luxon';
Meteor
Help wanted.
A quick tour
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:
var dt = DateTime.local(2017, 5, 15, 8, 30);
To get the current time, just do this:
var now = DateTime.local();
DateTime.local takes any number of arguments, all the way out to milliseconds. Underneath, this is just 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 two more.
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.
toString
The first thing we want to see is the DateTime as a string. Luxon returns ISO 8601 strings:
DateTime.local().toString() //=> '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 = DateTime.local()
dt.year //=> 2017
dt.month //=> 9
dt.day //=> 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({
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'}) //=> '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
Immutability
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".
Math
This is easier to show than to tell. All of these calls return new DateTime instances:
var dt = DateTime.local();
dt.plus({hours: 3, minutes: 2});
dt.minus({days: 7});
dt.startOf('day');
dt.endOf('hour');
Set
You can create new instances by overriding specific properties:
var dt = DateTime.local();
dt.set({hour: 3}).hour //=> 3
Intl
Luxon provides several different Intl capabilities, but the most important one is in formatting:
var dt = DateTime.local();
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
DateTime.local().setZone('America/Los_Angeles') // same
Luxon also supports UTC directly:
DateTime.utc(2017, 5, 15);
DateTime.utc();
DateTime.local().toUTC();
DateTime.utc().toLocal();
Durations
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:
dt.plus(dur);
They have getters just like DateTime:
dur.hours //=> 2
dur.minutes //=> 7
dur.seconds //=> 0
And some other useful stuff:
dur.as('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
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 = DateTime.local();
later = DateTime.local(2020, 10, 12);
i = Interval.fromDateTimes(now, later);
i.length() //=> 97098768468
i.length('years', true) //=> 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.
Intl
Intl
Luxon uses the native Intl API to provide easy-to-use internationalization. A quick example:
DateTime.local().setLocale('el').toLocaleString(DateTime.DATE_FULL); //=> '24 Σεπτεμβρίου 2017'
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 locale
locale
is a property of Luxon object, and it defaults to 'en-US'. 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 fromString
, 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
:
DateTime.local().setLocale('fr').locale //=> 'fr'
setLocale
is just a convenience for reconfigure
:
DateTime.local().reconfigure({ locale: 'fr' }).locale; //=> 'fr'
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
Formatting
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'
Parsing
You can parse localized strings:
DateTime.fromString('septembre 25, 2017 après Jésus-Christ', 'MMMM dd, yyyy GG', {locale: 'fr'})
Listing
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' ]
numberingSystem
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 = DateTime.local().setLocale('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 = DateTime.local().reconfigure({ locale: 'it', numberingSystem: 'beng' })
dt.toLocaleString(DateTime.DATE_FULL) //=> '২৪ settembre ২০১৭'
Time zones and offsets
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:
- 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.
- 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).
- 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.
- 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.
Terminology
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:
- 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.
- 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".
- 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".
- 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:
- 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.
- 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:
- Times will be formatted as they would be in that zone.
- Transformations to the DateTime (such as
plus
orstartOf
) 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 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:
- The date and time specified in the string was interpreted as specifying a Parisian local time (i.e. it's the time that corresponds to what would be called 9:10 there).
- 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 creates 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 case, 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.fromString("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'
setZone
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.fromString("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
setZone
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
keepCalendarTime
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', { keepCalendarTime: 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.
Accessors
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.
DateTime also has a zone
property that holds an Luxon Zone object. You don't normally need to interact with it, but don't get it confused with the zoneName
.
dt.zone //=> LocalZone {}
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 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. In the Northern Hemisphere, Spring Forward 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 became 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. In the Northern Hemisphere, 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 an hour before. So if you create a DateTime with a local time of 1:30, which time do you mean? It's an important question, because those 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
start.plus({days: 1}).hour //=> 10, stayed the same
start.plus({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'
Calendars
Calendars
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 = DateTime.local().reconfigure({ 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.
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 |
Formatting
Formatting
This section covers creating strings to represent a DateTime. There are three types of formatting capabilities:
- Technical formats like ISO 8601 and RFC 2822
- Internationalizable human-readable formats
- 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-W17-7'
dt.toISOWeekDate(); //=> '2017-04-20'
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'
toLocaleString (strings for humans)
The basics
Modern browsers (and other JS environments) provide brought 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'
Intl.DateTimeFormat
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({ weekday: 'long' }, DateTime.DATE_SHORT);
dt.toLocaleString(newFormat); //=> 'Thursday, 4/20/2017'
Presets
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_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 | 14 octobre 1983 à 13:30 UTC−4 | 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 |
Intl
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.
toFormat
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.
Intl
All of the strings (e.g. month names and weekday names) are internationalized by introspecting strings generated by the Intl API. Thus they exact strings you get are implementation-specific.
DateTime.fromISO('2014-08-06T13:07:04.054').setLocale('fr').toFormat('yyyy LLL dd') //=> '2014 août 06'
Escaping
You may escape strings using single quotes:
DateTime.local().toFormat("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") //=> 'август' (format)
d.toFormat("MMMM"); //=> 'августа' (standalone)
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).
Standlone token | Format token | Description | Example |
---|---|---|---|
S | millisecond, no padding | 54 | |
SSS | millisecond, padded to 3 | 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, padded to 2 | 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-digit year | 2014 | |
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 | |
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 |
Parsing
Parsing
Luxon is not an NLP tool and isn't suitable for all date parsing jobs. But it can do some parsing:
- Direct support for several well-known formats, including most valid ISO 8601 formats
- 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.
DateTime.fromISO('2016-05-25');
All of these are parsable by fromISO
:
2016-05-25
20160525
2016-05-25T09
2016-05-25T09:24
2016-05-25T09:24:15
2016-05-25T09:24:15.123
2016-05-25T0924
2016-05-25T092415
2016-05-25T092415.123
2016-05-25T09:24:15,123
2016-W21-3
2016W213
2016-W21-3T09:24:15.123
2016W213T09:24:15.123
2016-200
2016200
2016-200T09:24:15.123
Dates without times are parsed as that day's midnight.
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');
Ad-hoc parsing
Consider alternatives
You generally shouldn't use Luxon to parse arbitrarily formatted date strings:
- 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.
- 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.
fromString
See DateTime.fromString for the method signature. A brief example:
DateTime.fromString('May 25 1982', 'LLLL dd yyyy');
Intl
Luxon supports parsing internationalized strings:
DateTime.fromString('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).
Limitations
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 way. 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.
- Luxon doesn't yet support parsing the macro tokens it provides for formatting. This may eventually be addressed.
Debugging
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 fromStringExplain. It takes the same arguments as fromString
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.fromStringExplain("Aug 6 1982", "MMMM d yyyy")
{ input: 'Aug 6 1982',
tokens:
[ { 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.fromString("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 fromString
, again try fromStringExplain
:
> DateTime.fromStringExplain("August 32 1982", "MMMM d yyyy")
{ input: 'August 32 1982',
tokens:
[ { 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).
Standlone token | Format token | Description | Example |
---|---|---|---|
S | millisecond, no padding | 54 | |
SSS | millisecond, padded to 3 | 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, padded to 2 | 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, unpadded | 2014 | |
yy | two-digit year | 14 | |
yyyy | four-digit year | 2014 | |
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 | |
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 |
Math
Math
This page covers some oddball topics with 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.
- 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.
DSTs
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
start.plus({days: 1}).hour //=> 10, stayed the same
start.plus({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, but should you expect this to do?
DateTime.fromISO('2017-04-30').plus({months: 1, days: 1}).toISODate() //=> '2017-05-31'
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.
Duration math
Basics
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
dur.as('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'
Diffs
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 })
dur.as('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 |
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 });
DateTime.local().plus(dur.shiftTo('milliseconds')).year //=> 51984
DateTime.local().plus(dur).year //=> 52017
Those are 33 years apart! So Luxon offers an alternative conversion scheme, based on the 400-year calendar cycle:
Month | Week | Day | |
---|---|---|---|
Year | 12 | 52.1775 | 365.2425 |
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, { 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');
diffInMonths.as('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 that got spat out by our computation. 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!
diff.as('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. 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 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 }
Validity
Validity
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 if 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:
dt.plus({ 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
DateTime.local().setZone('America/Blorp').isValid //=> false
// provide contradictory information (here, this date is not a Wedensday)
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:
DateTime.local().set({ blorp: 7 }); //=> kerplosion
Debugging invalid DateTimes
Because DateTimes fail silently, they can be a pain to debug. There are two features that can help.
invalidReason
Invalid DateTime objects are happy to tell you why they're invalid. Like this:
DateTime.local().setZone('America/Blorp').invalidReason; //=> 'unsupported zone'
throwOnInvalid
You can make Luxon throw whenever it creates an invalid DateTime.
Settings.throwOnInvalid = true
DateTime.local().setZone('America/Blorp'); // Error: Invalid DateTime: unsupported zone
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
API reference
References
Class Summary
Static Public Class Summary | ||
public |
A DateTime is an immutable data structure representing a specific date and time and accompanying methods. |
|
public |
A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". |
|
public |
The Info class contains static methods for retrieving general time and date related data. |
|
public |
An Interval object represents a half-open interval of time, where each endpoint is a DateTime. |
|
public |
Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here. |
Interface Summary
Static Public Interface Summary | ||
public |
|
Support matrix
Support matrix
Luxon uses a slew of new browser Intl capabilities to tackle some of the tricker parts of dates and times. This means that not everything works in every environment.
What works everywhere
Fortunately, most of Luxon works in anything remotely recent. A non-exhaustive list:
- Create DateTime instances in the local or UTC time zones
- Parse and output known formats
- Parse and output using ad-hoc English formats
- All the transformations like
plus
andminus
- All of
Duration
andInterval
New capabilities and how they're used
Here are the areas that need help from newish browser capabilities:
- 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.
- 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.
- 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 in some platforms a recent addition
You can check whether your environment supports these capabilities using Luxon's Info
class:
Info.features() //=> { intl: true, intlTokens: true, zones: true }
The matrix
Here's the level of support for these features in different environments:
Area | Chrome | FF | IE | Edge | Safari | Node |
---|---|---|---|---|---|---|
Intl | 24+ | 29+ | 11+ | 12+ | 10+ | 0.11_ w/ICU† |
Intl tokens | 56+ | 51+ | None | 15+‡ | 11+ | 8+ w/ICU |
Zones | 24+† | 52+ | None | 15+‡ | 10+ | 6+ |
† This is an educated guess. I haven't tested this or found a definitive reference.
‡ Earlier versions may also support this, but I haven't been able to test them.
Notes:
- "w/ICU" refers to providing Node with ICU data. See here for more info
- Safari 11 is still in tech preview as of Sept 2017
- IE is terrible and it's weird that anyone uses it
What happens if a feature isn't supported?
You shouldn't use features of Luxon on projects that might be run on environments that don't support those features. Luxon tries to degrade gracefully if you don't, though.
Feature | Full support | No Intl at all | Intl but no formatToParts | No IANA zone support |
---|---|---|---|---|
Most things | OK | OK | OK | OK |
Explicit time zones | OK | Invalid DateTime | OK | Invalid DateTime |
toLocaleString |
OK | Native Date's toString |
OK | OK |
toFormat in en-US |
OK | OK | OK | OK |
toFormat in other locales |
OK | Uses English | Uses English if uses localized strings | OK |
fromString in en-US |
OK | OK | OK | OK |
fromString in other locales |
OK | Invalid DateTime if uses localized strings‡ | Uses English if uses localized strings‡ | OK |
Info.months , etc in en-US |
OK | OK | OK | OK |
Info.months , etc in other locales |
OK | Uses English | Uses English | OK |
‡ 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.
Polyfills
There are a couple of different polyfills available.
Intl
To backfill the Intl and Intl tokens, there's the Intl polyfill. Use it if your environment doesn't have Intl support or if it has Intl but not formatToParts
. Note that this fill comes with its own strings; there's no way to, say, just add the formatToParts
piece. Also note that the data isn't as complete as some of the browsers' and some more obscure parsing/formatting features in Luxon don't work very well with it. Finally, note that it does not add zone capabilities.
Zones
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 pollyfill.
For Moment users
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.
Immutability
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 = DateTime.local();
var d2 = d1.plus({ 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.
Other API style differences
- Luxon methods often take option objects as their last parameter
- Luxon has different static methods for object creation (e.g.
fromISO
), as opposed to Moment's one function that dispatches based on the input - Luxon parsers are very strict, whereas Moment's are more lenient.
- Luxon uses getters instead of accessor methods, so
dateTime.year
instead ofdateTime.year()
- Luxon centralizes its "setters", like
dateTime.set({year: 2016, month: 4})
instead ofdateTime.year(2016).month(4)
like in Moment. - Luxon's Durations are a separate top-level class.
- Arguments to Luxon's methods are not automatically coerced into Luxon instances. E.g.
m.diff('2017-04-01')
would bedt.diff(DateTime.fromISO('2017-04-01'))
.
Major functional differences
- Months in Luxon are 1-indexed instead of 0-indexed like in Moment and the native Date type.
- Localizations and time zones are implemented by the native Intl API (or a polyfill of it), instead of by the library itself.
- Luxon has both a Duration type and an Interval type
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.
Creation
Operation | Moment | Luxon | Notes |
---|---|---|---|
Now | moment() |
DateTime.local() |
|
From ISO | moment(String) |
DateTime.fromISO(String) |
|
From RFC 2822 | moment(String) |
DateTime.fromRFC2822(String) |
|
From custom format | moment(String, String) |
DateTime.fromString(String, String) |
|
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...) |
Luxon 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 keepZone 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 }) |
Transformation
Operation | Moment | Luxon | Notes |
---|---|---|---|
Addition | add(Number, String) |
plus(Object) |
Like dt.plus({ 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) |
zone(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) |
Query
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 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 |
|
-------------------------------------------- | ------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
Output
Basics
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(...) |
|
RFC 2822 | toRFC2822() |
||
HTTP date string | toHTTP() |
||
JS Date | toDate() |
toJSDate() |
|
Epoch time | valueOf() |
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 . |
Humanization
Luxon doesn't support these, and won't until the Relative Time Format proposal lands in browsers.
Operation | Moment | Luxon |
---|---|---|
Time from now | fromNow() |
None |
Time from other time | from(Moment) |
None |
Time to now | toNow() |
None |
Time to other time | `to(Moment) | None |
"Calendar time" | calendar() |
None |
Durations
Moment Durations and Luxon Durations are broadly similar in purpose and capabilities. The main differences are:
- 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. - Luxon does not (yet) have an equivalent of Moment's
humanize
method - Like DateTimes, Luxon Durations have separate methods for creating objects from different sources.
See the Duration API docs for more.
Intervals
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.