Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

size-suggestion: Remove Temporal.TimeZone class and protocol #2853

Closed
justingrant opened this issue May 21, 2024 · 20 comments
Closed

size-suggestion: Remove Temporal.TimeZone class and protocol #2853

justingrant opened this issue May 21, 2024 · 20 comments
Assignees
Labels
size-suggestion Suggestions to shrink size of Temporal to address implementer concerns

Comments

@justingrant
Copy link
Collaborator

justingrant commented May 21, 2024

This issue was split from #2826. This issue is solely focused on removing TimeZone. Please keep discussions on-topic.

Conclusion in the champions meeting of 2024-04-18. We've been asked to reduce the size and complexity of the proposal. The callable hooks in the time zone and calendar protocols are the part that implementations have been most uncomfortable with. They also seem to be where we get the biggest reduction in complexity for the least loss of use cases. The TimeZone and Calendar classes themselves make less sense to keep if the protocols are gone. So our conclusion is to remove them. This issue is focused on time zones only. A separate issue discusses calendars.

Motivations for removing custom time zones

  • Custom time zones add a lot of size and complexity to Temporal for relatively little gain.
  • Requiring observable calls in a particular sequence makes it difficult for implementations to optimize, unless they maintain separate code paths for built-in vs. user-supplied calendars/time zones.
  • By removing custom time zones from Temporal V1, we could enable a declarative (no user code at all!), standards-based solution in a follow-up proposal. For example, using JSCalendar's TimeZone data format from RFC 8984.
  • The current custom time zone functionality made more sense back when you could monkeypatch TimeZone.from() to "register" a custom ID. That ability was removed when the proposal went to stage 3 because the committee will not agree to global state.
  • Without a time zone protocol, the usefulness of a Temporal.TimeZone object is heavily reduced. It could be removed and replaced with as little as one new method on the ZonedDateTime type.

Use cases that would be deferred to a later proposal, and their replacements in the meantime

Work around incomplete/outdated support in TZDB

For example:

  • Make sure program runs against known version of time zone database regardless of whether environment is ahead or behind

Replacement: Previously, you could implement a custom calendar or time zone by creating an object that implemented the protocol, and monkeypatching or wrapping some of Temporal so that it would deserialize dates with your custom calendar/time zone's ID into a Temporal object with the correct protocol object. You would now have to monkeypatch or wrap more of Temporal.

Converting directly between Instant and PlainDateTime

Replacement: Go through ZonedDateTime, instead of through TimeZone. e.g.

// Before
timeZone.getPlainDateTimeFor(instant)
timeZone.getInstantFor(plainDateTime, { disambiguation })

// After
Temporal.Instant.from(instant).toZonedDateTimeISO(timeZoneID).toPlainDateTime()
Temporal.PlainDateTime.from(plainDateTime).toZonedDateTime(timeZoneID, { disambiguation }).toInstant()

Determine if two time zone IDs are aliases

