In the Calendar explainer, soon to be checked in as https://github.com/tc39/proposal-temporal/blob/main/docs/calendar-draft.md, I proposed a new option idToCalendar to map from strings to calendar objects. By default, there would be Temporal.Calendar.idToCalendar, which would resolve built-in calendar types, but the user could also provide their own mapping.
In my proposal, the user-provided idToCalendar would be called first, and if no match was found (a nullish return value), then the built-in idToCalendar would be called. @gibson042 suggested that in such a case, we should not fall back to the built-in idToCalendar, behavior described as an "implicit escape hatch".
It seems like user-provided idToCalendar functions should be able to specify whether they want to defer to the built-in idToCalendar or not. It seems reasonable that they could specify this using e.g. return null; to not defer, or return Temporal.Calendar.idToCalendar(id); to defer manually.
The main use case is that we want IDs for built-in calendars to work. We also want to make it possible for custom calendar IDs to work.
I don't like the idea of idToCalendar being an argument to the from function, I don't know anyone who does. It's clunky and confusing.
It would be easier to understand if we had a singleton object containing all the known calendars, such as:
Temporal.calendars = {
// One entry by default:
iso,
// 402 adds more entries:
hebrew,
gregory,
japanese,
// ...
};
// Example Temporal.Date.from:
Temporal.Date.from = function(fields) {
let calendar;
if (fields.calendar?.calendarId) {
// fields.calendar is a calendar object and not a string
calendar = fields.calendar;
} else {
// else, treat fields.calendar as a key into the global lookup table
calendar = Temporal.calendars[fields.calendar] ?? DEFAULT;
}
return calendar.dateFromFields(fields);
}
If someone wanted to add a custom calendar to the ID lookup table, they could add it globally to Temporal.calendars. If the idea of modifying global state is too acrid, they can extend Temporal.Date and make the .from method search a different object. Alternatively, they can always pass their custom calendar in as an object, rather than a string, and avoid the registry altogether.
Let's please not make intended functionality be dependent on mutating global state.
Meeting, Apr 16: We'll continue discussing this, keeping in mind that custom time zones would also need a similar mechanism (see #498). Current best proposal is to monkeypatch Temporal.{Calendar,TimeZone}.from() if you want your calendar/time zone to be able to be deserialized from a string, and otherwise use objects wherever possible.
I started to revise #498 to monkeypatch Temporal.TimeZone.from(), but I realized that the monkeypatcher has to get the handling of objects just right, and when to convert the argument to a string, in order to match the original Temporal.TimeZone.from(). For that reason I think I prefer a separate static method such as Temporal.TimeZone.idToTimeZone() that only takes a string and only returns an object or null.
How about Temporal.TimeZone.fromId()?
I guess I'm not quite convinced that it's necessary to separate out fromId from from. Would it be so error-prone to check whether it's a particular string, and otherwise defer to from? Or, on the other hand, if we do include fromId, I wonder if we should be calling it something like fromIANA, and reconsidering whether Dates should include fromISO methods in addition to from.
Meeting, 7 May: Consensus is to use Temporal.TimeZone.from() and Temporal.Calendar.from(). From the perspective of SES it seems like it would be the same as fromId(). In the absence of any overriding consideration from monkeypatching customers, we will do whatever fits best with the existing decisions that have been made, which is from().
I'd be happy to take an action to update all the existing places where this is mentioned.
Sorry, one more idea here.
In @ptomato's pull request, the logic required to properly resolve a string into a time zone or calendar identifier is more than I was expecting, and is likely prone to error.
If we want to "hide away" this feature, we could put it in a symbol method. This is already like the Symbol.iterator method that you need to override in order to add custom calendar objects. For example: Temporal.Calendar[Symbol.fromId]. Temporal.Calendar.from should delegate to that method when resolving a calendar ID. With this method, if you're implementing a custom calendar, you just need to override those two symbol methods.
The advantage is that this cleanly separates the data override from any other business logic.
It brings the list of OS-dependent interfaces to five:
Thoughts?
@sffc I see that that is a coherent option, but I'd prefer that we keep things as simple as possible, unless we can find someone who actually asks for these features. My preference is, unless someone demonstrates a use case, to remove the [Symbol.iterator] methods, and to not include the [Symbol.fromId] method either.
I agree with Shane that the logic that has to be reimplemented in an overridden from() is cumbersome and easy to get wrong. In the absence of other considerations, I'd much prefer a fromId() or [Symbol.fromId()] method.
On the other hand, in one of the meetings, someone mentioned that it's expected to be rare that a user would want to make a custom time zone or calendar available globally, so it should only rarely be necessary, and we can show an example of doing it in the cookbook. That's what changed my mind.
Most helpful comment
How about
Temporal.TimeZone.fromId()?