Timezone-specific Date Accessors in JavaScript

Here's how to access a date's fields for a specific timezone.

What You Have in Date

JavaScript Date has accessors for date fields. It only has two options, here seen for the hours field:

  • Get the field in local time (eg, new Date().getHours())

  • Get the field in UTC time (eg, new Date().getUTCHours())

As far as the Date API is concerned, "local" means the locality of the runtime from which you are accessing. This could be the server or the browser that this code is run from. It does not mean a specified timezone (because you can't specify a timezone in this API).

Input Must Match Output

If you put UTC in, you need to ask for UTC out.

If you put a local date in, you need to ask for a local date out -- in the same locality (eg, EDT -> EDT or MDT -> MDT). But how do you do that since local accessors are all based on the locality of the runtime?

A Scenario

This happened to me. Here's a time, represented in 3 timezones.

7:15pm MDT (-6 UTC)
9:15pm EDT (-4 UTC)
1:15am UTC

This was an incoming date, input as ET using a strategy like this:

import { DateTime } from 'luxon'

const date = new Date(
  DateTime.local(2023, 6, 22, 21, 15, 0, {
    zone: 'America/New_York',
  }).toString()
);

I needed to determine if that incoming date was within a time range, kept in ET as well, hour 9 (9am) to 21 (9pm).

First mistake: I didn't match timezone of output to input. I was doing:

const hour = date.getHours()
return hour >= 9 && hour < 21

getHours() is returning 19 because I'm executing this code from MT. That's calculated as in range in this case. In reality 9:15pm ET is out of this range. Bug.

getUTCHours() would return 1 and be false, out of range. This is correct for now, but the logic is wrong, so the answer would be wrong with different input.

So, how do we resolve this? Match the output (accessed) hours with the input date locality (ET). The good news: There's a native JS API for this.

Intl.DateTimeFormat to Access By Timezone

This API is not on Date It's on Intl, and it's Intl.DateTimeFormat.

In my mind this API is for formatting Dates for display. And that's probably true. But, we can also use it conceptually and practically as an accessor, here shown for hours:

function getHoursInTz(timeZone, date) {
  return parseInt(
    new Intl.DateTimeFormat([], {
      timeZone,
      hour: "numeric",
      hourCycle: "h24",
    }).format(date),
    10
  );
}

Note that this API returns a string, so we convert it to a number for our number comparisons.

What's this going to give us?

const hour = getHoursInTz('America/New_York', date)
return hour >= 9 && hour < 21

hour will be 21, and this will be false, out of range. Yay! Correct answer and correct logic. Will work all the time. Matching input timezone without accessed timezome.

Pretty cool, I guess. Probably would've been simpler for calculations to use UTC everywhere (oft-heard advice that I broke).