JS Date() and Timestamps

overview

The JS Date() is represented by a millisecond timestamp, milliseconds since Jan 1st 1970, 00:00:00 UTC (Unix Epoch).

read the timestamp

d_2021.getTime() // 1_609_459_200_000 (ms)
// getTimestamp() would be a better name

timestamp at read time:

Date.now() // 1_771_004_506_434 (ms)

build a Date from a timestamp:

new Date(1_627_541_982_738)
// '2021-07-29T06:59:42.738Z'

The REPL logs the date in its ISO 8601 format and in the UTC timezone for debugging.

timestamp difference

d_1971.getTime() - d_1970.getTime() // 31 536 000 000 (ms)

note on timestamp difference between Date objects (do not use)

In JS, We can subtract two Date objects: Date objects implement valueOf() to return the timestamp. The subtraction operator trigger this transformation and subtracts the timestamps. As such, the subtraction works on Dates

d_1971 - d_1970 // 31 536 000 000 (ms)

The Typescript compiler doesn't allow it: it sees a subtraction between objects, because it ignores valueOf().

compare date, check posteriority

We can use the comparison operators on dates, to check posteriority. TypeScript allows comparison between objects. We can also compare timestamps.

d2021 > d2020

JS Date() to calendar components

A timezone is needed to transform a timestamp to calendar date and time. The JS Date() object doesn't store a timezone.

Note: date and time have a precise meaning:

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

As such, we provide the timezone to a timezone-aware helper

The Luxon's DateTime is timezone aware.

get individual calendar components as numbers or strings (Luxon)

we first instantiate a zoneDateTime

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

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

// Or get them all at once from a plain object

const { year, month, day, hour, minute, second } = zdt.toObject()

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

pre-formatted strings (Luxon)

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

custom-format strings (Luxon)

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"

locale aware custom-format strings (Intl)

We provide the locale in the BCP 47 format, which includes a Language subtag and a Region subtag, e.g. en-GB or fr-FR.

