Closed thojanssens closed 1 year ago
Secondly, the ISO8601 standard provides a standardized format for representing dates and times in a machine-readable manner.
It is not a calendar like the Gregorian calendar though.
So this leaves me confused as to what the error actually is.
The problem you're running into is that locales have a calendar included , either implicitly (every locale like en-GB
or ar-SA
has a default calendar) or explicitly in the locale identifier like en-GB-u-ca-japanese
. These locales can be overridden using the calendar
option of toLocaleString
or the same option of the Intl.DateTimeFormat
constructor. But there's always a calendar (separate from the calendar of the Temporal object) present in the arguments of those functions. Even when toLocaleString is called with no arguments, the default locale is used, which in turn has a default calendar.
So when you call toLocaleString
or Intl.DateTimeFormat.prototype.format
, there are two calendars that could be used: the calendar in the Temporal object, or the calendar passed in the arguments to the formatter object constructor or formatting function. If the two calendars are the same (like your example above of calendar: currentYearMonth.calendar
) then there's no problem and the call succeeds.
But if the two calendars are different, then Temporal needs to figure out which calendar to use, or it needs to throw an exception if it can't figure out which one to use.
The next logical question to ask is why must you supply your own calendar
option for some Temporal types like Temporal.PlainYearMonth
, but not for others like Temporal.PlainDate
? This is a good question, but answering it requires some background info below about calendars in Temporal.
Intentional use of calendars is very, very rare in real-world software. Almost all Temporal objects will be created with the default iso8601
calendar), which isn't used by default in any real-world locales.
This means that when a Temporal object has the iso8601
calendar, we assume that the developer doesn't have a preference of which calendar should be used when formatting that object. In other words, for the purpose of formatting we treat iso8601
as an undefined calendar that is always overridden by the formatter arguments. This removes the conflict between the object's calendar and the calendar in the formatting arguments.
In pseudocode, here's how this works for a type like Temporal.PlainDate
:
getCalendarIdentifierFromArgs(locales, options) {
if (options.calendar) return options.calendar.toString();
const calendarInLocale = parse calendar ID from `locales`;
if (calendarInLocale) return calendarInLocale;
const defaultCalendarForLocale = get default calendar ID for `locales`;
return defaultCalendarForLocale;
}
class PlainDate {
. . .
toLocaleString(locales, options) {
const calendarFromArgs = getCalendarIdentifierFromArgs(locales, options);
if (this.calendar.id !== `iso8601` && this.calendar.id !== calendarFromArgs) {
throw new RangeError('Object calendar doesn't match locale calendar');
}
// Extract only the options relevant to Temporal.PlainDate. Omit others.
const { calendar, dateStyle, era, year, month, day, weekday, numberingSystem, localeMatcher, formatMatcher } = options;
const revisedOptions = { calendar, dateStyle, era, year, month, day, weekday, numberingSystem, localeMatcher, formatMatcher };
const formatter = new Intl.DateTimeFormat(locales, revisedOptions);
// Create a `Date` object using the equivalent date in the ISO 8601 calendar
const isoDate = this.getISOFields();
const date = new Date(isoDate.isoYear, this.isoMonth-1, this.isoDay);
// Finally, format the Date
return formatter.format(date);
}
}
This works for all Temporal types that store a full date: Temporal.PlainDate
, Temporal.PlainDateTime
, and Temporal.ZonedDateTime
, because full dates can be converted between calendars.
However, this trick doesn't work for Temporal types that *don't* store full dates: Temporal.PlainMonthDay
and Temporal.PlainYearMonth
. With those types, there's not enough information to convert between calendars. For example, assume my wedding anniversary birthday is June 1 in the Gregorian calendar. What is my birthday in the Japanese calendar? The answer is that it's impossible to determine without knowing the full date (including the year) of my wedding in the Japanese calendar. But Temporal.PlainMonthDay
and Temporal.PlainYearMonth
don't have the full date.
So this is why Temporal.PlainMonthDay
and Temporal.PlainYearMonth
require more verbose formatting code: because Temporal can't automatically convert between the object's calendar and the calendar of the locale. If, however, you manually set the calendar
option, then no conversion is needed between the object's calendar and the locale's calendar, and the call succeeds.
Apologies for the long explanation, I hope this makes sense.
According to the error message I got, I have
ISO 8601 is a standard for representing date and time formats specifically in the context of the Gregorian calendar.
Therefore I do not understand why it's not possible to convert the iso8601 year-month data into a year-month as the Gregory calendar. I understand that data is missing (the day), but the iso8601 is a representation in Gregorian.
There are two possible ways we could determine if calendars are equal, to determine whether to throw or not for any particular formatting operation where calendars must be equal:
iso8601
and gregory
would pass this test, as would chinese
/dangi
, islamiccc
/islamic-civil
, and ethiopic
/ethioaa
would all pass too.id
?The champions of this proposal chose (2) because it was simpler, had slightly better performance, and because it would avoid special-casing for gregory
in userland code.
@sffc may be able to add more context around this decision.
Related: https://github.com/tc39/proposal-temporal/issues/2521
I would be in favor of automatically setting the calendar
field in toLocaleString methods on relevant types, but this can be an improvement for a small follow-on proposal.
Ty for your answers. I think my points were clear but in other words:
the better pragmatic approach to allow an "iso8601 calendar" (which is not even a calendar) converting into Gregorian offers better UX
expect a lot of people questioning this option parameter, as I did
I don't see the advantage to be so explicit about it for such cases, as most apps are not so sophisticated to work with multiple calendars, and we can simply still throw if the conversion can't be automatic
I agree, this is one of the more confusing parts of the proposal.
About this specifically:
we can simply still throw if the conversion can't be automatic
That brings its own disadvantages: it seems likely that it would lead to apps working in development, and crashing in production when executed in locales that use a calendar that can't be automatically converted to and from ISO.
I think it deserves a thread in proposal-temporal-v2, let's continue designing the API there: https://github.com/js-temporal/proposal-temporal-v2/issues/29
When calling
toLocaleString
, if thecalendar
option is absent, the following error occurs:As
toLocaleString
is called on the temporal object (currentYearMonth
in this example), I do not understand why we have to specify again that the calendar is the one in that same object (currentYearMonth
).It makes
toLocaleString
calls excessively verbose.