Overview
terminology
- An instant is a point in time. We sometimes use the term timestamp.
- A duration is a quantity of time, and a distance between two instants.
- In the context of calendar components, date and time have specific meaning:
- date refers to calendar components from year to day (year, month and day), e.g.
2026-06-30orYYYY-MM-DD. It matches the Temporal's PlainDate. - time refers to components from hours to milliseconds (or even nanoseconds), e.g.
08:15:00.001orHH:mm:ss.SSS. It matches the Temporal's PlainTime.
- date refers to calendar components from year to day (year, month and day), e.g.
APIs
JavaScript offers severals ways to work with dates, time and durations:
- The
DateAPI - The
TemporalAPI (ES2026) - The
IntlAPI
libraries
In this book, we cover the Luxon library as it supports time zones whereas JS Date doesn't. Luxon also provides formatting capabilities that neither Date and Temporal provide.
JS Date API introduction
instants
JS Date objects represent and store instants (aka dates) as millisecond timestamps. The timestamp is the number of milliseconds since Jan 1st, 1970 at 00:00:00 UTC, aka the Unix Epoch.
read the millisecond timestamps
We read the timestamp from a date object:
d_2021.getTime() // 1,609,459,200,000 (ms)
We can read the current timestamp:
Date.now() // 1,777,109,775,765 (ms)
build a Date from a timestamp
We build a date from a given timestamp (or from the current one)
new Date(1_627_541_982_738) // 2021-07-29T06:59:42.738Z
new Date() // from current timestamp
compare dates
The difference between two timestamps, considered as an absolute value, represent the amount of time between both dates.
d_1971.getTime() - d_1970.getTime() // 31,536,000,000 (ms)
Note: if we try to subtract dates directly, it fails in TypeScript.
We can use the comparison operators on dates to check for posteriority:
d_2021 > d_2020 // true
(misc) console built-in date formatting
JavaScript consoles don't log dates as milliseconds as it's unreadable. They log dates in a calendar format:
- The Node.js REPL use the standard ISO 8601 format (in the UTC time zone).
- The browsers' console uses a lengthier format, using the device time zone.
new Date(1_627_541_982_738)
// 2021-07-29T06:59:42.738Z // Node.js
// Thu Jul 29 2021 08:59:42 GMT+0200 (Central European Summer Time) // Chrome
Formatting dates
calendar components
The main way to format dates is to express them with appropriate calendar components, correct units and the correct time zone.
formatting APIs
We can format dates using Intl or Luxon.
the need for a timezone
An instant must be combined with a time zone to be translated to calendar components.
lack of a time zone in a JS Date
A JS Date doesn't store a time zone: we must read it from another source.
When the time zone is omitted, some formatting helpers fall back to the host machine's time zone, but:
- The host machine time zone doesn't necessarily match the event's time zone. In this case, it becomes impossible to read the event's local date and time on which it occurred.
- Even if the time zones match, there is no guarantee that the machine's time zone will remain fixed, thus it risks breaking the reading later on.
Format dates with Luxon
Luxon uses DateTime as its main API to work with dates. We first instantiate a DateTime instance.
(preliminary) instantiate a DateTime instance
We can instantiate a DateTime from:
- a JS Date
- an ISO string
- a milliseconds timestamp
DateTime.fromJSDate(date, { zone: "Europe/London"; });
DateTime.fromISO(isoString, { zone: "Europe/London"; });
DateTime.fromMillis(ms, { zone: "Europe/London"; });
format calendar component(s) with a format string
Date components format strings:
dt.toFormat("yyyy") // "2026"
dt.toFormat("yy") // "26"
dt.toFormat("MMMM") // "January"
dt.toFormat("MMM") // "Jan"
dt.toFormat("MM") // "01"
dt.toFormat("M") // "1"
dt.toFormat("dd") // "09"
dt.toFormat("d") // "9"
dt.toFormat("EEEE") // "Friday"
dt.toFormat("EEE") // "Fri"
Time component format strings:
dt.toFormat("HH:mm:ss") // "08:05:05"
dt.toFormat("H 'hours'") // "8 hours"
dt.toFormat("m 'minutes'") // "5 minutes"
dt.toFormat("s 'seconds'") // "5 seconds"
dt.toFormat("s.SSS's'") // "5.123s"
// 12 hour format
dt.toFormat("h:mm a") // "8:05 AM"
add literal text
We can add literal text safely with single quotes:
dt.toFormat("MMMM d 'at' HH:mm") // "June 19 at 14:30"
Some characters such as slashes can be added unquoted as they don't clash with date and time tokens:
dt.toFormat("yyyy/MM/dd") // "2026/06/19"
ISO formatting
DateTime supports three ISO string helpers:
dt.toISO() // "2026-02-13T19:50:15.123+01:00" <-- Includes offset
dt.toISODate() // "2026-02-13"
dt.toISOTime({ includeOffset: false }) // '07:34:19.361'
(optional) read individual calendar components
We can read the calendar components, as numbers, or sometimes as strings:
const dt = DateTime.fromJSDate(date, { zone: "Europe/London" })
// Getters
dt.zoneName // 'Europe/London'
dt.year
dt.month // 1-12
dt.day // 1-31
dt.hour // 0-23
dt.minute // 0-59
dt.second // 0-59
dt.millisecond
dt.weekdayLong // 'Monday', 'Tuesday', etc.
dt.monthLong // 'January', 'February', etc.
For destructuring, we convert to an object beforehand:
// Make plain object and destructure
const { year, month, day, hour, minute, second } = dt.toObject()
Format dates with Intl
overview
Intl relies on creating and using formatters, providing options and a locale:
const formatter = new Intl.DateTimeFormat("en-US", options)
formatter.format(new Date(1781860473660)) // "June 19, 2026"
options
An options object sets the format for the calendar components we want to include. If a component is left out (or is set to undefined), it is omitted from the formatted output:
const options: Intl.DateTimeFormatOptions = {
timeZone: "America/Los_Angeles",
year: "numeric", // "2026"
month: "long", // "June"
day: "numeric", // "19"
}
Note: We specify the time zone in the formatter options.
locale support
Intl supports a locale to customize the format. It is sometimes required. It is not part of the options object: we provide it separately.
The locale follows the BCP 47 format, e.g. en-GB or fr-FR.
const formatter = new Intl.DateTimeFormat("en-GB", options)
If undefined, Intl defaults to the host machine's locale.
create a custom formatter
Create a fully-fledged, reusable formatter:
const formatter = new Intl.DateTimeFormat("fr-FR", {
// timeZone: "Europe/Paris",
// month: "long",
// day: "numeric",
// hour: "2-digit",
// minute: "2-digit",
})
formatter.format(date) // "19 juin à 11:14"
use a built-in formatter with options
Built-in formatters use Intl under the hood: the options object is of the same format:
date.toLocaleDateString("fr-FR", options) // "19 juin à 11:14"
locale-aware built-in formatters on Date instances
Note: those function work with a locale and an optional options object with the timezone.
Using undefined for the locale means to use the host's locale:
// const options = { timeZone: "America/Los_Angeles" }
// device locale: en-US
// devide time zone: Europe/Paris
date.toLocaleString("fr-FR") // 29/01/2022, 21:52:08
date.toLocaleString("fr-FR", options) // 29/01/2022 12:52:08
date.toLocaleString(undefined) // 1/29/2022, 9:52:08 PM
date.toLocaleString(undefined, options) // 1/29/2022, 12:52:08 PM
date.toLocaleTimeString("en-US", options) // 12:52:08 PM
// ... other variants
date.toLocaleDateString("en-US", options) // 1/29/2022
// ... other variants
(advanced) list of options
see the list of options (Intl.DateTimeFormatOptions).
type Intl.DateTimeFormatOptions = {
// shortcut options
dateStyle: 'full' | 'long' | 'medium' | 'short';
timeStyle: 'full' | 'long' | 'medium' | 'short';
// granular options
weekday: 'long' | 'short' | 'narrow';
year: 'numeric' | '2-digit';
month: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow';
day: 'numeric' | '2-digit';
hour: 'numeric' | '2-digit';
minute: 'numeric' | '2-digit';
second: 'numeric' | '2-digit';
fractionalSecondDigits: 1 | 2 | 3;
timeZoneName: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric';
timeZone: string;
hour12: boolean | 'h11' | 'h12' | 'h23' | 'h24';
};
Other formatting
JS helpers that rely on the host timezone (avoid)
Those helpers ignore the timezone in which the event happened (which isn't stored by the JS Date). Instead, they display the event as it reads in the host machine timezone.
For example, an event that occurred at midnight in Paris is read as 7AM with no mention of midnight at all when the device is in the China timezone (getHours() on a China-based machine reads as 7.
pre-formatted strings (relying on the host machine timezone)
Note: those helpers don't accept a locale or an options object with a timezone.
date.toString()
// Sun Jun 12 2022 11:44:53 GMT+0200 (Central European Summer Time)
date.toDateString()
// Sun Jun 12 2022
date.toTimeString()
// 11:44:53 GMT+0200 (Central European Summer Time)
to UTC timezone ISO string
ISO8601 is an international standard. It indicates both the date component and the time component. It may provide the timezone.
birthDate.toISOString() // 2015-06-18T11:13:00.000Z
Date.parse("2015-06-18T11:13:00.000Z")
(optional) read calendar components
// the event occured on January 1st 2025, at 00:00 AM (Paris), device in China
birth.getFullYear() // 2025
birth.getMonth() // 0 (January)
birth.getDate() // 1 (for 1st)
birth.getHours() // 7
birth.getMinutes() // 0
// the UNIX Epoch instant
// Device in (Europe/Paris)
new Date(0).getHours() // 1
Parsing Calendar components
A timezone is required to disambiguate the calendar components.
derive JS Dates from timezone aware objects
From a Luxon object:
const dt = DateTime.fromObject(
{
year: 2015,
month: 6,
day: 18,
hour: 13,
minute: 13,
},
{
zone: "Europe/Paris",
},
)
const birthDate = dt.toJSDate()
From a Temporal ZonedDateTime object (through milliseconds conversion):
const dt = Temporal.ZonedDateTime.from({
year: 2015,
month: 6,
day: 18,
hour: 13,
minute: 13,
timeZone: "Europe/Paris",
})
const birthDate = new Date(dt.toInstant().epochMilliseconds)
derive the JS date from an ISO string
With an UTC offset in the date time string. This is worse than providing a timezone because it requires knowledge about the offset of the location at this date.
new Date(isoString)
new Date("2015-06-18T13:13+02:00") // offset ISO, minutes granularity
new Date("2015-06-18T13:13:00+02:00") // offset ISO, seconds granularity
new Date("2015-06-18T11:13:00.000Z") // UTC ISO (Z)
helpers that assume UTC
from a date-only ISO string:
new Date("2015-06-18") // assumed to be UTC
helpers that assume host timezone (brittle, do not use)
The helpers assume the local timezone:
const birth = new Date(2015, 5 /* June */, 18, 13, 13)
new Date("2015-06-18T13:13:00")
Format Durations with Luxon
Luxon uses Duration to manage durations.
instantiation
The unit(s) we pick for instantiation determines the default unit(s) being used:
Duration.fromMillis(1_000_000)
// dur.toHuman() // '1000000 milliseconds'
Duration.fromObject({ hour: 36 })
// dur.toHuman() // '36 hours'
We can build a duration from two DateTime instances, and set the units being used:
const dur = dt2.diff(dt1, ["years", "months", "days"])
// dur.toHuman() // "3 years, 2 months, 14 days"
control the unit(s)
We configure the duration units directly on the duration object, controlling subsequent outputs:
rescale()automatically determines which units to use. It eagerly uses the higher magnitude units, then sets them as the current units:
// dur.toHuman() // '1000000 milliseconds'
dur.rescale().toHuman() // '16 minutes, 40 seconds'
shiftTo()aims to set the units imperatively, and set them as the current units:
// dur.toHuman() // '1 day, 12 hours'
dur.shiftTo("hours", "minutes").toHuman() // '36 hours, 0 minutes'
dur.shiftTo("minutes").toHuman() // '2160 minutes'
output the configured duration
toHuman() outputs a human-readable duration. It uses the units as configured:
// 1.0 configuration
// 2.0 output
dur.toHuman() // '1 day, 12 hours'
dur.toHuman({ listStyle: "long" }) // '1 day and 12 hours'
Format Durations with Intl
Format durations with Intl.DurationFormat (limited).
Relative time
We use methods from Luxon's DateTime instances.
describe the distance in time from now
A relative time describes how far we are, as of now, from an event that happens in the past or in the future. It is a duration relative to now. We focus on the duration, such as 3 hours ago, or in 2 minutes.
dt.toRelative() // "4 hours ago"
dt.toRelative() // "in 2 days"
dt.toRelative({ style: "short" }) // "4 hr. ago"
dt.toRelative({ style: "narrow" }) // "4h ago"
dt.toRelative({ unit: "seconds" }) // "300 seconds ago"
dt.toRelative({ unit: "years" }) // "1 year ago"
dt.toRelative({ round: false }) // "4.5 hours ago"
dt.toRelative({ base: anchor }) // "5 years ago"
prioritize the calendar aspects, day granularity, day and month difference
We describe where the event takes place in the calendar relative to now. It uses calendar date components, with precision capped to day difference, such as today or yesterday.
dt.toRelativeCalendar() // "today"
dt.toRelativeCalendar() // "yesterday"
dt.toRelativeCalendar() // "tomorrow"
dt.toRelativeCalendar() // "2 days ago"
dt.toRelativeCalendar() // "last month"