If we don't provide a locale, it default to the host's locale (which is separate from the host's timezone). The following examples were made with a en-US host locale.

with a dedicated 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"

with an inline option object:

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

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

Further customize the format (Intl)

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

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

options

type Intl.DateTimeFormatOptions = {

 dateStyle: 	'full' | 'long' | 'medium' | 'short';
 timeStyle: 	'full' | 'long' | 'medium' | 'short';

 // granular method

 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 (do not use)

Those helpers ignore the timezone in which the event happened. They only care about what was the time in the current timezone when the event happened.

For example, a Paris midnight-registered bedtime reads as a 7AM bedtime when the device switches to China timezone, but the bedtime should read as midnight instead.

we should avoid unless we 100% want the host timezone and we want to rely on the implicit timezone behavior.

read the calendar components

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

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

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

Calendar components to JS Date()

A timezone is required.

We talk about timezone aware helpers first.

from calendar components (Luxon):

const zdt = DateTime.fromObject(
    {
        year: 2015,
        month: 6,
        day: 18,
        hour: 13,
        minute: 13,
    },
    {
        zone: "Europe/Paris",
    }
)

const birthDate = zdt.toJSDate()

from calendar components (Temporal):

const zdt = Temporal.ZonedDateTime.from({
    year: 2015,
    month: 6,
    day: 18,
    hour: 13,
    minute: 13,
    timeZone: "Europe/Paris",
})

const birthDate = new Date(zdt.toInstant().epochMilliseconds)

with an offset ISO string (harder)

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(_iso_string_)

new Date("2015-06-18T13:13+02:00") // minute granularity
new Date("2015-06-18T13:13:00+02:00") // seconds granularity
new Date("2015-06-18T11:13:00.000Z") // zero offset, aka UTC

interpreted as midnight UTC.

new Date("2015-06-18") // no time and no timezone, interpreted as UTC 00:00

with local timezone date components (brittle) (do not use)

Note that in this helper, the birth date and time is inputed as the user remembers it, in the timezone he was born, and it will only work if it matches the device timezone. Instead, it should be better to specify the timezone explicitly.

const birth = new Date(2015, 5, 18, 13, 13) // June
// brittle

provide date time string with no offset.

new Date("2015-06-18T13:13:00") // no timezone, interpreted as in host timezone

with UTC components (do not use)

beware, the date-only ISO string assumes UTC, not the host timezone:

new Date("2021-01-01")

Duration

luxon

from milliseconds

const dur = Duration.fromMillis(1_000_000)

dur.toHuman() // '1000000 milliseconds'
dur.rescale().toHuman() // '16 minutes, 40 seconds'

from calendar components

const dur = Duration.fromObject({ hour: 36 })

dur.toHuman() // '36 hours'
dur.rescale().toHuman() // '1 day, 12 hours'
dur.shiftTo("minutes").toHuman() // '2160 minutes'

from difference

const dur = zdt2.diff(zdt1, ["years", "months", "days"])
dur.toHuman() // "3 years, 2 months, 14 days"

Relative time

Duration centric (Luxon)

zdt.toRelative() // "4 hours ago"
zdt.toRelative() // "in 2 days"
zdt.toRelative({ style: "short" }) // "4 hr. ago"
zdt.toRelative({ style: "narrow" }) // "4h ago"
zdt.toRelative({ unit: "seconds" }) // "300 seconds ago"
zdt.toRelative({ unit: "years" }) // "1 year ago"
zdt.toRelative({ round: false }) // "4.5 hours ago"
zdt.toRelative({ base: anchor }) // "5 years ago"

Calendar distance centric (Luxon)

zdt.toRelativeCalendar() // "yesterday"
zdt.toRelativeCalendar() // "tomorrow"
zdt.toRelativeCalendar() // "2 days ago"
zdt.toRelativeCalendar({ unit: "months" }) // "last month"
earlymorning logo

© Antoine Weber 2026 - All rights reserved

JS Date() and Timestamps

overview

The JS Date() is represented by a millisecond timestamp, milliseconds since Jan 1st 1970, 00:00:00 UTC (Unix Epoch).

read the timestamp

d_2021.getTime() // 1_609_459_200_000 (ms)
// getTimestamp() would be a better name

timestamp at read time:

Date.now() // 1_771_004_506_434 (ms)

build a Date from a timestamp:

new Date(1_627_541_982_738)
// '2021-07-29T06:59:42.738Z'

The REPL logs the date in its ISO 8601 format and in the UTC timezone for debugging.

timestamp difference

d_1971.getTime() - d_1970.getTime() // 31 536 000 000 (ms)

note on timestamp difference between Date objects (do not use)

In JS, We can subtract two Date objects: Date objects implement valueOf() to return the timestamp. The subtraction operator trigger this transformation and subtracts the timestamps. As such, the subtraction works on Dates

d_1971 - d_1970 // 31 536 000 000 (ms)

The Typescript compiler doesn't allow it: it sees a subtraction between objects, because it ignores valueOf().

compare date, check posteriority

We can use the comparison operators on dates, to check posteriority. TypeScript allows comparison between objects. We can also compare timestamps.

d2021 > d2020

JS Date() to calendar components

A timezone is needed to transform a timestamp to calendar date and time. The JS Date() object doesn't store a timezone.

Note: date and time have a precise meaning:

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

As such, we provide the timezone to a timezone-aware helper

The Luxon's DateTime is timezone aware.

get individual calendar components as numbers or strings (Luxon)

we first instantiate a zoneDateTime

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

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

// Or get them all at once from a plain object

const { year, month, day, hour, minute, second } = zdt.toObject()

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

pre-formatted strings (Luxon)

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

custom-format strings (Luxon)

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"

locale aware custom-format strings (Intl)

We provide the locale in the BCP 47 format, which includes a Language subtag and a Region subtag, e.g. en-GB or fr-FR.

If we don't provide a locale, it default to the host's locale (which is separate from the host's timezone). The following examples were made with a en-US host locale.

with a dedicated 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"

with an inline option object:

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

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

Further customize the format (Intl)

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

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

options

type Intl.DateTimeFormatOptions = {

 dateStyle: 	'full' | 'long' | 'medium' | 'short';
 timeStyle: 	'full' | 'long' | 'medium' | 'short';

 // granular method

 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 (do not use)

Those helpers ignore the timezone in which the event happened. They only care about what was the time in the current timezone when the event happened.

For example, a Paris midnight-registered bedtime reads as a 7AM bedtime when the device switches to China timezone, but the bedtime should read as midnight instead.

we should avoid unless we 100% want the host timezone and we want to rely on the implicit timezone behavior.

read the calendar components

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

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

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

Calendar components to JS Date()

A timezone is required.

We talk about timezone aware helpers first.

from calendar components (Luxon):

const zdt = DateTime.fromObject(
    {
        year: 2015,
        month: 6,
        day: 18,
        hour: 13,
        minute: 13,
    },
    {
        zone: "Europe/Paris",
    }
)

const birthDate = zdt.toJSDate()

from calendar components (Temporal):

const zdt = Temporal.ZonedDateTime.from({
    year: 2015,
    month: 6,
    day: 18,
    hour: 13,
    minute: 13,
    timeZone: "Europe/Paris",
})

const birthDate = new Date(zdt.toInstant().epochMilliseconds)

with an offset ISO string (harder)

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(_iso_string_)

new Date("2015-06-18T13:13+02:00") // minute granularity
new Date("2015-06-18T13:13:00+02:00") // seconds granularity
new Date("2015-06-18T11:13:00.000Z") // zero offset, aka UTC

interpreted as midnight UTC.

new Date("2015-06-18") // no time and no timezone, interpreted as UTC 00:00

with local timezone date components (brittle) (do not use)

Note that in this helper, the birth date and time is inputed as the user remembers it, in the timezone he was born, and it will only work if it matches the device timezone. Instead, it should be better to specify the timezone explicitly.

const birth = new Date(2015, 5, 18, 13, 13) // June
// brittle

provide date time string with no offset.

new Date("2015-06-18T13:13:00") // no timezone, interpreted as in host timezone

with UTC components (do not use)

beware, the date-only ISO string assumes UTC, not the host timezone:

new Date("2021-01-01")

Duration

luxon

from milliseconds

const dur = Duration.fromMillis(1_000_000)

dur.toHuman() // '1000000 milliseconds'
dur.rescale().toHuman() // '16 minutes, 40 seconds'

from calendar components

const dur = Duration.fromObject({ hour: 36 })

dur.toHuman() // '36 hours'
dur.rescale().toHuman() // '1 day, 12 hours'
dur.shiftTo("minutes").toHuman() // '2160 minutes'

from difference

const dur = zdt2.diff(zdt1, ["years", "months", "days"])
dur.toHuman() // "3 years, 2 months, 14 days"

Relative time

Duration centric (Luxon)

zdt.toRelative() // "4 hours ago"
zdt.toRelative() // "in 2 days"
zdt.toRelative({ style: "short" }) // "4 hr. ago"
zdt.toRelative({ style: "narrow" }) // "4h ago"
zdt.toRelative({ unit: "seconds" }) // "300 seconds ago"
zdt.toRelative({ unit: "years" }) // "1 year ago"
zdt.toRelative({ round: false }) // "4.5 hours ago"
zdt.toRelative({ base: anchor }) // "5 years ago"

Calendar distance centric (Luxon)

zdt.toRelativeCalendar() // "yesterday"
zdt.toRelativeCalendar() // "tomorrow"
zdt.toRelativeCalendar() // "2 days ago"
zdt.toRelativeCalendar({ unit: "months" }) // "last month"