Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sebinsua/3c87afd621e9bd2cbb8bb51c6c1a87a3 to your computer and use it in GitHub Desktop.
Save sebinsua/3c87afd621e9bd2cbb8bb51c6c1a87a3 to your computer and use it in GitHub Desktop.
// A JavaScript `Date` is fundamentally specified as the number of milliseconds that have elapsed since the ECMAScript epoch,
// which is defined as January 1, 1970, UTC (equivalent to the UNIX epoch).
//
// However, depending on how you construct a `Date` the date passed in can be parsed as a date in your local time zone or a UTC date.
// For instance `"2022-08-01"` will be treated as a UTC value, but `"2022-08-01T00:00:00.000"` will be treated as if it
// is a localised date and offset before setting the underlying value of the `Date`. Similarly, there is a difference
// between passing in `(2022, 7, 1, 0, 0, 0, 0)` which is assumed to be localised and `Date.UTC(2022, 7, 1, 0, 0, 0, 0)`
// which is a unix timestamp (in milliseconds).
// Firstly, let's print our local timezone and the current timezone offset:
console.log(
"local time zone:",
Intl.DateTimeFormat().resolvedOptions().timeZone
);
console.log("local time zone offset:", new Date().getTimezoneOffset());
const dateFromLocalString = new Date("2022-08-01T00:00:00.000");
console.log(
"dateFromLocalString formatted in local time",
dateFromLocalString.toString()
);
console.log(
"dateFromLocalString formatted in UTC time",
dateFromLocalString.toISOString()
);
console.log(
"dateFromLocalString has this offset",
dateFromLocalString.getTimezoneOffset()
);
// Note: months are zero-indexed.
const dateFromLocalIntegers = new Date(2022, 7, 1, 0, 0, 0, 0);
console.log(
"dateFromLocalIntegers formatted in local time",
dateFromLocalIntegers.toString()
);
console.log(
"dateFromLocalIntegers formatted in UTC time",
dateFromLocalIntegers.toISOString()
);
console.log(
"dateFromLocalIntegers has this offset",
dateFromLocalIntegers.getTimezoneOffset()
);
const dateFromUTC = new Date(
// `Date.UTC` returns a unix timestamp which is always UTC.
Date.UTC(2022, 7, 1, 0, 0, 0, 0)
);
console.log("dateFromUTC formatted in local time", dateFromUTC.toString());
console.log("dateFromUTC formatted in UTC time", dateFromUTC.toISOString());
console.log("dateFromUTC has this offset", dateFromUTC.getTimezoneOffset());
const dateFromUTCString = new Date("2022-08-01T00:00:00.000Z");
console.log(
"dateFromUTCString formatted in local time",
dateFromUTCString.toString()
);
console.log(
"dateFromUTCString formatted in UTC time",
dateFromUTCString.toISOString()
);
console.log(
"dateFromUTCString has this offset",
dateFromUTCString.getTimezoneOffset()
);
console.log(
`dateFromLocalString !== dateFromUTC; dateFromLocalIntegers !== dateFromUTC`,
{
dateFromLocalString: dateFromLocalString.getTime(),
dateFromLocalIntegers: dateFromLocalIntegers.getTime(),
dateFromUTC: dateFromUTC.getTime(),
localStringCreatedDateNotUTC:
dateFromLocalString.getTime() !== dateFromUTC.getTime(),
localIntegerCreatedDateNotUTC:
dateFromLocalIntegers.getTime() !== dateFromUTC.getTime(),
// The `getTimezoneOffset()` method returns the difference,
// in minutes, between a date as evaluated in the UTC time zone,
// and the same date as evaluated in the local time zone.
timezoneOffsetAlwaysTheSame:
dateFromLocalString.getTimezoneOffset() ===
dateFromUTC.getTimezoneOffset() &&
dateFromLocalIntegers.getTimezoneOffset() ===
dateFromUTC.getTimezoneOffset() &&
dateFromUTCString.getTimezoneOffset() === dateFromUTC.getTimezoneOffset()
}
);
// Unfortunately even if you have a `Date` that was created using
// a UTC string or unix timestamp (UTC), it still can provide local
// date/time components if you're not careful.
//
// For example:
// Mon Aug 01 2022 01:00:00 GMT+0100 (British Summer Time)
console.log(dateFromUTC.toString());
// 01:00:00 GMT+0100 (British Summer Time)
console.log(dateFromUTC.toTimeString());
// As you can see, if you're in Britain (and reading this during the summer),
// the time component is altered from UTC with regard to British Summer Time (BST / GMT + 1).
//
// Often this isn't noticeable, since, during the non-summer months,
// our timezone is GMT which is the same as UTC.
//
// Obviously, this problem is way worse if you are in a local timezone
// with a larger positive/negative timezone offset, as this can cause
// the local representation to be in the wrong day, month or even year
// when you are at an edge (e.g. first or last day of the month).
//
// `date-fns#format` is an example of a library which formats dates
// using your local timezone.
//
// Fortunately, `date-fns-tz` has `formatInTimeZone` as well as a
// few other useful helpers to correct this.
// However, unfortunately, all of the date manipulation functions of
// `date-fns` use the underlying local `get*` and `set*` `Date` methods,
// so all date manipulation will be broken for UTC Dates at the edges.
//
// See:
const startOfYearUTCDate = new Date(
// `Date.UTC` returns a unix timestamp which is always UTC.
Date.UTC(2022, 0, 1, 0, 0, 0, 0)
);
// To *break* this change your timezone to "Baltimore, MD - United States"
// which is currently Eastern Daylight Time (EDT), a `getTimeZoneOffset` of 240.
//
// This can be done either through your system settings or by running this script
// in `node` using an environment variable of `TZ='America/New_York'`.
console.log(
`Localised year component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getFullYear()
);
// But if we use the correct UTC method it'd have worked...
console.log(
`UTC year component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCFullYear()
);
// And the same thing is the case for `getMonth`, `getDate`, `getDay`
// and so on...
console.log(
`Localised month component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getMonth()
);
console.log(
`UTC month component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCMonth()
);
console.log(
`Localised month component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getMonth()
);
console.log(
`UTC month component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCMonth()
);
console.log(
`Localised date component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getDate()
);
console.log(
`UTC date component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCDate()
);
console.log(
`Localised minutes component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getMinutes()
);
console.log(
`UTC minutes component of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCMinutes()
);
// This one probably seems unimportant but `getDay` is often
// used by logic which calculates weekends, so getting this
// one wrong breaks the business concept of week days.
console.log(
`Localised day of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getDay()
);
console.log(
`UTC day of UTC Date (${startOfYearUTCDate.toISOString()}):`,
startOfYearUTCDate.getUTCDay()
);
// Anybody that is using `date-fns` (or for that matter
// `date-io/date-fns` with a `material-ui` `DatePicker`)
// will be affected by this, because `date-fns` always
// uses these localised methods internally.
//
// e.g.
// `addMonths`: https://github.com/date-fns/date-fns/blob/7e8026159b9496eddc3a683895d2e0566715d4bb/src/addMonths/index.ts
// `isWeekend`: https://github.com/date-fns/date-fns/blob/7e8026159b9496eddc3a683895d2e0566715d4bb/src/isWeekend/index.ts
// `eachMonthOfInterval`: https://github.com/date-fns/date-fns/blob/7e8026159b9496eddc3a683895d2e0566715d4bb/src/eachMonthOfInterval/index.ts
// `differenceInMonths`: https://github.com/date-fns/date-fns/blob/7e8026159b9496eddc3a683895d2e0566715d4bb/src/differenceInMonths/index.ts
//
// This comment explains the underlying problem quite nicely:
// https://github.com/date-fns/date-fns/issues/2519#issuecomment-907699984
//
// It will result in problems like:
// https://github.com/date-fns/date-fns/issues/571
// https://github.com/date-fns/date-fns/issues/2484
// https://github.com/date-fns/date-fns/issues/2993
// https://github.com/date-fns/date-fns/issues/3118
@sebinsua
Copy link
Author

sebinsua commented Aug 3, 2022

In the example below, the time zone of the first line has a local date of the 31st (of December, 2021), which might be unexpected to someone expecting to see a UTC date.

TZ="America/New_York" node -e "console.log(new Date('2022-01-01').toISOString(), { localDate: new Date('2022-01-01').getDate(), utcDate: new Date('2022-01-01').getUTCDate() })"

TZ="Europe/London" node -e "console.log(new Date('2022-01-01').toISOString(), { localDate: new Date('2022-01-01').getDate(), utcDate: new Date('2022-01-01').getUTCDate() })"

We can also look at how Dates operate in very different time zones. Here is Pacific/Apia:

TZ="Pacific/Apia" node -e "const date = new Date(Date.UTC(2021, 11, 31)); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="Pacific/Apia" node -e "const date = new Date('2021-12-31'); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="Pacific/Apia" node -e "const date = new Date(2021, 11, 31); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="Pacific/Apia" node -e "const date = new Date(Date.UTC(2022, 0, 1)); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="Pacific/Apia" node -e "const date = new Date('2022-01-01'); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="Pacific/Apia" node -e "const date = new Date(2022, 0, 1); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

And, here is America/Los_Angeles:

TZ="America/Los_Angeles" node -e "const date = new Date(Date.UTC(2021, 11, 31)); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="America/Los_Angeles" node -e "const date = new Date('2021-12-31'); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="America/Los_Angeles" node -e "const date = new Date(2021, 11, 31); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="America/Los_Angeles" node -e "const date = new Date(Date.UTC(2022, 0, 1)); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="America/Los_Angeles" node -e "const date = new Date('2022-01-01'); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

TZ="America/Los_Angeles" node -e "const date = new Date(2022, 0, 1); console.log(date, date.toISOString(), { unixTimestamp: date.getTime() / 1000, localDate: date.getDate(), utcDate: date.getUTCDate() })"

And finally, the example below shows how the underlying milliseconds of time since the unix epoch (which is UTC) can be different depending on whether the Date was configured so it is in UTC or your local timezone.:

TZ="Europe/London" node -e "console.log(new Date('2022-08-01').toString(), new Date('2022-08-01').getTime())"

TZ="Europe/London" node -e "console.log(new Date(2022, 7, 1).toString(), new Date(2022, 7, 1).getTime())"

Passing these time values across to Python, gives the correct UTC with the former, but a UTC an hour in the past with the latter:

from datetime import datetime

datetime.utcfromtimestamp(1659312000000 / 1000).strftime('%Y-%m-%d %H:%M:%S')
datetime.utcfromtimestamp(1659308400000 / 1000).strftime('%Y-%m-%d %H:%M:%S')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment