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

Rebase up to September 2023 #292

Merged
merged 22 commits into from
Jan 13, 2025
Merged

Rebase up to September 2023 #292

merged 22 commits into from
Jan 13, 2025

Conversation

ptomato
Copy link
Contributor

@ptomato ptomato commented Jan 11, 2025

I did an afternoon of rebasing and have completed up to the normative changes that landed in September 2023. That seems to be a nice sized batch for reviewing.

Note, I skipped tc39/proposal-temporal@36f288a because the implementation for transition-finding has already diverged so much from the proposal-polyfill, and already had a different lookahead.

Copy link
Contributor

@justingrant justingrant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you sooooooo much for doing this!

@justingrant
Copy link
Contributor

Note, I skipped tc39/proposal-temporal@36f288a because the implementation for transition-finding has already diverged so much from the proposal-polyfill, and already had a different lookahead.

I actually had started a rebase 6 months ago and never finished it (only did 5 commits), but I did end up porting this commit using the diff below. Feel free to use or not use as you see fit!

justingrant@Justins-Laptop temporal-polyfill % git show 2f78fecd
commit 2f78fecd786d395c3871cb4e7c0bcf773e9f64df
Author: Justin Grant <justingrant@users.noreply.github.com>
Date:   Sat Aug 19 01:19:47 2023 -0700

    Polyfill: Extend transition-finding lookahead
    
    GetNamedTimeZonePreviousTransition currently has an optimization to
    avoid fruitlessly searching for a transition for far-future dates. This
    optimization ignores cases where a time zone's offset changes are
    scheduled more than one year in advance.  This commit extends the
    lookahead period to three years.
    
    Similarly, GetNamedTimeZoneNextTransition has an optimization
    where it assumes that no time zone has any transitions more than
    one year in the future. This commit extends this lookahead to three
    years later than either the current date or the input instant, whichever
    is later.
    
    When testing locally, these changes slow down these AOs by 2%-4%,
    which IMO is an OK tradeoff for more accuracy.
    
    Fixes #2564
    
    UPSTREAM_COMMIT=36f288a64f6e1fde74002c2d8131b17980283f2b

