Dates to calendar components

date and time terminology in the context of calendar components

date and time have a specific meaning in the context of calendar components:

  • date refers to calendar components from year to day.
  • time refers to components from hours to milliseconds.

the need for a timezone

A timezone is needed to transform a timestamp to some calendar components (date and time).

Events are most often closely linked to a timezone, the one where they happen, and it's preferable to retain this information and display the event's date in that timezone. The only exception is when one explicitely asks to read the event in another timezone, or if the event is not linked to a specific timezone.

JS Dates objects don't store a timezone: they don't retain this information.

Most Dates helpers are hardcoded to make use of the host machine timezone, which is not desirable as we've seen.

Instead, we want to use timezone-aware helpers, ones that we initialize with the correct timezone, the one where the event happened.

The Luxon library and the Temporal API provide such helpers.

The Luxon library provides a DateTime class to create timezone-aware instances, from which we extract calendar components.

Luxon

get individual calendar components (Luxon)

We initialize a DateTime instance, then read the calendar components, as numbers or strings:

const zdt = DateTime.fromJSDate(bedtimeJSDate, { zone: "Europe/Paris" })

// Getters
zdt.zoneName // 'Europe/Paris'
zdt.year
zdt.month // 1-12
zdt.day // 1-31
zdt.hour // 0-23
zdt.minute // 0-59
zdt.second // 0-59
zdt.millisecond

zdt.weekdayLong // 'Monday', 'Tuesday', etc.
zdt.monthLong // 'January', 'February', etc.

// Make plain object and destructure
const { year, month, day, hour, minute, second } = zdt.toObject()

get single-component strings and multi-component strings (Luxon)

custom-format string

zdt.toFormat("yyyy") // "2026"
zdt.toFormat("yy") // "26"

zdt.toFormat("M") // "1"
zdt.toFormat("MM") // "01"
zdt.toFormat("MMM") // "Jan"
zdt.toFormat("MMMM") // "January"

zdt.toFormat("d") // "9"
zdt.toFormat("dd") // "09"

zdt.toFormat("EEE") // "Fri"
zdt.toFormat("EEEE") // "Friday"

ISO string

zdt.toISODate() // "2026-02-13"
zdt.toISO() // "2026-02-13T19:50:15.123+01:00" <-- Includes offset

Intl formatter

locale

We provide the locale in the BCP 47 format, e.g. en-GB or fr-FR. The locale is separate from the timezone. If omitted, Intl defaults to the host machine's locale.

reusable formatter

Create a fully-fledged, reusable formatter:

const formatter = new Intl.DateTimeFormat("en-GB", {
    timeZone: "Europe/Paris",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
})

formatter.format(date)
// "3 March at 14:05"

Use a pre-defined formatter in addition to an options object:

const options = {
    timeZone: "America/Los_Angeles",
    year: "numeric", // "2025"
    month: "long", // "November"
    day: "numeric", // "8"
}

twoBillionDate.toLocaleDateString("fr-FR", options)
// '17 mai 2033'

options overview

The option object sets the format for each component we want to include. If a component is not mentioned in the object, or set to undefined, it is left out from the output.

const options = {
    weekday: undefined,
    day: "numeric",
    month: "long",
    year: "numeric",
}

list of options:

type Intl.DateTimeFormatOptions = {

 // shortcut options
 dateStyle: 	'full' | 'long' | 'medium' | 'short';
 timeStyle: 	'full' | 'long' | 'medium' | 'short';

 // granular options
 weekday: 	'long' | 'short' | 'narrow';
 era: 		'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';

 timeZoneName: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric';

 timeZone: string;
 fractionalSecondDigits: 1 | 2 | 3;

 hour12: boolean | 'h11' | 'h12' | 'h23' | 'h24';
 calendar: 'gregory' | 'buddhist' | 'chinese'

 localeMatcher: 'best fit' | 'lookup';
 formatMatcher: 'best fit' | 'basic' | 'standard';

};

JS helpers that rely on the host timezone (avoid)

Those helpers ignore the timezone in which the event happened. They display the event's time as it reads in the current timezone.

For example, a Paris midnight-registered bedtime is transformed to a 7AM bedtime because the device switched to China timezone. Instead, the bedtime should stick to midnight, unless we specifically want to read when the event happened in the current timezone.

For example, if we read the UNIX epoch's getHours() from a Paris-based machine, it reads as 1.

calendar components read as in the current timezone

birth.getFullYear() // 2025
birth.getMonth() // 0 for January, 11 for December
birth.getDate() // 18 for 18th
birth.getHours()
birth.getMinutes()

// Example: UNIX Epoch instant
// Device in (Europe/Paris)
// The instant is read in the Paris timezone
new Date(0).getHours() // 1

pre-formatted strings

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)

locale aware pre-formatted strings

The device's locale is picked if omitted.

date.toLocaleString() // 1/29/2022, 9:52:08 PM
date.toLocaleString("fr-FR") // 29/01/2022, 21:52:08

date.toLocaleDateString() // 1/29/2022
date.toLocaleDateString("fr-FR") // 29/01/2022

date.toLocaleTimeString() // 9:52:08 PM
date.toLocaleTimeString("fr-FR") // 21:52:08

others

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")
earlymorning logo

© Antoine Weber 2026 - All rights reserved

Dates to calendar components

