diff --git a/docs/cookbook.md b/docs/cookbook.md index 28403a991..f9ee7e521 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -129,6 +129,15 @@ Here's an example of rounding a time _down_ to the previously occurring whole ho ## Time zone conversion +### Preserving local time + +Map a zoneless date and time of day into a `Temporal.Absolute` instance at which the local date and time of day in a specified time zone matches it. +This is easily done with `dateTime.inTimeZone()`, but here is an example of implementing different disambiguation behaviours than the `"earlier"`, `"later"`, and `"reject'` ones built in to Temporal. + +```javascript +{{cookbook/getInstantWithLocalTimeInZone.mjs}} +``` + ### Preserving absolute instant Map a zoned date and time of day into a string serialization of the local time in a target zone at the corresponding instant in absolute time. diff --git a/docs/cookbook/all.mjs b/docs/cookbook/all.mjs index 87fee8e0a..c9445f4f6 100644 --- a/docs/cookbook/all.mjs +++ b/docs/cookbook/all.mjs @@ -12,6 +12,7 @@ import './getElapsedDurationSinceInstant.mjs'; import './getFirstTuesdayOfMonth.mjs'; import './getInstantBeforeOldRecord.mjs'; import './getInstantOfNearestOffsetTransitionToInstant.mjs'; +import './getInstantWithLocalTimeInZone.mjs'; import './getLocalizedArrival.mjs'; import './getParseableZonedStringAtInstant.mjs'; import './getParseableZonedStringWithLocalTimeInOtherZone.mjs'; diff --git a/docs/cookbook/getInstantWithLocalTimeInZone.mjs b/docs/cookbook/getInstantWithLocalTimeInZone.mjs new file mode 100644 index 000000000..bf8db69e6 --- /dev/null +++ b/docs/cookbook/getInstantWithLocalTimeInZone.mjs @@ -0,0 +1,92 @@ +/** + * Get an absolute time corresponding with a calendar date / wall-clock time in + * a particular time zone, the same as Temporal.TimeZone.getAbsoluteFor() or + * Temporal.DateTime.inTimeZone(), but with more disambiguation options. + * + * As well as the default Temporal disambiguation options 'earlier', 'later', + * and 'reject', there are additional options possible: + * + * - 'earlierLater': Same as what the Moment Timezone and Luxon libraries do; + * equivalent to 'earlier' when turning the clock back, and 'later' when + * setting the clock forward. + * - 'clipEarlier': Equivalent to 'earlier' when turning the clock back, and + * when setting the clock forward returns the time just before the clock + * changes. + * - 'clipLater': Equivalent to 'later' when turning the clock back, and when + * setting the clock forward returns the exact time of the clock change. + * + * @param {Temporal.DateTime} dateTime - Calendar date and wall-clock time to + * convert + * @param {Temporal.TimeZone} timeZone - Time zone in which to consider the + * wall-clock time + * @param {string} disambiguation - Disambiguation mode, see description. + * @returns {Temporal.Absolute} Absolute time in timeZone at the time of the + * calendar date and wall-clock time from dateTime + */ +function getInstantWithLocalTimeInZone(dateTime, timeZone, disambiguation = 'earlier') { + // Handle the built-in modes first + if (['earlier', 'later', 'reject'].includes(disambiguation)) { + return timeZone.getAbsoluteFor(dateTime, { disambiguation }); + } + + const possible = timeZone.getPossibleAbsolutesFor(dateTime); + + // Return only possibility if no disambiguation needed + if (possible.length === 1) return possible[0]; + + switch (disambiguation) { + case 'earlierLater': + if (possible.length === 0) return timeZone.getAbsoluteFor(dateTime, { disambiguation: 'later' }); + return possible[0]; + case 'clipEarlier': + if (possible.length === 0) { + const before = dateTime.inTimeZone(timeZone, { disambiguation: 'earlier' }); + return timeZone + .getTransitions(before) + .next() + .value.minus({ nanoseconds: 1 }); + } + return possible[0]; + case 'clipLater': + if (possible.length === 0) { + const before = dateTime.inTimeZone(timeZone, { disambiguation: 'earlier' }); + return timeZone.getTransitions(before).next().value; + } + return possible[1]; + } + throw new RangeError(`invalid disambiguation ${disambiguation}`); +} + +const germany = Temporal.TimeZone.from('Europe/Berlin'); +const nonexistentGermanWallTime = Temporal.DateTime.from('2019-03-31T02:45'); + +const germanResults = { + earlier: /* */ '2019-03-31T01:45+01:00[Europe/Berlin]', + later: /* */ '2019-03-31T03:45+02:00[Europe/Berlin]', + earlierLater: /**/ '2019-03-31T03:45+02:00[Europe/Berlin]', + clipEarlier: /* */ '2019-03-31T01:59:59.999999999+01:00[Europe/Berlin]', + clipLater: /* */ '2019-03-31T03:00+02:00[Europe/Berlin]' +}; +for (const [disambiguation, result] of Object.entries(germanResults)) { + assert.equal( + getInstantWithLocalTimeInZone(nonexistentGermanWallTime, germany, disambiguation).toString(germany), + result + ); +} + +const brazilEast = Temporal.TimeZone.from('America/Sao_Paulo'); +const doubleEasternBrazilianWallTime = Temporal.DateTime.from('2019-02-16T23:45'); + +const brazilianResults = { + earlier: /* */ '2019-02-16T23:45-02:00[America/Sao_Paulo]', + later: /* */ '2019-02-16T23:45-03:00[America/Sao_Paulo]', + earlierLater: /**/ '2019-02-16T23:45-02:00[America/Sao_Paulo]', + clipEarlier: /* */ '2019-02-16T23:45-02:00[America/Sao_Paulo]', + clipLater: /* */ '2019-02-16T23:45-03:00[America/Sao_Paulo]' +}; +for (const [disambiguation, result] of Object.entries(brazilianResults)) { + assert.equal( + getInstantWithLocalTimeInZone(doubleEasternBrazilianWallTime, brazilEast, disambiguation).toString(brazilEast), + result + ); +}