js-temporal/temporal-polyfill

Range Error when trying to formate dates using calendar other then `gregory` or `iso8601`

khawarizmus opened this issue · 3 comments

Given this example test:

const dateTimeUTCString = "20201001T090000Z"; // UTC date-time

const dateTimeUTC = Temporal.Instant.from(
      dateTimeUTCString
    ).toZonedDateTime({
      timeZone: "UTC",
      calendar: "gregory", // <-- test success depends on this
});
    
expect(iCalendarTimeFormat(dateTimeUTC)).toBe("090000"); // <-- fails if calendar is other then gregory or iso8601

where iCalendarTimeFormat is as follows:

export function iCalendarTimeFormat(date: Date | DateTime): string {
  return date
    .toLocaleString("us-EN", {
      hour12: false,
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    })
    .split(":")
    .join("");
}

the test fails is we change the calendar property of the ZonedDate to anything other then gregory or iso8601 throwing the following error:

RangeError: cannot format ZonedDateTime with calendar [inserted calendar] in locale with calendar gregory

Tested with values like: islamic-umalqura, japanes, chinese, coptic etc..

If it helps, here is the part of the code that is responsible for throwing the error

My question is why this is failing and what does the error actually mean?

Fixed the tests by changing iCalendarTimeFormat implementation to this:

export function iCalendarTimeFormat(date: Date | DateTime): string {
  return date
    .toLocaleString("us-EN", {
      hour12: false,
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
      ...(date instanceof Temporal.ZonedDateTime ||
      date instanceof Temporal.PlainDateTime
        ? { calendar: date.calendarId }
        : {}),
    })
    .split(":")
    .join("");
}

But still want to understand the error meaning and why it's hapenning

The error is because the date's calendar doesn't match the locale's calendar. The thinking goes, if you have a non-ISO8601 calendar (a 'human' calendar) then that's probably intentional and you want it formatted in that calendar. At the same time, locales have a human calendar associated with them as well, either explicitly or as a default, and it's surprising if you are formatting dates in that locale and they end up in a completely different calendar.

So since it's not clear which calendar should 'win' in that case, we throw, so that the programmer knows they have to make an explicit choice.

More precisely:

  • The date's calendar must match the locale's calendar; or
  • The date must be in the ISO 8601 calendar, in which case the locale's calendar wins.

So if you want the locale's calendar to always win, do this:

date.withCalendar("iso8601").toLocaleString(...);

If you want the date's calendar to always win, then the workaround you posted above is the correct one.

More info: tc39/proposal-temporal#262 (comment)

Thank you for the explanation. it's clear to me.