Replacement: Use ZonedDateTime.p.equals() with the same instant. (This is more inconvenient, but it's a pretty uncommon operation. If this really needs to be more ergonomic, a dedicated static method can be added in a follow up.)

// Before
Temporal.TimeZone.from('Asia/Kolkata').equals(Temporal.TimeZone.from('Asia/Calcutta'))

// After
zonedDateTime.withTimeZone('Asia/Kolkata').equals(zonedDateTime.withTimeZone('Asia/Calcutta'))

Look up previous/next UTC offset transition in a time zone

Replacement: We'll add 1-2 methods to ZonedDateTime that replace the two methods on TimeZone.

// Before
timeZone.getNextTransition(instant)
timeZone.getPreviousTransition(instant)

// After
zdt.getTimeZoneTransition('next');
zdt.getTimeZoneTransition('previous');
     // OR (if we don't want to save a method)
zdt.getNextTimeZoneTransition();
zdt.getPreviousTimeZoneTransition();

Custom disambiguation behaviour

A niche use case is implementing PlainDateTime-to-Instant conversions with disambiguation behaviours other than the built-in earlier, later, compatible modes. By request, we have a cookbook recipe for this.
Replacement: You can still do it with two conversions, for example:

// Before
arrayOfInstants = timeZone.getPossibleInstantsFor(plainDateTime)

// After
arrayOfInstants = [
  plainDateTime.toZonedDateTime(timeZoneID, { disambiguation: 'earlier' }).toInstant(),
  plainDateTime.toZonedDateTime(timeZoneID, { disambiguation: 'later' }).toInstant(),
].reduce((a, b) => a.equals(b) ? [a] : [a, b])

Scope of issue

  • Remove Temporal.TimeZone
  • User-defined methods are not looked up nor called during calculations
  • Time-zone-taking APIs no longer accept objects, only string IDs, and date/date-time strings from which an ID is determined
  • [[TimeZone]] internal slot of ZonedDateTime only stores string ID
  • Remove getTimeZone() method from ZonedDateTime prototype
  • timeZone property of object returned by ZonedDateTime's getISOFields() methods can only be a string
  • Add ZonedDateTime.p.getTimeZoneTransition('next' | 'previous') (or a pair of methods instead)
  • Update TypeScript types to match the changes above.
  • Create proof of concept for how you would polyfill a time zone going forward
@justingrant justingrant added the size-suggestion Suggestions to shrink size of Temporal to address implementer concerns label May 21, 2024
@justingrant justingrant changed the title size-suggestion: Remove TimeZone type and protocol size-suggestion: Remove Temporal.TimeZone type and protocol May 21, 2024
@justingrant justingrant changed the title size-suggestion: Remove Temporal.TimeZone type and protocol size-suggestion: Remove Temporal.TimeZone class and protocol May 21, 2024
@anba
Copy link
Contributor

anba commented May 21, 2024

  • Make sure program runs against known version of time zone database regardless of whether environment is ahead or behind

I wonder if this is a good example, because custom time zones (or custom calendars) don't work well with Intl.DateTimeFormat. (They either don't work at all or give different results because the used tzdb rules differ.)

@ptomato
Copy link
Collaborator

ptomato commented May 23, 2024

I wanted to capture on this thread one thing that custom time zones allow, which I forgot about: time zones with constant sub-minute offsets, like UTC+00:00:30.

Note that there are still IANA time zones that used sub-minute UTC offsets after 1970, such as Africa/Monrovia. These are unaffected and continue to work.

Custom time zones with constant sub-minute offsets were used in a couple places in test262 tests, mainly to test the same logic that would be used for zones like Africa/Monrovia, without requiring an IANA DB.

I doubt they are much used in real life. If they were really needed, we could consider going back to sub-minute offset strings: e.g., new Temporal.TimeZone(0n, "-00:00:30").

@justingrant
Copy link
Collaborator Author

Meeting 2024-05-23: At the upcoming June 2024 TC39 plenary meeting, we'll propose removing the Temporal.TimeZone class and protocol, for the following reasons:

  • To simplify the Temporal API, both for end users and for implementers.
  • To address size concerns by shaving net 13 functions, one class, and a lot of code to implement custom time zones.
  • We believe that a better custom-timezone solution can be designed in a follow-up proposal that defines a custom time zone declaratively and thus requires no user code at all, and therefore no TimeZone type nor methods. One promising format is a standards-based using the custom time zone JSON format from JSCalendar (RFC 8984).

To save one more function, the TimeZone.p.getNextTransition/getPreviousTransition methods are proposed to be merged into a single prototype method on Temporal.ZonedDateTime. Follow #2850 for details about that merger.

@ljharb
Copy link
Member

ljharb commented May 24, 2024

So, with the issue's proposed changes, how do i identify the timezone of all the kinds of Temporal instances? How would I transform between a timezone ID, a UTC offset, the IANA name vs alternative names (in particular, the name Date uses), etc?

@justingrant
Copy link
Collaborator Author

how do i identify the timezone of all the kinds of Temporal instances?

This change would remove the ability for custom userland time zones, but it would make no changes to how built-in time zones (either named or offset) are handled in Temporal, in Intl, or by Date.

Before and after the change in this issue, if you want to know the timezone of a Temporal.ZonedDateTime object, you'd use its timeZoneId getter. If you want to know the time zone offset of a Temporal.ZonedDateTime object, you'd continue to use the offset getter. Temporal.Now.timeZoneId() is also unchanged by this issue.

How would I transform between a timezone ID, a UTC offset, the IANA name

Today there's no "transformation" between time zones. An offset time zone has an ID that looks like an offset, and a named time zone has an ID that's a name, e.g. "UTC" or "Europe/Paris". And Temporal.ZonedDateTime and Intl.DateTimeFormat (the only objects in ECMAScript that hold a time zone) are immutable, so the only way to transform anything would be to create a new object.

vs alternative names (in particular, the name Date uses), etc?

By "the name Date uses" do you mean SystemTimeZoneIdentifier? If so, this change does not affect that AO.

If the responses above don't address your concern, could you clarify what you're wondering about?

@ljharb
Copy link
Member

ljharb commented May 24, 2024

What I mean is, with a TimeZone object, there's an obvious single place to add future convenience methods to provide mappings to a given built-in timezone. Without one, it'd require adding a getter to each Temporal type, for each convenience method. Given size concerns, that seems like making this reduction now would effectively prevent any enhancement in the future.

@anba
Copy link
Contributor

anba commented May 24, 2024

If there's ever a need to have extensive methods around time zones, we can revisit adding Temporal.TimeZone.

@ljharb
Copy link
Member

ljharb commented May 24, 2024

It's not clear to me how that would ever be possible - if "size" considerations block it now, they'd block it forever, and without the objects existing initially, how could it be possible to add in a nonbreaking way?

@justingrant
Copy link
Collaborator Author

To clarify: there's only one Temporal type with a time zone: Temporal.ZonedDateTime. So any future timezone-related methods could be put on that class, like we're planning to do with the prev/next transition APIs.

And in 5+ years of working on Temporal we haven't found a justification for another Temporal type with a time zone. (In theory one could create a time+timezone or a date+timezone type, but the delta of value for those have never come close to justifying adding those types to the language.)

So personally I'm not too worried about future compatibility here, because if something is judged to be important enough to be on TimeZone then it would likely be judged important enough to add to ZonedDateTime, or to add a TimeZone type again.

@anba
Copy link
Contributor

anba commented May 24, 2024

IIUC the size limitations are because of current memory limits for certain devices. Smaller devices will likely gain more memory in the future, so adding a few kilobytes here and there won't matter that much anymore.

@ptomato
Copy link
Collaborator

ptomato commented May 27, 2024

@ljharb I'd like to understand your use cases better, I'm not sure what you mean by mappings. Here's what I would guess might help you:

To get the time zone ID of a Temporal.ZonedDateTime instance is zdt.timeZoneId.

There isn't a one-to-one mapping between time zone ID and UTC offset. To map to a UTC offset you need a pair of time zone ID and exact time. You can get the UTC offset from zdt.offsetNanoseconds (docs link), or zdt.offset (docs link) if you want it in human-readable form.

Mapping IANA names to alternative time zone names has never been supported by Temporal even with Temporal.TimeZone. I don't believe Date uses alternative names because it doesn't carry a time zone - but maybe you are talking about the localized time zone name in the output you get from Date.prototype.toLocaleString()? You'll still get that same name from zdt.toLocaleString().

@ljharb
Copy link
Member

ljharb commented May 28, 2024

ok, so, if i have a time zone ID, and nothing else, how do i get to that localized time zone name? If i have that localized time zone name from a Date object (and whatever else that Date object needs to provide), how can i get a time zone ID?

@justingrant
Copy link
Collaborator Author

ok, so, if i have a time zone ID, and nothing else, how do i get to that localized time zone name?

The problem you'll run into is that the localized name (at least in some locales and some time zones) depends on the date chosen, e.g. "Eastern Standard Time" vs. "Eastern Daylight Time". I believe that this is why the DisplayNames V1 proposal didn't include time zone names, but you'd have to ask the champions of that proposal to know for sure.

I believe (not 100% sure) that the timeZonename: 'longGeneric' in Intl.DateTimeFormat provides the date-neutral format. @sffc do you know if this is true? Also do you know if there is a plan to extend Intl.DisplayNames with time zone names?

In the meantime, here's how to get (what I think is?) the date-neutral time zone name:

function getLocalizedTimeZoneName(locale, timeZone) {
  const formatter = new Intl.DateTimeFormat(locale, { timeZoneName: 'longGeneric', timeZone });
  const parts = formatter.formatToParts(new Date());
  const timeZoneName = parts.find(part => part.type === 'timeZoneName');
  return timeZoneName?.value;
}
getLocalizedTimeZoneName('en', 'America/Denver');
// => 'Mountain Time'
getLocalizedTimeZoneName('en', 'Asia/Tokyo');
// => 'Japan Standard Time'

If i have that localized time zone name from a Date object (and whatever else that Date object needs to provide), how can i get a time zone ID?

Well, Date objects don't store a time zone, so the "time zone name from a Date object" is really just the system time zone. You can get that from Temporal.Now.timeZoneId() or, pre-Temporal, from new Intl.DateTimeFormat().resolvedOptions().timeZone.

If what you're asking is how to take a localized date string that's been output on some computer and to parse that string into a time zone ID on some other computer, well I don't think that this is (or should be!) a supported use case. Localization is a one-way street.

@ljharb
Copy link
Member

ljharb commented May 28, 2024

In general, I'm hoping to be able to treat a timezone as a first-class, reified thing, that i can extract from a Date or a relevant Temporal type, associate with an unzoned Temporal type to produce a zoned one, etc. The class and protocol seemed like the proper way to do that, and forcing all of that to be stringly typed doesn't seem like an improvement to me.

@sffc
Copy link
Collaborator

sffc commented May 28, 2024

The only correct, unambiguous localized representation of a time zone in CLDR is the exemplar city format, such as "New York Time".

This solves ambiguities such as "Phoenix Time" and "Denver Time" being different, despite both being called "Mountain Time".

We don't have that format currently exposed in Intl because it requires a lot of data (in the worst case, translations of 600 entities in each locale) and it is less popular when formatting a zoned datetime. However, it is the format that should back a DisplayNames impl if someone wanted to make such a proposal.

@justingrant
Copy link
Collaborator Author

first-class, reified thing, that i can extract from a Date

Well, you can never extract a time zone from a Date because a Date doesn't have a time zone.

The class and protocol seemed like the proper way to do that

I don't think anyone is saying that removing 1/3 of Temporal surface area (from this issue plus all the others) is necessarily an improvement across the board (although some folks may suggest that some of the removals make things better). But our goal at this point is to make Temporal shippable by implementers who are concerned that Temporal as-is is too big and too complex. Cutting custom time zones provides >15% of the savings, which is considerable. And I'd much, much rather see us cut custom time zones instead of 15+ other functions in Temporal that have much more value for users.

Especially given that we now understand that custom time zones can be implemented declaratively with no user code, and based on a standardized JSON format like JSCalendar. It seems reasonable IMO to drop the class and protocol from V1 and implement a better, declarative, standards-based custom time zone in a follow-up.

It would be good to discuss as part of that follow-up proposal whether a dedicated TimeZone class makes sense to bring back.