diff --git a/lib/ecmascript.ts b/lib/ecmascript.ts
index 3777632a..7f9a5bff 100644
--- a/lib/ecmascript.ts
+++ b/lib/ecmascript.ts
 const BUILTIN_CALENDAR_IDS = [
@@ -3448,44 +3447,19 @@ export function GetNamedTimeZoneDateTimeParts(id: string, epochNanoseconds: JSBI
   return BalanceISODateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
 }
 
-function maxJSBI(one: JSBI, two: JSBI) {
-  return JSBI.lessThan(one, two) ? two : one;
-}
-
-/**
- * Our best guess at how far in advance new rules will be put into the TZDB for
- * future offset transitions. We'll pick 10 years but can always revise it if
- * we find that countries are being unusually proactive in their announcing
- * of offset changes.
- */
-function afterLatestPossibleTzdbRuleChange() {
-  return JSBI.add(SystemUTCEpochNanoSeconds(), ABOUT_TEN_YEARS_NANOS);
-}
-
 export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: JSBI): JSBI | null {
   if (JSBI.lessThan(epochNanoseconds, BEFORE_FIRST_OFFSET_TRANSITION)) {
     return GetNamedTimeZoneNextTransition(id, BEFORE_FIRST_OFFSET_TRANSITION);
   }
-  // Decide how far in the future after `epochNanoseconds` we'll look for an
-  // offset change. There are two cases:
-  // 1. If it's a past date (or a date in the near future) then it's possible
-  //    that the time zone may have newly added DST in the next few years. So
-  //    we'll have to look from the provided time until a few years after the
-  //    current system time. (Changes to DST policy are usually announced a few
-  //    years in the future.) Note that the first DST anywhere started in 1847,
-  //    so we'll start checks in 1847 instead of wasting cycles on years where
-  //    there will never be transitions.
-  // 2. If it's a future date beyond the next few years, then we'll just assume
-  //    that the latest DST policy in TZDB will still be in effect.  In this
-  //    case, we only need to look one year in the future to see if there are
-  //    any DST transitions.  We actually only need to look 9-10 months because
-  //    DST has two transitions per year, but we'll use a year just to be safe.
-  const oneYearLater = JSBI.add(epochNanoseconds, ABOUT_ONE_YEAR_NANOS);
-  const uppercap = maxJSBI(afterLatestPossibleTzdbRuleChange(), oneYearLater);
-  // The first transition (in any timezone) recorded in the TZDB was in 1847, so
-  // start there if an earlier date is supplied.
-  let leftNanos = maxJSBI(BEFORE_FIRST_OFFSET_TRANSITION, epochNanoseconds);
-  const leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos);
+  // Optimization: the farthest that we'll look for a next transition is 3 years
+  // after the later of epochNanoseconds or the current time. If there are no
+  // transitions found before then, we'll assume that there will not be any more
+  // transitions after that.
+  const now = SystemUTCEpochNanoSeconds();
+  const base = JSBI.GT(epochNanoseconds, now) ? epochNanoseconds : now;
+  const uppercap = JSBI.add(base, ABOUT_THREE_YEARS_NANOS);
+  let leftNanos = epochNanoseconds;
+  let leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos);
   let rightNanos = leftNanos;
   let rightOffsetNs = leftOffsetNs;
   while (leftOffsetNs === rightOffsetNs && JSBI.lessThan(JSBI.BigInt(leftNanos), uppercap)) {
@@ -3508,28 +3482,17 @@ export function GetNamedTimeZoneNextTransition(id: string, epochNanoseconds: JSB
 }
 
 export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds: JSBI): JSBI | null {
-  // If a time zone uses DST (at the time of `epochNanoseconds`), then we only
-  // have to look back one year to find a transition. But if it doesn't use DST,
-  // then we need to look all the way back to 1847 (the earliest rule in the
-  // TZDB) to see if it had other offset transitions in the past. Looping back
-  // from a far-future date to 1847 is very slow (minutes of 100% CPU!), and is
-  // also unnecessary because DST rules aren't put into the TZDB more than a few
-  // years in the future because the political changes in time zones happen with
-  // only a few years' warning. Therefore, if a far-future date is provided,
-  // then we'll run the check in two parts:
-  // 1. First, we'll look back for up to one year to see if the latest TZDB
-  //    rules have DST.
-  // 2. If not, then we'll "fast-reverse" back to a few years later than the
-  //    current system time, and then look back to 1847. This reduces the
-  //    worst-case loop from 273K years to 175 years, for a ~1500x improvement
-  //    in worst-case perf.
-  const afterLatestRule = afterLatestPossibleTzdbRuleChange();
-  const isFarFuture = JSBI.greaterThan(epochNanoseconds, afterLatestRule);
-  const lowercap = isFarFuture ? JSBI.subtract(epochNanoseconds, ABOUT_ONE_YEAR_NANOS) : BEFORE_FIRST_OFFSET_TRANSITION;
-
-  // TODO: proposal-temporal polyfill has different code for very similar
-  // optimizations as above, as well as in GetNamedTimeZonePreviousTransition.
-  // We should figure out if we should change one polyfill to match the other.
+  // Optimization: if the instant is more than 3 years in the future and there
+  // are no transitions between the present day and 3 years from now, assume
+  // there are none after.
+  const now = SystemUTCEpochNanoSeconds();
+  const lookahead = JSBI.add(now, ABOUT_THREE_YEARS_NANOS);
+  if (JSBI.greaterThan(epochNanoseconds, lookahead)) {
+    const prevBeforeLookahead = GetNamedTimeZonePreviousTransition(id, lookahead);
+    if (prevBeforeLookahead === null || JSBI.lessThan(prevBeforeLookahead, now)) {
+      return prevBeforeLookahead;
+    }
+  }
 
   // We assume most time zones either have regular DST rules that extend
   // indefinitely into the future, or they have no DST transitions between now
@@ -3550,7 +3513,7 @@ export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds:
   const rightOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, rightNanos);
   let leftNanos = rightNanos;
   let leftOffsetNs = rightOffsetNs;
