Skip to content

Commit

Permalink
Normative: Replace TimeZone transition methods with ZonedDateTime met…
Browse files Browse the repository at this point in the history
…hods

TimeZone.p.getNextTransition → ZonedDateTime.p.nextTransition
TimeZone.p.getPreviousTransition → ZonedDateTime.p.previousTransition

This is one step towards removing Temporal.TimeZone. The functionality of
querying UTC offset transition remains, but is moved to ZonedDateTime.

See: #2826

Co-Authored-By: Richard Gibson <richard.gibson@gmail.com>
  • Loading branch information
ptomato and gibson042 committed Jun 13, 2024
1 parent 768cceb commit b7e4b36
Show file tree
Hide file tree
Showing 14 changed files with 168 additions and 181 deletions.
6 changes: 3 additions & 3 deletions docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,13 @@ Take the difference between two `Temporal.Instant` instances as a `Temporal.Dura
```
<!-- prettier-ignore-end -->

### Nearest offset transition in a time zone
### Next offset transition in a time zone

Map a `Temporal.Instant` instance and a `Temporal.TimeZone` object into a `Temporal.Instant` instance representing the nearest following exact time at which there is an offset transition in the time zone (e.g., for setting reminders).
Map a `Temporal.ZonedDateTime` instance into another `Temporal.ZonedDateTime` instance representing the nearest following exact time at which there is an offset transition in the time zone (e.g., for setting reminders).

<!-- prettier-ignore-start -->
```javascript
{{cookbook/getInstantOfNearestOffsetTransitionToInstant.mjs}}
{{cookbook/getNextOffsetTransitionFromExactTime.mjs}}
```
<!-- prettier-ignore-end -->

Expand Down
2 changes: 1 addition & 1 deletion docs/cookbook/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import './getCurrentDate.mjs';
import './getElapsedDurationSinceInstant.mjs';
import './getFirstTuesdayOfMonth.mjs';
import './getInstantBeforeOldRecord.mjs';
import './getInstantOfNearestOffsetTransitionToInstant.mjs';
import './getInstantWithLocalTimeInZone.mjs';
import './getLocalizedArrival.mjs';
import './getNextOffsetTransitionFromExactTime.mjs';
import './getParseableZonedStringAtInstant.mjs';
import './getSortedLocalDateTimes.mjs';
import './getTimeStamp.mjs';
Expand Down
34 changes: 0 additions & 34 deletions docs/cookbook/getInstantOfNearestOffsetTransitionToInstant.mjs

This file was deleted.