date and time terminology in the context of calendar components

date and time have a specific meaning in the context of calendar components:

  • date refers to calendar components from year to day.
  • time refers to components from hours to milliseconds.

the need for a timezone

A timezone is needed to transform a timestamp to some calendar components (date and time).

Events are most often closely linked to a timezone, the one where they happen, and it's preferable to retain this information and display the event's date in that timezone. The only exception is when one explicitely asks to read the event in another timezone, or if the event is not linked to a specific timezone.

JS Dates objects don't store a timezone: they don't retain this information.

Most Dates helpers are hardcoded to make use of the host machine timezone, which is not desirable as we've seen.

Instead, we want to use timezone-aware helpers, ones that we initialize with the correct timezone, the one where the event happened.

The Luxon library and the Temporal API provide such helpers.

The Luxon library provides a DateTime class to create timezone-aware instances, from which we extract calendar components.

Luxon

get individual calendar components (Luxon)

We initialize a DateTime instance, then read the calendar components, as numbers or strings:

const zdt = DateTime.fromJSDate(bedtimeJSDate, { zone: "Europe/Paris" })

// Getters
zdt.zoneName // 'Europe/Paris'
zdt.year
zdt.month // 1-12
zdt.day // 1-31
zdt.hour // 0-23
zdt.minute // 0-59
zdt.second // 0-59
zdt.millisecond

zdt.weekdayLong // 'Monday', 'Tuesday', etc.
zdt.monthLong // 'January', 'February', etc.

// Make plain object and destructure
const { year, month, day, hour, minute, second } = zdt.toObject()

get single-component strings and multi-component strings (Luxon)

custom-format string

zdt.toFormat("yyyy") // "2026"
zdt.toFormat("yy") // "26"

zdt.toFormat("M") // "1"
zdt.toFormat("MM") // "01"
zdt.toFormat("MMM") // "Jan"
zdt.toFormat("MMMM") // "January"

zdt.toFormat("d") // "9"
zdt.toFormat("dd") // "09"

zdt.toFormat("EEE") // "Fri"
zdt.toFormat("EEEE") // "Friday"

ISO string

zdt.toISODate() // "2026-02-13"
zdt.toISO() // "2026-02-13T19:50:15.123+01:00" <-- Includes offset

Intl formatter

locale

We provide the locale in the BCP 47 format, e.g. en-GB or fr-FR. The locale is separate from the timezone. If omitted, Intl defaults to the host machine's locale.

reusable formatter

Create a fully-fledged, reusable formatter:

const formatter = new Intl.DateTimeFormat("en-GB", {
    timeZone: "Europe/Paris",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
})

formatter.format(date)
// "3 March at 14:05"

Use a pre-defined formatter in addition to an options object:

const options = {
    timeZone: "America/Los_Angeles",
    year: "numeric", // "2025"
    month: "long", // "November"
    day: "numeric", // "8"
}

twoBillionDate.toLocaleDateString("fr-FR", options)
// '17 mai 2033'

options overview

The option object sets the format for each component we want to include. If a component is not mentioned in the object, or set to undefined, it is left out from the output.

const options = {
    weekday: undefined,
    day: "numeric",
    month: "long",
    year: "numeric",
}

list of options:

type Intl.DateTimeFormatOptions = {

 // shortcut options
 dateStyle: 	'full' | 'long' | 'medium' | 'short';
 timeStyle: 	'full' | 'long' | 'medium' | 'short';

 // granular options
 weekday: 	'long' | 'short' | 'narrow';
 era: 		'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';

 timeZoneName: 'long' | 'short' | 'shortOffset' | 'longOffset' | 'shortGeneric' | 'longGeneric';

 timeZone: string;
 fractionalSecondDigits: 1 | 2 | 3;

 hour12: boolean | 'h11' | 'h12' | 'h23' | 'h24';
 calendar: 'gregory' | 'buddhist' | 'chinese'

 localeMatcher: 'best fit' | 'lookup';
 formatMatcher: 'best fit' | 'basic' | 'standard';

};

JS helpers that rely on the host timezone (avoid)

Those helpers ignore the timezone in which the event happened. They display the event's time as it reads in the current timezone.

For example, a Paris midnight-registered bedtime is transformed to a 7AM bedtime because the device switched to China timezone. Instead, the bedtime should stick to midnight, unless we specifically want to read when the event happened in the current timezone.

For example, if we read the UNIX epoch's getHours() from a Paris-based machine, it reads as 1.

calendar components read as in the current timezone

birth.getFullYear() // 2025
birth.getMonth() // 0 for January, 11 for December
birth.getDate() // 18 for 18th
birth.getHours()
birth.getMinutes()

// Example: UNIX Epoch instant
// Device in (Europe/Paris)
// The instant is read in the Paris timezone
new Date(0).getHours() // 1

pre-formatted strings

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)

locale aware pre-formatted strings

The device's locale is picked if omitted.

date.toLocaleString() // 1/29/2022, 9:52:08 PM
date.toLocaleString("fr-FR") // 29/01/2022, 21:52:08

date.toLocaleDateString() // 1/29/2022
date.toLocaleDateString("fr-FR") // 29/01/2022

date.toLocaleTimeString() // 9:52:08 PM
date.toLocaleTimeString("fr-FR") // 21:52:08

others

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")