-  while (rightOffsetNs === leftOffsetNs && JSBI.greaterThan(rightNanos, lowercap)) {
+  while (rightOffsetNs === leftOffsetNs && JSBI.greaterThan(rightNanos, BEFORE_FIRST_OFFSET_TRANSITION)) {
     leftNanos = JSBI.subtract(rightNanos, TWO_WEEKS_NANOS);
     if (JSBI.lessThan(leftNanos, BEFORE_FIRST_OFFSET_TRANSITION)) return null;
     leftOffsetNs = GetNamedTimeZoneOffsetNanoseconds(id, leftNanos);
@@ -3558,20 +3521,7 @@ export function GetNamedTimeZonePreviousTransition(id: string, epochNanoseconds:
       rightNanos = leftNanos;
     }
   }
-  if (rightOffsetNs === leftOffsetNs) {
-    if (isFarFuture) {
-      // There was no DST after looking back one year, which means that the most
-      // recent TZDB rules don't have any recurring transitions. To check for
-      // transitions in older rules, back up to a few years after the current
-      // date and then look all the way back to 1847. Note that we move back one
-      // day from the latest possible rule so that when the recursion runs it
-      // won't consider the new time to be "far future" because the system clock
-      // has advanced in the meantime.
-      const newTimeToCheck = JSBI.subtract(afterLatestRule, DAY_NANOS);
-      return GetNamedTimeZonePreviousTransition(id, newTimeToCheck);
-    }
-    return null;
-  }
+  if (rightOffsetNs === leftOffsetNs) return null;
   const result = bisect(
     (epochNs: JSBI) => GetNamedTimeZoneOffsetNanoseconds(id, epochNs),
     leftNanos,

Copy link
Contributor

@12wrigja 12wrigja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working through this, Philip!

justingrant and others added 22 commits January 13, 2025 09:21
GetNamedTimeZonePreviousTransition currently has an optimization to
avoid fruitlessly searching for a transition for far-future dates. This
optimization ignores cases where a time zone's offset changes are
scheduled more than one year in advance.  This commit extends the
lookahead period to three years.

Similarly, GetNamedTimeZoneNextTransition has an optimization
where it assumes that no time zone has any transitions more than
one year in the future. This commit extends this lookahead to three
years later than either the current date or the input instant, whichever
is later.

When testing locally, these changes slow down these AOs by 2%-4%,
which IMO is an OK tradeoff for more accuracy.

Fixes #2564

UPSTREAM_COMMIT=36f288a64f6e1fde74002c2d8131b17980283f2b
This was left over from some refactoring.

UPSTREAM_COMMIT=d5a759156d0250937c44be418a34a1d026782f40
The ZonedDateTime code path in ToTemporalDate was missing a return
statement. Falling through to the PlainDateTime code path still gave the
right answer, but options.overflow would be accessed an extra time.

The spec is already correct here, so this makes the reference code more
compliant.

UPSTREAM_COMMIT=80f9c395dcab35e92fb6256812457d350a0f52fb
In order to be able to properly test the normative change of not
observably iterating an Array in CalendarFields, we have to eliminate a
few more places in the reference code where, contrary to the spec, array
iteration was observable.

(Rebase note: This contains many more adjustments than the original
commit. Probably these adjustments were skipped in an earlier rebase.)

UPSTREAM_COMMIT=ca7288c4a74205bb007186543199787ec3ea8ef5
UPSTREAM_COMMIT=3ca02f1cf339de5a9ca0a3b24d9e716e3b507aeb
This commit refactors several operations related to range validation of
Temporal.PlainDateTime and Temporal.Instant.

* Adds optional _offsetNanoseconds_ parameter to GetUTCEpochNanoseconds,
  and uses it in call sites of GetUTCEpochNanoseconds where an offset
  was previously applied to its result.
* Makes polyfill GetUTCEpochNanoseconds infallible, like in the spec.
* Simplifies the polyfill's GetNamedTimeZoneOffsetNanoseconds by using
  the now-infallible GetUTCEpochNanoseconds.
* Simplifies the polyfill's PlainDateTime range validation in
  RejectDateTimeRange.
* Clarifies the PlainDateTime docs about that type's valid range.
* Removes non-parsing logic from ParseTemporalInstantString, aligning
  Instant parsing with the parsing pattern used in other Temporal
  types where parsing AOs contain only parsing without interpretation.
* Inlines the single-caller ParseTemporalInstant into ToTemporalInstant,
  matching the pattern used by ToTemporalYearMonth, ToTemporalTime, etc.
* Marks ParseDateTimeUTCOffset as infallible after parsing instant
  strings, because ParseTemporalInstantString guarantees that the
  offset is valid. Fixes #2637.
* Aligns polyfill more closely to spec for Instant parsing.

UPSTREAM_COMMIT=0dfd93a2c6a05eb96c8353c304a1abf5303b47a7
The spec throws a RangeError much earlier than we did in the reference
code. Change the reference code to do the same, and change the original
location where the RangeError was thrown into an assertion.

UPSTREAM_COMMIT=60ec103b726bb75aaf0d974f361275d3f18eef55
The spec creates a PlainDateTime with the ISO 8601 calendar here, not with
the DateTimeFormat's calendar. It may make an observable difference in the
case where GetInstantFor has to disambiguate, although after #2519 that
difference should not be observable anymore. In any case, the spec says to
do this, so we should align the reference code with that.

UPSTREAM_COMMIT=6af74eef2f98140e69c6e8ef3ea5d3131c7afd5a
The spec does not say to create a PlainDateTime with the DateTimeFormat's
calendar here. This is an observable difference from the spec because now
GetInstantFor may call the original PlainDateTime instance's calendar's
dateAdd method, although after #2519 that should no longer be observable.

UPSTREAM_COMMIT=b1c699bca2360dca3857b8e2c91b6ed0a3c75687
The constructors of the Plain types call the
ObjectImplementsTemporalCalendarProtocol AO which performs observable
HasProperty operations. The spec does not say to call the constructors
here, so those operations should not be performed. Instead, match the spec
by calling CreateTemporalDate and CreateTemporalDateTime to create
instances when necessary.

UPSTREAM_COMMIT=678fdd10b03dcd6e5547fb4b3c75c4d4d104cdec
We've received enough user confusion about valueOf throwing that it's
worth clarifying the error message.

UPSTREAM_COMMIT=088f2b3b7897bfd457a8ea6f867dc9566986aabe
These are mistakes from #2586. Spotted by Anba.

Closes: #2620

UPSTREAM_COMMIT=573de945e3a00c6ad76237ce68429428c1ff5b4b
If built in debug mode, the constructor of each object defines a _repr_
property for use in the browser playground, and a custom util.inspect
function for use in Node.

In order to display ISO date time of exact times, or time zone and
calendar annotations, this would have to perform user-observable
operations. This actually made it harder to debug user-observable calls
in debug mode!

This changes the debug printing to be, as far as I know, side-effect-free.
If the calendar or time zone is built-in (i.e. a string is stored in the
internal slot), the output should be the same.

If the calendar or time zone is an object and therefore has been exposed
to user code, we don't get its ID and we don't print the local time in the
time zone.

Examples of what changed:

PlainDate with custom calendar:
  before: "Temporal.PlainDate <2023-09-07[u-ca=cascadian]>"
  after:  "Temporal.PlainDate <2023-09-07[u-ca=<calendar object>]>"

ZonedDateTime with custom time zone:
  before: "Temporal.ZonedDateTime <2023-09-07T17:46:10-07:00[Custom/Cascadia]>"
  after:  "Temporal.ZonedDateTime <2023-09-08T00:46:10Z[<time zone object>]>"

Note that toString() does *not* change. This only modifies code that is
not part of the proposal!

UPSTREAM_COMMIT=97baad31e0b948979de0c7a1ec703a04ec079074
If the given rounding options specify a smallest unit of nanoseconds and a
rounding increment of 1, then the rounding operation is a no-op. In that
case, return a copy of the duration, without actually performing the
rounding operation. This applies to all round(), until(), and since()
methods.

In the case of Duration.p.round(), other conditions must be fulfilled as
well for the operation to be a no-op: no balancing must take place.

In the case of Instant.p.round(), PlainDate.p.since(),
PlainDate.p.until(), PlainDateTime.p.round(), PlainDateTime.p.since(),
PlainDateTime.p.until(), PlainTime.p.round(), PlainTime.p.since(),
and PlainTime.p.until(), the change is not observable, so implementors are
free to make this optimization anyway.

This optimization was already the case in PlainDate.p.since/until() and
PlainYearMonth.p.since/until(), so this makes the other since/until()
methods consistent with those.

UPSTREAM_COMMIT=51ea969a42f85b33cbd22a3b501d5170f3e10cc7
…uration

relativeTo as a PlainDate is only needed when smallestUnit is year, month,
or week. relativeTo as a ZonedDateTime is used additionally when
smallestUnit is day. However, ToTemporalDate only needs to be called in
the former case. Since ToTemporalDate potentially calls user code,
rearrange some steps to make sure to call it only when necessary.

See: #2247

UPSTREAM_COMMIT=540dc4d197fa53ce6caf4ed2d726eb7ef2421106
…mpare

We call UnbalanceDurationRelative twice with the same relativeTo object.
UnbalanceDurationRelative only uses plain or undefined relativeTo, not
zoned, so it will convert it with ToTemporalDate. No need to do this
twice; pre-convert it before the first call.

See: #2247

UPSTREAM_COMMIT=e9b9a6fcc377ddced0c45c840d2dd1a686314656
This converts a ZonedDateTime relativeTo into a PlainDateTime relativeTo
only when necessary, only after potentially throwing other errors, and
only once. Previously, it could be converted up to a few separate times in
each operation, such as UnbalanceDurationRelative, RoundDuration, and
BalanceDurationRelative. Since the conversion is user-visible, we don't
want to perform it when not necessary or perform it more times than
necessary.

Closes: #2247
Closes: #2529

UPSTREAM_COMMIT=742248e8a876f7259e96dae4b51f81e326e98631
…ds()

When calling CalendarFields with a built-in calendar, previously we would
create a JS Array from the passed List, and pass it to %Temporal.Calendar.
prototype.fields%, which would iterate it observably. So, instead we call
CalendarDateFields when the calendar is a built-in calendar, which doesn't
do anything observable at all.

See: #2289

(TypeScript refactor: change parameter type and return type of fields()
to FieldKey[])

UPSTREAM_COMMIT=392d86e15a0f43848d979ec66bfdf4aefbd7f371
…Time

There are several places where a ZonedDateTime is converted to a
PlainDateTime, which is a user-visible time zone operation, and then the
same operation is performed again right afterwards in AddZonedDateTime.

Avoid this by making AddZonedDateTime take an optional precalculated
PlainDateTime. If we have it, we can pass it in, and avoid the second
conversion.

UPSTREAM_COMMIT=853a2122a45425e9bb9262e4e7d99ec332dfc5af
If all the fields are equal, then no balancing has to happen, and the
relativeTo point doesn't matter; we know the durations are equal.

UPSTREAM_COMMIT=30ad5ec3b885b3fcf9fc092ca149fc5117da867b
CalculateOffsetShift is only used in Duration.compare, when relativeTo is
a ZonedDateTime. Calling it twice performs two ZonedDateTime additions,
which are potentially user-observable method calls. If we are performing
these two additions anyway, we may as well just add each duration to
relativeTo and compare the timestamps. This saves a lot of other
user-observable calls.

It also allows removing the offset-shift parameter from
TotalDurationNanoseconds, because now it is always zero. That parameter
was confusing anyway.
At the same time we remove the days parameter as well, because now there
is no indication that days should only be 24-hour days; we push the
responsibility of deciding whether 24-hour days are appropriate to the
caller.

UPSTREAM_COMMIT=4b31c7d896e7a1f83786bbee9e697d6b701af2cc
UPSTREAM_COMMIT=ec1eca80e3b181bccb48c978d79ddc9f89daddd2
@ptomato
Copy link
Contributor Author

ptomato commented Jan 13, 2025

I added Justin's solution for the lookahead. Thanks!

@ptomato ptomato merged commit a64021c into main Jan 13, 2025
19 checks passed
@ptomato ptomato deleted the rebase-2023-09 branch January 13, 2025 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants