Timezone sensitive date comparisons in Javascript

Tags: , , , ,



I’m determining the difference between two calendar dates in Javascript in an Ember app. I’m currently using date-fns’s differenceInCalendarDays.

Now that mostly provides the desired results, the only issue is that I need to handle calculating the difference between 2 dates that is sensitive to a timezone (not the local timezone to the browser).

As far as I’m aware JS Dates are tracked as UTC, with no timezone stored on the date object itself. Any timezone localization I’ve done in JS has been outputting a string. Is there a good library or way to accomplish differenceInCalendarDays while taking into account the timezone?

const daysAgo = this.intl.formatRelative(-differenceInCalendarDays(new Date(), someOtherDay), {
    unit: 'day', 
    numeric: 'auto', 
    timeZone: 'this.intl.timeZone
});

This is a small sample of what I’m doing, obviously differenceInCalendarDays will resolve to a number which won’t take into account any timeZone. The docs for differenceInDays is timezone sensitive to the browser’s local time (which is not helpful here), but differenceInCalendarDays makes no such mention. Any help would be greatly appreciated!

Answer

Logically, the difference between two calendar dates such as 2020-01-01 and 2020-01-02 is not time zone sensitive, nor does it involve time at all. It is exactly one day. In this context a day is not 24 hours, but rather it is a logical division of a year. Think of it as a square on a paper calendar.

However – at any given instant two different time zones might be on the same calendar date, or they might be on two different calendar dates. Thus, a time zone matters when determining the date that it is “now” (or “today”, “yesterday”, “tomorrow”, etc.)

To illustrate both points and hopefully answer your question, the following code can be used to get the number of days passed since “today” in a given time zone:

function daysSince(year, month, day, timeZone) {

  // Create a DateTimeFormat object for the given time zone.
  // Force 'en' for English to prevent issues with languages that don't use Arabic numerals.
  const formatter = new Intl.DateTimeFormat('en', { timeZone });
  
  // Format "now" to a parts array, then pull out each part.
  const todayParts = formatter.formatToParts();  // now is the default when no Date object is passed.
  const todayYear = todayParts.find(x=> x.type === 'year').value;
  const todayMonth = todayParts.find(x=> x.type === 'month').value;
  const todayDay = todayParts.find(x=> x.type === 'day').value;
  
  // Make a pseudo-timestamp from those parts, abusing Date.UTC.
  // Note we are intentionally lying - this is not actually UTC or a Unix/Epoch timestamp.
  const todayTimestamp = Date.UTC(+todayYear, todayMonth-1, +todayDay);

  // Make another timestamp from the function input values using the same approach.
  const otherTimestamp = Date.UTC(+year, month-1, +day);

  // Since the context is the same, we can subtract and divide to get number of days.
  return (todayTimestamp - otherTimestamp) / 864e5;
}

// example usage:
console.log("US Pacific: " + daysSince(2020, 1, 1, 'America/Los_Angeles'));
console.log("Japan: " + daysSince(2020, 1, 1, 'Asia/Tokyo'));

This approach only works because UTC doesn’t have transitions (such as DST or changes in standard time offset).

Also note that I don’t use Date objects here, because we’d have to be very careful about how those objects were constructed. If you only have a Date object coming from a date picker UI, that object was likely created assuming local time – not the time in a particular time zone. So, you’ll want to pull out the year, month, and day from that object before continuing. For example:

daysSince(dt.getFullYear(), dt.getMonth() + 1, dt.getDate(), 'America/New_York');

Pay close attention to +1 and -1. The Date object uses 0-based months, but I prefer 1-based.



Source: stackoverflow