21 changes: 10 additions & 11 deletions docs/cookbook/getInstantWithLocalTimeInZone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,26 @@
function getInstantWithLocalTimeInZone(dateTime, timeZone, disambiguation = 'earlier') {
// Handle the built-in modes first
if (['compatible', 'earlier', 'later', 'reject'].includes(disambiguation)) {
return timeZone.getInstantFor(dateTime, { disambiguation });
return dateTime.toZonedDateTime(timeZone, { disambiguation }).toInstant();
}

const possible = timeZone.getPossibleInstantsFor(dateTime);
const zdts = ['earlier', 'later'].map((disambiguation) => dateTime.toZonedDateTime(timeZone, { disambiguation }));
const instants = zdts.map((zdt) => zdt.toInstant()).reduce((a, b) => (a.equals(b) ? [a] : [a, b]));

// Return only possibility if no disambiguation needed
if (possible.length === 1) return possible[0];
if (instants.length === 1) return instants[0];

switch (disambiguation) {
case 'clipEarlier':
if (possible.length === 0) {
const before = timeZone.getInstantFor(dateTime, { disambiguation: 'earlier' });
return timeZone.getNextTransition(before).subtract({ nanoseconds: 1 });
if (zdts[0].toPlainDateTime().equals(dateTime)) {
return instants[0];
}
return possible[0];
return zdts[0].getTimeZoneTransition('next').subtract({ nanoseconds: 1 }).toInstant();
case 'clipLater':
if (possible.length === 0) {
const before = timeZone.getInstantFor(dateTime, { disambiguation: 'earlier' });
return timeZone.getNextTransition(before);
if (zdts[1].toPlainDateTime().equals(dateTime)) {
return instants[1];
}
return possible[possible.length - 1];
return zdts[0].getTimeZoneTransition('next').toInstant();
}
throw new RangeError(`invalid disambiguation ${disambiguation}`);
}
Expand Down
33 changes: 33 additions & 0 deletions docs/cookbook/getNextOffsetTransitionFromExactTime.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Get the nearest following exact time that the given time zone transitions
* to another UTC offset, inclusive or exclusive.
*
* @param {Temporal.ZonedDateTime} zonedDateTime - Starting exact time and time
* zone to consider
* @param {boolean} inclusive - Include the start time, or not
* @returns {(Temporal.ZonedDateTime|null)} - Next UTC offset transition, or
* null if none known at this time
*/
function getNextOffsetTransitionFromExactTime(zonedDateTime, inclusive) {
let nearest;
if (inclusive) {
// In case instant itself is the exact time of a transition:
nearest = zonedDateTime.subtract({ nanoseconds: 1 }).getTimeZoneTransition('next');
} else {
nearest = zonedDateTime.getTimeZoneTransition('next');
}
return nearest;
}

const nycTime = Temporal.ZonedDateTime.from('2019-04-16T21:01Z[America/New_York]');

const nextTransition = getNextOffsetTransitionFromExactTime(nycTime, false);
assert.equal(nextTransition.toString(), '2019-11-03T01:00:00-05:00[America/New_York]');

// Inclusive
const sameTransition = getNextOffsetTransitionFromExactTime(nextTransition, true);
assert.equal(sameTransition.toString(), nextTransition.toString());

// No known future DST transitions in a time zone
const reginaTime = Temporal.ZonedDateTime.from('2019-04-16T21:01Z[America/Regina]');
assert.equal(getNextOffsetTransitionFromExactTime(reginaTime), null);
66 changes: 0 additions & 66 deletions docs/timezone.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,72 +446,6 @@ See [Resolving ambiguity](./ambiguity.md) for usage examples and a more complete
Although this method is useful for implementing a custom time zone or custom disambiguation behavior, but otherwise `getInstantFor()` should be used instead, because it is more convenient, because it's compatible with the behavior of other methods and libraries, and because it always returns a single value.
For example, during "skipped" clock time like the hour after DST starts in the spring, `getPossibleInstantsFor()` returns an empty array while `getInstantFor()` returns a `Temporal.Instant`.

### timeZone.**getNextTransition**(_startingPoint_: Temporal.Instant | string) : Temporal.Instant

**Parameters:**

- `startingPoint` (`Temporal.Instant` or value convertible to one): Time after which to find the next UTC offset transition.

**Returns:** A `Temporal.Instant` object representing the next UTC offset transition in this time zone, or `null` if no transitions later than `startingPoint` could be found.

This method is used to calculate a possible future UTC offset transition after `startingPoint` for this time zone.
A "transition" is a point in time where the UTC offset of a time zone changes, for example when Daylight Saving Time starts or stops.
Transitions can also be caused by other political changes like a country permanently changing the UTC offset of its time zone.

The returned `Temporal.Instant` will represent the first nanosecond where the new UTC offset is used, not the last nanosecond where the previous UTC offset is used.

When no more transitions are expected, this method will return `null`.
Some time zones (e.g., `Etc/GMT+5` or `-05:00`) have no offset transitions and will return `null` for all values of `startingPoint`.

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method should be overridden if the time zone changes offsets.
Single-offset time zones can use the default implementation which returns `null`.

Example usage:

```javascript
// How long until the next offset change from now, in the current location?
tz = Temporal.Now.timeZone();
now = Temporal.Now.instant();
nextTransition = tz.getNextTransition(now);
duration = nextTransition.since(now);
duration.toLocaleString(); // output will vary
```

### timeZone.**getPreviousTransition**(_startingPoint_: Temporal.Instant | string) : Temporal.Instant

**Parameters:**

- `startingPoint` (`Temporal.Instant` or value convertible to one): Time before which to find the previous UTC offset transition.

**Returns:** A `Temporal.Instant` object representing the previous UTC offset transition in this time zone, or `null` if no transitions earlier than `startingPoint` could be found.

This method is used to calculate a possible past UTC offset transition before `startingPoint` for this time zone.
A "transition" is a point in time where the UTC offset of a time zone changes, for example when Daylight Saving Time starts or stops.
Transitions can also be caused by other political changes like a country permanently changing the UTC offset of its time zone.

The returned `Temporal.Instant` will represent the first nanosecond where the new UTC offset is used, not the last nanosecond where the previous UTC offset is used.

When no previous transitions exist, this method will return `null`.
Some time zones (e.g., `Etc/GMT+5` or `-05:00`) have no offset transitions and will return `null` for all values of `startingPoint`.

If `instant` is not a `Temporal.Instant` object, then it will be converted to one as if it were passed to `Temporal.Instant.from()`.

When subclassing `Temporal.TimeZone`, this method should be overridden if the time zone changes offsets.
Single-offset time zones can use the default implementation which returns `null`.

Example usage:

```javascript
// How long until the previous offset change from now, in the current location?
tz = Temporal.Now.timeZone();
now = Temporal.Now.instant();
previousTransition = tz.getPreviousTransition(now);
duration = now.since(previousTransition);
duration.toLocaleString(); // output will vary
```

### timeZone.**toString**() : string

**Returns:** The string given by `timeZone.id`.
Expand Down
38 changes: 38 additions & 0 deletions docs/zoneddatetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,44 @@ zdt = Temporal.ZonedDateTime.from('2018-11-04T12:00-02:00[America/Sao_Paulo]').s
```
<!-- prettier-ignore-end -->

### zonedDateTime.**getTimeZoneTransition**(direction: string | object) : Temporal.ZonedDateTime | null

**Parameters:**

- `direction` (string | object): A required string or object to control the operation.
A string parameter is treated the same as an object whose `direction` property value is that string.
If an object is passed, the following properties are recognized:
- `direction` (required string): The direction in which to search for the closest UTC offset transition.
Valid values are `'next'` and `'previous'`.

**Returns:** A `Temporal.ZonedDateTime` object representing the following UTC offset transition in `zonedDateTime`'s time zone in the given direction, or `null` if no transitions farther than `zonedDateTime` could be found.

This method is used to calculate the closest past or future UTC offset transition from `zonedDateTime` for its time zone.
A "transition" is a point in time where the UTC offset of a time zone changes, for example when Daylight Saving Time starts or stops.
Transitions can also be caused by other political changes like a country permanently changing the UTC offset of its time zone.

The returned `Temporal.ZonedDateTime` will represent the first nanosecond where the newer UTC offset is used, not the last nanosecond where the previous UTC offset is used.

When no more transitions are expected in the given directoin, this method will return `null`.
Some time zones (e.g., `Etc/GMT+5` or `-05:00`) have no offset transitions.
If `zonedDateTime` has one of these time zones, this method will always return `null`.

Example usage:

```javascript
// How long until the next offset change from now, in the current location?
tz = Temporal.Now.timeZoneId();
now = Temporal.Now.zonedDateTimeISO(tz);
nextTransition = now.getTimeZoneTransition('next');
duration = nextTransition.since(now);
duration.toLocaleString(); // output will vary

// How long until the previous offset change from now, in the current location?
previousTransition = now.getTimeZoneTransition('previous');
duration = now.since(previousTransition);
duration.toLocaleString(); // output will vary
```

### zonedDateTime.**equals**(_other_: Temporal.ZonedDateTime) : boolean

**Parameters:**
Expand Down
10 changes: 6 additions & 4 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,11 @@ export namespace Temporal {
relativeTo?: Temporal.ZonedDateTime | Temporal.PlainDateTime | ZonedDateTimeLike | PlainDateTimeLike | string;
}

/**
* Options to control behaviour of `ZonedDateTime.prototype.getTimeZoneTransition()`
*/
export type TransitionDirection = 'next' | 'previous' | { direction: 'next' | 'previous' };

export type DurationLike = {
years?: number;
months?: number;
Expand Down Expand Up @@ -1094,8 +1099,6 @@ export namespace Temporal {
dateTime: Temporal.PlainDateTime | PlainDateTimeLike | string,
options?: ToInstantOptions
): Temporal.Instant;
getNextTransition?(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
getPreviousTransition?(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
getPossibleInstantsFor(dateTime: Temporal.PlainDateTime | PlainDateTimeLike | string): Temporal.Instant[];
toString?(): string;
toJSON?(): string;
Expand Down Expand Up @@ -1132,8 +1135,6 @@ export namespace Temporal {
dateTime: Temporal.PlainDateTime | PlainDateTimeLike | string,
options?: ToInstantOptions
): Temporal.Instant;
getNextTransition(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
getPreviousTransition(startingPoint: Temporal.Instant | string): Temporal.Instant | null;
getPossibleInstantsFor(dateTime: Temporal.PlainDateTime | PlainDateTimeLike | string): Temporal.Instant[];
toString(): string;
toJSON(): string;
Expand Down Expand Up @@ -1300,6 +1301,7 @@ export namespace Temporal {
roundTo: RoundTo<'day' | 'hour' | 'minute' | 'second' | 'millisecond' | 'microsecond' | 'nanosecond'>
): Temporal.ZonedDateTime;
startOfDay(): Temporal.ZonedDateTime;
getTimeZoneTransition(direction: TransitionDirection): Temporal.ZonedDateTime | null;
toInstant(): Temporal.Instant;
toPlainDateTime(): Temporal.PlainDateTime;
toPlainDate(): Temporal.PlainDate;
Expand Down
5 changes: 5 additions & 0 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,10 @@ export function GetTemporalShowOffsetOption(options) {
return GetOption(options, 'offset', ['auto', 'never'], 'auto');
}

export function GetDirectionOption(options) {
return GetOption(options, 'direction', ['next', 'previous'], REQUIRED);
}

export function GetRoundingIncrementOption(options) {
let increment = options.roundingIncrement;
if (increment === undefined) return 1;
Expand Down Expand Up @@ -5584,6 +5588,7 @@ export function GetOption(options, property, allowedValues, fallback) {
}
return value;
}
if (fallback === REQUIRED) throw new RangeError(`${property} option is required`);
return fallback;
}

Expand Down
30 changes: 0 additions & 30 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,36 +120,6 @@ export class TimeZone {
);
return possibleEpochNs.map((ns) => new Instant(ns));
}
getNextTransition(startingPoint) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
startingPoint = ES.ToTemporalInstant(startingPoint);
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.IsOffsetTimeZoneIdentifier(id) || id === 'UTC') {
return null;
}

let epochNanoseconds = GetSlot(startingPoint, EPOCHNANOSECONDS);
const Instant = GetIntrinsic('%Temporal.Instant%');
epochNanoseconds = ES.GetNamedTimeZoneNextTransition(id, epochNanoseconds);
return epochNanoseconds === null ? null : new Instant(epochNanoseconds);
}
getPreviousTransition(startingPoint) {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
startingPoint = ES.ToTemporalInstant(startingPoint);
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.IsOffsetTimeZoneIdentifier(id) || id === 'UTC') {
return null;
}

let epochNanoseconds = GetSlot(startingPoint, EPOCHNANOSECONDS);
const Instant = GetIntrinsic('%Temporal.Instant%');
epochNanoseconds = ES.GetNamedTimeZonePreviousTransition(id, epochNanoseconds);
return epochNanoseconds === null ? null : new Instant(epochNanoseconds);
}
toString() {
if (!ES.IsTemporalTimeZone(this)) throw new TypeError('invalid receiver');
return GetSlot(this, TIMEZONE_ID);
Expand Down
28 changes: 28 additions & 0 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,34 @@ export class ZonedDateTime {
const instant = ES.GetInstantFor(timeZoneRec, dtStart, 'compatible');
return ES.CreateTemporalZonedDateTime(GetSlot(instant, EPOCHNANOSECONDS), timeZoneRec.receiver, calendar);
}
getTimeZoneTransition(directionParam) {
if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver');
const timeZone = GetSlot(this, TIME_ZONE);
const id = ES.ToTemporalTimeZoneIdentifier(timeZone);

if (directionParam === undefined) throw new TypeError('options parameter is required');
if (ES.Type(directionParam) === 'String') {
const stringParam = directionParam;
directionParam = ObjectCreate(null);
directionParam.direction = stringParam;
} else {
directionParam = ES.GetOptionsObject(directionParam);
}
const direction = ES.GetDirectionOption(directionParam);
if (direction === undefined) throw new TypeError('direction option is required');

// Offset time zones or UTC have no transitions
if (ES.IsOffsetTimeZoneIdentifier(id) || id === 'UTC') {
return null;
}

const thisEpochNanoseconds = GetSlot(this, EPOCHNANOSECONDS);
const epochNanoseconds =
direction === 'next'
? ES.GetNamedTimeZoneNextTransition(id, thisEpochNanoseconds)
: ES.GetNamedTimeZonePreviousTransition(id, thisEpochNanoseconds);
return epochNanoseconds === null ? null : new ZonedDateTime(epochNanoseconds, timeZone, GetSlot(this, CALENDAR));
}
toInstant() {
if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver');
const TemporalInstant = GetIntrinsic('%Temporal.Instant%');
Expand Down
Loading

0 comments on commit b7e4b36

Please sign in to comment.