Open devongovett opened 3 years ago
@sffc @manishearth - what's the latest status (if any) of an enumeration API for calendar eras?
@devongovett - https://github.com/tc39/ecma402/issues/541 is the issue currently tracking which Japanese eras will be supported in Temporal and what their identifiers will be. You should probably watch that issue. Keep in mind that the decision about which eras to support is out of scope of the Temporal spec. This doesn't matter from an end-user standpoint, but it matters somewhat for who in the TC39 world is actually making era-related decisions: the 402 working group, not the Temporal champions.
BTW, @manishearth wrote a document that goes into a lot of detail about the problem of creating era identifiers for Japanese. Manish, are there any updates from what's in that doc that you'd want to share?
As for why we don't use numeric identifiers for eras, the main reason was readability. But also, for Japanese, new historical eras are being discovered from time to time, so any numbering scheme would have to either be sparse (to allow new ones to be injected) or would have to be out-of-order. Neither of those is a great experience, so using alphanumeric strings was considered better because consecutive, sequential-across-time identifiers wouldn't be expected. @sffc or @manishearth can correct or expand on this if I got it wrong.
BTW, if you want to find years of eras, if you have an enumeration then it's trivial to create a Temporal.PlainDate for the first day year of the era:
Temporal.PlainDate.from({era: 'meiji', eraYear: 1, month: 1, day: 1, calendar: 'japanese'});
Note that Japanese eras that start mid-year are interesting. The way Temporal handles this (at least in the polyfill currently) is that era
/eraYear
are used to calculate the year
, and then that year
value is used. So that's why the era below is returned as ce
(which is a placeholder in the polyfilll until https://github.com/tc39/ecma402/issues/541 is resolved) instead of 'meiji'
because that era started midyear.
It's an interesting question of whether an era enumeration API should include the specific dates where the era starts and ends. This seems like a reasonable requirement to add to whatever TC39 proposal targets that enumeration use case.
Thanks, that's useful information. I didn't realize that new historical eras are still being discovered. In that case, string identifiers with a way to enumerate the valid values definitely seems like a better solution.
BTW, @Manishearth wrote a document that goes into a lot of detail about the problem of creating era identifiers for Japanese.
It seems I don't have access to this.
Note that Japanese eras that start mid-year are interesting.
Indeed. Determining the number of years in an era isn't so simple with this in mind. For the date picker use case, the maximum allowed value for the year field should change depending on both the era as well as the month and day fields. For example, in the heisei
era, the maximum value should be 31 for dates before May 1st and 30 for dates after May 1st.
It's an interesting question of whether an era enumeration API should include the specific dates where the era starts and ends.
For my use case, either a specific method to get the number of years in an era, or providing the exact dates and letting me compute that myself would work.
BTW, @Manishearth wrote a document that goes into a lot of detail about the problem of creating era identifiers for Japanese. Manish, are there any updates from what's in that doc that you'd want to share?
No updates, the doc is as far as we've gotten.
It seems I don't have access to this.
Feel free to request access; unfortunately my employer's settings don't let me link-share docs.
@devongovett - I'm going to transfer this issue over to the https://github.com/tc39/ecma402/ repo which would be the place where the actual decision would live about how to handle this enumeration use case.
Here's a starting point suggestion for requirements of this API, although as the user of it I'd defer to you to edit/change these suggestions to match your use case.
Use Case Summary
Proposed API Requirements
402 experts - The list below is a rough initial idea. Feel free to replace with a better solution!
'fr-FR-u-ca-buddhist'
) and an options bag which may also contain a calendar
property which may be a calendar ID or a Temporal.Calendar instance. calendar
option is required. (Or should it default to the user's current locale and calendar?){
era: string,
displayName: string,
startDate: Temporal.PlainDate | null, // or ISO string instead?
endDate: Temporal.PlainDate | null, // or ISO string instead?
}
'bc'
and 'ad
' in the 'gregorian'
calendar), the unbounded date should be null
.'iso8601'
with no eras should return an empty array/iterable (or should it return null
or undefined
?)getISOCalendarFields()
or withCalendar
on those objects.DateTimeFormat.p.formatToParts
can supply the localized names) but performance of that operation will be abysmal for the Japanese calendar's hundreds of eras, so it probably makes sense to include the localized name too.Oops, I don't have permission to transfer this issue to the 402 repo. @sffc is this something you can do?
@justingrant thanks for your detailed thoughts! Overall it seems like your proposal would cover my use cases.
Just confirming: do you think this would be better as a top-level Intl
method rather than a method on a Temporal.Calendar
instance? I had originally thought it would be something like: new Temporal.Calendar('japanese').getEras()
. This way you could also easily get the eras from an existing date object like date.calendar.getEras()
.
Similarly, calendar.getYearsInEra(date)
would also be a bit simpler, and would match with the existing methods to get limits for fields from a calendar. Getting the actual start/end date of the era would probably also be useful though.
This seems like a good fit for a future addition to the Intl.Enumeration proposal?
This seems like a good fit for a future addition to the Intl.Enumeration proposal?
What's the difference between Intl.Enumeration and Intl.DisplayNames? When would a use case go into one vs. the other?
Just confirming: do you think this would be better as a top-level
Intl
method rather than a method on aTemporal.Calendar
instance? I had originally thought it would be something like:new Temporal.Calendar('japanese').getEras()
. This way you could also easily get the eras from an existing date object likedate.calendar.getEras()
.
@devongovett I don't have an opinion about the right long-term home of accessing era-related metadata, but I suspect that @sffc and/or @ptomato may have an opinion.
Similarly,
calendar.getYearsInEra(date)
would also be a bit simpler, and would match with the existing methods to get limits for fields from a calendar. Getting the actual start/end date of the era would probably also be useful though.
I would not support an API that returned a scalar number of years in an era, because it would seem to complicate use cases where eras start or end mid-year (which is most eras, AFAIK). If the era starts in the 8th month of one year and ends in the 3rd month of another year, should it include or exclude the partial years on either end?
Instead, if we do offer an API to fetch metadata about a specific era, then IMHO it should return two PlainDate instances that represent the first and last day of the era. Then the caller can pull out whatever info is needed.
Ideally, the result of a "get metadata for one era" API would have the same shape as the elements returned by the proposed enumeration API, so callers could either ask for metadata about one era or could enumerate through all of them, but could use the same code to process one era's metadata.
Another alternative is not to offer this kind of single-era API at all, and instead require callers to filter the results of the proposed enumeration API. Given that era metadata is somewhat of a niche use case, this may be an OK workaround.
BTW, instead of thinking of this as an enumeration API, another alternative could be to think of it as a "Get Calendar Info" API which would return additional metadata about the calendar beyond eras, e.g.:
I'm not saying this is better or worse than a plain enumeration API, but it may be something to consider.
I like the idea of using Enumeration to list calendar IDss, and each calendar object describing itself, rather than trying to make an Enumeration API for each property of a calendar.
What's the difference between Intl.Enumeration and Intl.DisplayNames? When would a use case go into one vs. the other?
Enumeration is for IDs, DisplayNames is for localized, human-readable names.
+1 to making this a method on Calendar
instances.
Enumeration is for IDs
Ahh, got it. I had assumed that there was a was a method on the DisplayNames V2 prototype to get all the localized text values in a single call, but I was mistaken.
BTW, given that showing a UI picker is the canonical use case for enumeration, and given that UI pickers require localized text for each option, I opened https://github.com/tc39/proposal-intl-enumeration/issues/36 to understand the reasoning for not also offering a more ergonomic way to fetch localized text for all IDs in one method call.
2022-02-10 discussion: https://github.com/tc39/ecma402/blob/master/meetings/notes-2022-02-10.md#accessing-the-sequence-of-eras-of-a-calendar-598
Conclusion: Revisit this issue as part of a bigger proposal about calendar display names or datetime picker components.
The Temporal polyfill had to build an internal "enumerate all eras for a calendar" API. Based on that experience, below is a suggestion for the metadata required for each era.
My assumption is that an enumeration API would return an array that's ordered chronologically. (Not sure why we'd need an iterator instead of an array here.) FWIW, the most recent era is always used more than older eras, so having the list be sorted in reverse chronological order may make it easier to work with because the current era would always be [0]
, but the reverse order would be OK too.
interface Era {
/**
* Non-localized string ID of the era, e.g. "reiwa" or "bce". Only intended for programmer use,
* not for display to end users. Valid characters are: a-z (lowercase only), 0-9, and `-`.
* */
id: string;
/**
* Earliest day of this era. If undefined, this is the oldest era and has no lower bound (e.g. BC).
* It should be in the era's calendar. If the user needs the ISO date, it's easy to convert using `withCalendar`.
* */
startDate?: Temporal.PlainDate;
/**
* Latest day of this era, inclusive. If undefined, this era is the most recent era and has no upper bound.
* It should be in the era's calendar. If the user needs the ISO date, it's easy to convert using `withCalendar`.
* */
endDate?: Temporal.PlainDate;
/**
* If true, this era counts years backwards like BC. There can be at most one era with
* `reverseYears===true` for each calendar, and if present it must be the oldest era.
* */
reverseYears: boolean;
}
BTW, there are other metadata that the polyfill uses internally that probably are not needed in a public API because they can be easily derived from the metadata above:
buddhist
calendar is the only ICU calendar that has a year 0)const hasYearZero = era => (era.reverseYears ? era.endDate : era.startDate).year === 0
year
and eraYear
?"const getAnchorYear = eras => eras.find(e => e.startDate.year === e.startDate.eraYear)?.year;
New non-array.prototype APIs are generally expected to return an iterator, not an array - that’s why matchAll does.
New non-array.prototype APIs are generally expected to return an iterator, not an array - that’s why matchAll does.
It makes sense that matchAll
uses an iterator because strings can be huge and I assume there are optimizations possible by streaming the results and avoiding keeping a potentially-huge array in RAM. Ditto for any API that returns a large/unbounded set, or APIs that pull data from async or streaming sources.
But could you remind me why arrays are discouraged for cases where the number of results is known to be small and the enumerated data is built-in and immutable? In all ICU calendars except Japanese, the maximum numbers of eras is three. (In Japanese, it will be a few hundred elements, which is also small.) And the underlying era data will almost certainly be a native array before it's wrapped in an iterator to send back to the client.
What's the benefit of requiring implementers and callers to write extra code to deal with an intermediate iterator?
BTW, here's a few era-list use cases that I know about:
transform an era list into a data structure needed to populate a dropdown list with an ID/localized-name pair for each era
const eraNames = new Intl.DisplayNames(['en'], { type: 'era', calendar: 'japanese' });
const dropdownData = eras.map(e => ({ value: e.id, label: eraNames.of(e) }));
find an era with a particular ID
meiji = eras.find(e => e.id === 'meiji');
find the most recent era
eras[0];
eras.filter(e => Math.round(e.startDate.withCalendar('iso8601').year / 100) === 15);
For each of these cases, an iterator version of eras
could be replaced with [...eras]
or Array.from
to get back to the more-ergonomic array form. But why is this better?
That's a fair argument; just be prepared for the pushback.
At this point I'm mostly just curious: what's the advantage of iterators for cases where the underlying data is synchronous and small? If there's a significant advantage then the extra hassle of [...eras]
would be OK, but for now I'm just trying to understand what that advantage is.
I'm not arguing that there is one; i agree with you.
Do you know what people who would argue for iterators would say? I'm sure there must be a good reason, just trying to understand what it is.
Also, unrelated to iterator vs. array, another thing to think about is whether era iteration would be supported for custom calendars. My assumption would be "no" because we don't support localization for custom calendars nor timezones today. Enumerating eras of a custom calendar may have limited value without the ability to also localize the names of those eras, which would be another new API.
Do you know what people who would argue for iterators would say? I'm sure there must be a good reason, just trying to understand what it is.
I asked delegates about this. Conclusion:
TL;DR - returning an Array
of eras would be correct because no calendar other than Japanese has >3 eras, and even in Japanese the total number will be <500.
I'm curious about the decision to use string identifiers for the
era
field. There are some cases where it might be useful to be able to manipulate the era field similarly to other numeric fields. For example, a date picker UI control might want to allow the user to change the era using the arrow keys. This example is from the macOS native date picker control on the Japanese calendar.https://user-images.githubusercontent.com/19409/126418252-95f4c73a-6123-4b43-accf-3ab9c1f9c9d6.mov
In ICU and Joda Time (I believe), the era field is a number like other fields, which means it can be incremented and decremented. But this is not possible with Temporal because the era field is a string, and not included in Duration.
I do like that eras are more user-friendly by using identifiers though (an enum would also work). Perhaps an alternative would be for calendars to have a method to retrieve the list of (or at least the previous and next) available era identifiers. This way, UI code could find the current era identifier, and determine the next/previous one from there.
I also think a way to retrieve the number of years in an era would be useful. For example, a date picker UI might wish to make the year field wrap back to the first year when the end of an era is met (so that editing one field does not affect another). This could be a method on calendar similar to the other methods for retrieving limits.