forcing all of that to be stringly typed doesn't seem like an improvement to me.

Well, implementers demanded that we switch from objects to strings for performance reasons, so you may need to convince V8 and SpiderMonkey and JSC more than you need to convince me! 😄

@ljharb
Copy link
Member

ljharb commented May 28, 2024

@justingrant i can extract the local timezone from Date with (new Date().toString()).match(/ \((.*)\)$/)[1].

@anba
Copy link
Contributor

anba commented May 28, 2024

@justingrant i can extract the local timezone from Date with (new Date().toString()).match(/ \((.*)\)$/)[1].

That's not a time zone id, though. It's just a localised time zone name.

js> console.log((new Date().toString()).match(/ \((.*)\)$/)[1])
Mitteleuropäische Sommerzeit

And the same output when starting the js-shell with TZ=Europe/Paris:

js> console.log((new Date().toString()).match(/ \((.*)\)$/)[1])
Mitteleuropäische Sommerzeit

@Yay295
Copy link

Yay295 commented Jul 9, 2024

Work around incomplete/outdated support in TZDB

Not a solution, but it might be helpful to expose the TZDB version number/ID that is being used.

@ptomato
Copy link
Collaborator

ptomato commented Jul 11, 2024

That'd certainly be helpful if the host environment could expose it out-of-band, but hosts aren't required to use any version of the TZDB as long as they don't claim to be "time zone aware".

ptomato added a commit that referenced this issue Aug 22, 2024
This is a very large change, as it not only removes Temporal.Calendar and
Temporal.TimeZone, but also tries to eliminate any extra complexity due to
no longer having to deal with user code calls for calendar and time zone
calculations.

Some of the things that are removed or simplified include:

- No more Calendar Method Records and Time Zone Method Records

- In many places, no need to pass around the user's original options bag

- In many places, no need to pass around the user's original PlainDate or
  Instant; use epoch nanoseconds, ISO Date Records, and ISO Date-Time
  Records instead

- No more copying the own properties of options bags

- Most of the calendar and time zone operations are now infallible

- The set of extra calendar fields that used to be returned by
  Temporal.Calendar.prototype.fields() is now static; so no need to have
  the complicated PrepareTemporalFields operation that returns a null-
  prototype object with own data properties that correspond to arbitrary
  user fields. Dates in calendar space can be represented by a Calendar
  Fields Record with known fields.

- Much of the special-casing to avoid user calls that was added in #2519
  and similar PRs is now unobservable and is removed.

Closes: #2836
Closes: #2853
Closes: #2854
ptomato added a commit that referenced this issue Sep 5, 2024
This is a very large change, as it not only removes Temporal.Calendar and
Temporal.TimeZone, but also tries to eliminate any extra complexity due to
no longer having to deal with user code calls for calendar and time zone
calculations.

Some of the things that are removed or simplified include:

- No more Calendar Method Records and Time Zone Method Records

- In many places, no need to pass around the user's original options bag

- In many places, no need to pass around the user's original PlainDate or
  Instant; use epoch nanoseconds, ISO Date Records, and ISO Date-Time
  Records instead

- No more copying the own properties of options bags

- Most of the calendar and time zone operations are now infallible

- The set of extra calendar fields that used to be returned by
  Temporal.Calendar.prototype.fields() is now static; so no need to have
  the complicated PrepareTemporalFields operation that returns a null-
  prototype object with own data properties that correspond to arbitrary
  user fields. Dates in calendar space can be represented by a Calendar
  Fields Record with known fields.

- Much of the special-casing to avoid user calls that was added in #2519
  and similar PRs is now unobservable and is removed.

Closes: #2836
Closes: #2853
Closes: #2854
@ptomato ptomato closed this as completed in b889242 Sep 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size-suggestion Suggestions to shrink size of Temporal to address implementer concerns
Projects
None yet
Development

No branches or pull requests

6 participants