Repository: denoland/deno
Pull request: fix(unstable/temporal): implement `Temporal.ZonedDateTime.getTimeZoneTransition`

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->
This patches Temporal to support the `getTimeZoneTransition` method on `ZonedDateTime`.

See https://github.com/denoland/deno/issues/27731.

Changed files:
filename: runtime/js/99_main.js
patch: @@ -42,6 +42,7 @@ const {
   ObjectSetPrototypeOf,
   PromisePrototypeThen,
   PromiseResolve,
+  RangeError,
   StringPrototypePadEnd,
   Symbol,
   SymbolIterator,
@@ -473,6 +474,136 @@ function exposeUnstableFeaturesForWindowOrWorkerGlobalScope(unstableFeatures) {
   }
 }
 
+function updateTemporal() {
+  // Removes the obsoleted `Temporal` API.
+  // https://github.com/tc39/proposal-temporal/pull/2895
+  // https://github.com/tc39/proposal-temporal/pull/2914
+  // https://github.com/tc39/proposal-temporal/pull/2925
+  if (typeof globalThis.Temporal.Instant.fromEpochSeconds === "undefined") {
+    throw "V8 removes obsoleted Temporal API now, no need to delete them";
+  }
+  delete globalThis.Temporal.Instant.fromEpochSeconds;
+  delete globalThis.Temporal.Instant.fromEpochMicroseconds;
+  delete globalThis.Temporal.Instant.prototype.epochSeconds;
+  delete globalThis.Temporal.Instant.prototype.epochMicroseconds;
+  delete globalThis.Temporal.Instant.prototype.toZonedDateTime;
+  delete globalThis.Temporal.PlainDate.prototype.getISOFiels; // weird
+  delete globalThis.Temporal.PlainDate.prototype.getISOFields;
+  delete globalThis.Temporal.PlainDateTime.prototype.withPlainDate;
+  delete globalThis.Temporal.PlainDateTime.prototype.toPlainYearMonth;
+  delete globalThis.Temporal.PlainDateTime.prototype.toPlainMonthDay;
+  delete globalThis.Temporal.PlainDateTime.prototype.getISOFields;
+  delete globalThis.Temporal.PlainMonthDay.prototype.getISOFields;
+  delete globalThis.Temporal.PlainTime.prototype.calendar;
+  delete globalThis.Temporal.PlainTime.prototype.toPlainDateTime;
+  delete globalThis.Temporal.PlainTime.prototype.toZonedDateTime;
+  delete globalThis.Temporal.PlainTime.prototype.getISOFields;
+  delete globalThis.Temporal.PlainYearMonth.prototype.getISOFields;
+  delete globalThis.Temporal.ZonedDateTime.prototype.epochSeconds;
+  delete globalThis.Temporal.ZonedDateTime.prototype.epochMicroseconds;
+  delete globalThis.Temporal.ZonedDateTime.prototype.withPlainDate;
+  delete globalThis.Temporal.ZonedDateTime.prototype.toPlainYearMonth;
+  delete globalThis.Temporal.ZonedDateTime.prototype.toPlainMonthDay;
+  delete globalThis.Temporal.ZonedDateTime.prototype.getISOFields;
+  delete globalThis.Temporal.Now.zonedDateTime;
+  delete globalThis.Temporal.Now.plainDateTime;
+  delete globalThis.Temporal.Now.plainDate;
+  delete globalThis.Temporal.Calendar;
+  delete globalThis.Temporal.TimeZone;
+
+  // Modify `Temporal.Calendar` to calendarId string
+  ArrayPrototypeForEach([
+    globalThis.Temporal.PlainDate,
+    globalThis.Temporal.PlainDateTime,
+    globalThis.Temporal.PlainMonthDay,
+    globalThis.Temporal.PlainYearMonth,
+    globalThis.Temporal.ZonedDateTime,
+  ], (target) => {
+    const getCalendar =
+      ObjectGetOwnPropertyDescriptor(target.prototype, "calendar").get;
+    ObjectDefineProperty(target.prototype, "calendarId", {
+      __proto__: null,
+      get: function calendarId() {
+        return FunctionPrototypeCall(getCalendar, this).id;
+      },
+      enumerable: false,
+      configurable: true,
+    });
+    delete target.prototype.calendar;
+  });
+
+  // Modify `Temporal.TimeZone` to timeZoneId string
+  {
+    const getTimeZone = ObjectGetOwnPropertyDescriptor(
+      globalThis.Temporal.ZonedDateTime.prototype,
+      "timeZone",
+    ).get;
+    ObjectDefineProperty(
+      globalThis.Temporal.ZonedDateTime.prototype,
+      "timeZoneId",
+      {
+        __proto__: null,
+        get: function timeZoneId() {
+          return FunctionPrototypeCall(getTimeZone, this).id;
+        },
+        enumerable: false,
+        configurable: true,
+      },
+    );
+    ObjectAssign(globalThis.Temporal.ZonedDateTime.prototype, {
+      getTimeZoneTransition(options) {
+        if (options === undefined) {
+          throw new TypeError("options parameter is required");
+        }
+        if (typeof options === "string") {
+          options = {
+            direction: options,
+          };
+        }
+        const direction = options.direction;
+        if (direction === undefined) {
+          throw new TypeError("direction option is required");
+        }
+        const tz = FunctionPrototypeCall(getTimeZone, this);
+        let resultInstant;
+        switch (direction) {
+          case "next":
+            resultInstant = tz.getNextTransition(this.toInstant());
+            break;
+          case "previous":
+            resultInstant = tz.getPreviousTransition(this.toInstant());
+            break;
+          default:
+            throw new RangeError(
+              `direction must be one of next, previous, not ${options.direction}`,
+            );
+        }
+        return resultInstant?.toZonedDateTimeISO(tz.id) ?? null;
+      },
+    });
+    delete globalThis.Temporal.ZonedDateTime.prototype.timeZone;
+  }
+  {
+    const nowTimeZone = globalThis.Temporal.Now.timeZone;
+    ObjectDefineProperty(globalThis.Temporal.Now, "timeZoneId", {
+      __proto__: null,
+      value: function timeZoneId() {
+        return nowTimeZone().id;
+      },
+      writable: true,
+      enumerable: false,
+      configurable: true,
+    });
+    delete globalThis.Temporal.Now.timeZone;
+  }
+
+  // deno-lint-ignore prefer-primordials
+  if (new Temporal.Duration().toLocaleString("en-US") !== "PT0S") {
+    throw "V8 supports Temporal.Duration.prototype.toLocaleString now, no need to shim it";
+  }
+  shimTemporalDurationToLocaleString();
+}
+
 function shimTemporalDurationToLocaleString() {
   const DurationFormat = Intl.DurationFormat;
   if (!DurationFormat) {
@@ -803,102 +934,7 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) {
       delete globalThis.Temporal;
       delete globalThis.Date.prototype.toTemporalInstant;
     } else {
-      // Removes the obsoleted `Temporal` API.
-      // https://github.com/tc39/proposal-temporal/pull/2895
-      // https://github.com/tc39/proposal-temporal/pull/2914
-      // https://github.com/tc39/proposal-temporal/pull/2925
-      if (typeof globalThis.Temporal.Instant.fromEpochSeconds === "undefined") {
-        throw "V8 removes obsoleted Temporal API now, no need to delete them";
-      }
-      delete globalThis.Temporal.Instant.fromEpochSeconds;
-      delete globalThis.Temporal.Instant.fromEpochMicroseconds;
-      delete globalThis.Temporal.Instant.prototype.epochSeconds;
-      delete globalThis.Temporal.Instant.prototype.epochMicroseconds;
-      delete globalThis.Temporal.Instant.prototype.toZonedDateTime;
-      delete globalThis.Temporal.PlainDate.prototype.getISOFiels; // weird
-      delete globalThis.Temporal.PlainDate.prototype.getISOFields;
-      delete globalThis.Temporal.PlainDateTime.prototype.withPlainDate;
-      delete globalThis.Temporal.PlainDateTime.prototype.toPlainYearMonth;
-      delete globalThis.Temporal.PlainDateTime.prototype.toPlainMonthDay;
-      delete globalThis.Temporal.PlainDateTime.prototype.getISOFields;
-      delete globalThis.Temporal.PlainMonthDay.prototype.getISOFields;
-      delete globalThis.Temporal.PlainTime.prototype.calendar;
-      delete globalThis.Temporal.PlainTime.prototype.toPlainDateTime;
-      delete globalThis.Temporal.PlainTime.prototype.toZonedDateTime;
-      delete globalThis.Temporal.PlainTime.prototype.getISOFields;
-      delete globalThis.Temporal.PlainYearMonth.prototype.getISOFields;
-      delete globalThis.Temporal.ZonedDateTime.prototype.epochSeconds;
-      delete globalThis.Temporal.ZonedDateTime.prototype.epochMicroseconds;
-      delete globalThis.Temporal.ZonedDateTime.prototype.withPlainDate;
-      delete globalThis.Temporal.ZonedDateTime.prototype.toPlainYearMonth;
-      delete globalThis.Temporal.ZonedDateTime.prototype.toPlainMonthDay;
-      delete globalThis.Temporal.ZonedDateTime.prototype.getISOFields;
-      delete globalThis.Temporal.Now.zonedDateTime;
-      delete globalThis.Temporal.Now.plainDateTime;
-      delete globalThis.Temporal.Now.plainDate;
-      delete globalThis.Temporal.Calendar;
-      delete globalThis.Temporal.TimeZone;
-
-      // Modify `Temporal.Calendar` to calendarId string
-      ArrayPrototypeForEach([
-        globalThis.Temporal.PlainDate,
-        globalThis.Temporal.PlainDateTime,
-        globalThis.Temporal.PlainMonthDay,
-        globalThis.Temporal.PlainYearMonth,
-        globalThis.Temporal.ZonedDateTime,
-      ], (target) => {
-        const getCalendar =
-          ObjectGetOwnPropertyDescriptor(target.prototype, "calendar").get;
-        ObjectDefineProperty(target.prototype, "calendarId", {
-          __proto__: null,
-          get: function calendarId() {
-            return FunctionPrototypeCall(getCalendar, this).id;
-          },
-          enumerable: false,
-          configurable: true,
-        });
-        delete target.prototype.calendar;
-      });
-
-      // Modify `Temporal.TimeZone` to timeZoneId string
-      {
-        const getTimeZone = ObjectGetOwnPropertyDescriptor(
-          globalThis.Temporal.ZonedDateTime.prototype,
-          "timeZone",
-        ).get;
-        ObjectDefineProperty(
-          globalThis.Temporal.ZonedDateTime.prototype,
-          "timeZoneId",
-          {
-            __proto__: null,
-            get: function timeZoneId() {
-              return FunctionPrototypeCall(getTimeZone, this).id;
-            },
-            enumerable: false,
-            configurable: true,
-          },
-        );
-        delete globalThis.Temporal.ZonedDateTime.prototype.timeZone;
-      }
-      {
-        const nowTimeZone = globalThis.Temporal.Now.timeZone;
-        ObjectDefineProperty(globalThis.Temporal.Now, "timeZoneId", {
-          __proto__: null,
-          value: function timeZoneId() {
-            return nowTimeZone().id;
-          },
-          writable: true,
-          enumerable: false,
-          configurable: true,
-        });
-        delete globalThis.Temporal.Now.timeZone;
-      }
-
-      // deno-lint-ignore prefer-primordials
-      if (new Temporal.Duration().toLocaleString("en-US") !== "PT0S") {
-        throw "V8 supports Temporal.Duration.prototype.toLocaleString now, no need to shim it";
-      }
-      shimTemporalDurationToLocaleString();
+      updateTemporal();
     }
 
     // Setup `Deno` global - we're actually overriding already existing global
@@ -1014,98 +1050,7 @@ function bootstrapWorkerRuntime(
       delete globalThis.Temporal;
       delete globalThis.Date.prototype.toTemporalInstant;
     } else {
-      // Removes the obsoleted `Temporal` API.
-      // https://github.com/tc39/proposal-temporal/pull/2895
-      // https://github.com/tc39/proposal-temporal/pull/2914
-      // https://github.com/tc39/proposal-temporal/pull/2925
-      if (typeof globalThis.Temporal.Instant.fromEpochSeconds === "undefined") {
-        throw "V8 removes obsoleted Temporal API now, no need to delete them";
-      }
-      delete globalThis.Temporal.Instant.fromEpochSeconds;
-      delete globalThis.Temporal.Instant.fromEpochMicroseconds;
-      delete globalThis.Temporal.Instant.prototype.epochSeconds;
-      delete globalThis.Temporal.Instant.prototype.epochMicroseconds;
-      delete globalThis.Temporal.Instant.prototype.toZonedDateTime;
-      delete globalThis.Temporal.PlainDate.prototype.getISOFiels; // weird
-      delete globalThis.Temporal.PlainDate.prototype.getISOFields;
-      delete globalThis.Temporal.PlainDateTime.prototype.withPlainDate;
-      delete globalThis.Temporal.PlainDateTime.prototype.toPlainYearMonth;
-      delete globalThis.Temporal.PlainDateTime.prototype.toPlainMonthDay;
-      delete globalThis.Temporal.PlainDateTime.prototype.getISOFields;
-      delete globalThis.Temporal.PlainMonthDay.prototype.getISOFields;
-      delete globalThis.Temporal.PlainTime.prototype.calendar;
-      delete globalThis.Temporal.PlainTime.prototype.toPlainDateTime;
-      delete globalThis.Temporal.PlainTime.prototype.toZonedDateTime;
-      delete globalThis.Temporal.PlainTime.prototype.getISOFields;
-      delete globalThis.Temporal.PlainYearMonth.prototype.getISOFields;
-      delete globalThis.Temporal.ZonedDateTime.prototype.epochSeconds;
-      delete globalThis.Temporal.ZonedDateTime.prototype.epochMicroseconds;
-      delete globalThis.Temporal.ZonedDateTime.prototype.withPlainDate;
-      delete globalThis.Temporal.ZonedDateTime.prototype.toPlainYearMonth;
-      delete globalThis.Temporal.ZonedDateTime.prototype.toPlainMonthDay;
-      delete globalThis.Temporal.ZonedDateTime.prototype.getISOFields;
-      delete globalThis.Temporal.Now.zonedDateTime;
-      delete globalThis.Temporal.Now.plainDateTime;
-      delete globalThis.Temporal.Now.plainDate;
-      delete globalThis.Temporal.Calendar;
-      delete globalThis.Temporal.TimeZone;
-
-      // Modify `Temporal.Calendar` to calendarId string
-      ArrayPrototypeForEach([
-        globalThis.Temporal.PlainDate,
-        globalThis.Temporal.PlainDateTime,
-        globalThis.Temporal.PlainMonthDay,
-        globalThis.Temporal.PlainYearMonth,
-        globalThis.Temporal.ZonedDateTime,
-      ], (target) => {
-        const getCalendar =
-          ObjectGetOwnPropertyDescriptor(target.prototype, "calendar").get;
-        ObjectDefineProperty(target.prototype, "calendarId", {
-          __proto__: null,
-          get: function calendarId() {
-            return FunctionPrototypeCall(getCalendar, this).id;
-          },
-          enumerable: false,
-          configurable: true,
-        });
-        delete target.prototype.calendar;
-      });
-
-      // Modify `Temporal.TimeZone` to timeZoneId string
-      {
-        const getTimeZone = ObjectGetOwnPropertyDescriptor(
-          globalThis.Temporal.ZonedDateTime.prototype,
-          "timeZone",
-        ).get;
-        ObjectDefineProperty(
-          globalThis.Temporal.ZonedDateTime.prototype,
-          "timeZoneId",
-          {
-            __proto__: null,
-            get: function timeZoneId() {
-              return FunctionPrototypeCall(getTimeZone, this).id;
-            },
-            enumerable: false,
-            configurable: true,
-          },
-        );
-        delete globalThis.Temporal.ZonedDateTime.prototype.timeZone;
-      }
-      {
-        const nowTimeZone = globalThis.Temporal.Now.timeZone;
-        ObjectDefineProperty(globalThis.Temporal.Now, "timeZoneId", {
-          __proto__: null,
-          value: function timeZoneId() {
-            return nowTimeZone().id;
-          },
-          writable: true,
-          enumerable: false,
-          configurable: true,
-        });
-        delete globalThis.Temporal.Now.timeZone;
-      }
-
-      shimTemporalDurationToLocaleString();
+      updateTemporal();
     }
 
     // Setup `Deno` global - we're actually overriding already existing global

filename: tests/specs/run/unstable_temporal_api_list/main.out
patch: @@ -39,22 +39,22 @@ Temporal.Instant.prototype
 ]
 Temporal.ZonedDateTime.prototype
 [
-  "add",               "calendarId",        "constructor",
-  "day",               "dayOfWeek",         "dayOfYear",
-  "daysInMonth",       "daysInWeek",        "daysInYear",
-  "epochMilliseconds", "epochNanoseconds",  "equals",
-  "era",               "eraYear",           "hour",
-  "hoursInDay",        "inLeapYear",        "microsecond",
-  "millisecond",       "minute",            "month",
-  "monthCode",         "monthsInYear",      "nanosecond",
-  "offset",            "offsetNanoseconds", "round",
-  "second",            "since",             "startOfDay",
-  "subtract",          "timeZoneId",        "toInstant",
-  "toJSON",            "toLocaleString",    "toPlainDate",
-  "toPlainDateTime",   "toPlainTime",       "toString",
-  "until",             "valueOf",           "weekOfYear",
-  "with",              "withCalendar",      "withPlainTime",
-  "withTimeZone",      "year"
+  "add",               "calendarId",       "constructor",
+  "day",               "dayOfWeek",        "dayOfYear",
+  "daysInMonth",       "daysInWeek",       "daysInYear",
+  "epochMilliseconds", "epochNanoseconds", "equals",
+  "era",               "eraYear",          "getTimeZoneTransition",
+  "hour",              "hoursInDay",       "inLeapYear",
+  "microsecond",       "millisecond",      "minute",
+  "month",             "monthCode",        "monthsInYear",
+  "nanosecond",        "offset",           "offsetNanoseconds",
+  "round",             "second",           "since",
+  "startOfDay",        "subtract",         "timeZoneId",
+  "toInstant",         "toJSON",           "toLocaleString",
+  "toPlainDate",       "toPlainDateTime",  "toPlainTime",
+  "toString",          "until",            "valueOf",
+  "weekOfYear",        "with",             "withCalendar",
+  "withPlainTime",     "withTimeZone",     "year"
 ]
 Temporal.PlainDate.prototype
 [

filename: tests/specs/run/unstable_temporal_api_patch/main.out
patch: @@ -6,3 +6,4 @@ UTC
 undefined
 undefined
 1 day, 6 hr, 30 min
+2020-03-08T03:00:00-04:00[America/New_York]

filename: tests/specs/run/unstable_temporal_api_patch/main.ts
patch: @@ -12,3 +12,8 @@ console.log(zoned.timeZone);
 
 const duration = Temporal.Duration.from("P1DT6H30M");
 console.log(duration.toLocaleString("en-US"));
+
+const transition = Temporal.ZonedDateTime.from(
+  "2020-01-01T00:00:00-05:00[America/New_York]",
+).getTimeZoneTransition("next");
+console.log(transition);