From 6ae165e5fe5f43207cec921973f08d8c784ce6bb Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 11 Feb 2021 13:22:26 -0800 Subject: [PATCH] cookbook: Add "Expanded Temporal" example A partial example illustrating how to extend Temporal to support an unlimited range of dates into the future and the past. It goes on its own page because it's much larger than the other cookbook recipes, and it's more for illustrative purposes than that anyone would want to use it. Closes: #604 --- docs/cookbook-expandedyears.md | 15 +++ docs/cookbook.md | 11 ++ docs/cookbook/all.mjs | 1 + docs/cookbook/makeExpandedTemporal.mjs | 177 +++++++++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 docs/cookbook-expandedyears.md create mode 100644 docs/cookbook/makeExpandedTemporal.mjs diff --git a/docs/cookbook-expandedyears.md b/docs/cookbook-expandedyears.md new file mode 100644 index 0000000000..6f5f794882 --- /dev/null +++ b/docs/cookbook-expandedyears.md @@ -0,0 +1,15 @@ +## Expanded years example + +This is an example of an approach to extend Temporal to support arbitrarily-large years (e.g., **+635427810-02-02**) for astronomical purposes. + +The code below is just an example to show how this could be approached. +To do this completely would require adding support to Temporal.Instant and Temporal.ZonedDateTime, and overriding more methods. + +For example, arithmetic will not work correctly in this example. + +> **NOTE**: This is a very specialized use of Temporal and is not something you would normally need to do. +> A dedicated third-party library might be a better solution to this problem. + +```javascript +{{cookbook/makeExpandedTemporal.mjs}} +``` diff --git a/docs/cookbook.md b/docs/cookbook.md index 85bf0d6398..1463f2eca9 100644 --- a/docs/cookbook.md +++ b/docs/cookbook.md @@ -454,3 +454,14 @@ The following example calculates this. ```javascript {{cookbook/bridgePublicHolidays.mjs}} ``` + +## Advanced use cases + +These are not expected to be part of the normal usage of Temporal, but show some unusual things that can be done with Temporal. +Since they are generally larger than these cookbook recipes, they're on their own pages. + +### Extra-expanded years + +Extend Temporal to support arbitrarily-large years (e.g., **+635427810-02-02**) for astronomical purposes. + +→ [Extra-expanded years](cookbook-expandedyears.md) diff --git a/docs/cookbook/all.mjs b/docs/cookbook/all.mjs index d0cc4ce8a7..cb5430e2eb 100644 --- a/docs/cookbook/all.mjs +++ b/docs/cookbook/all.mjs @@ -25,6 +25,7 @@ import './getUtcOffsetStringAtInstant.mjs'; import './getWeeklyDaysInMonth.mjs'; import './legacyDateFromDateTime.mjs'; import './localTimeForFutureEvents.mjs'; +import './makeExpandedTemporal.mjs'; import './nextWeeklyOccurrence.mjs'; import './noonOnDate.mjs'; import './plusAndRoundToMonthStart.mjs'; diff --git a/docs/cookbook/makeExpandedTemporal.mjs b/docs/cookbook/makeExpandedTemporal.mjs new file mode 100644 index 0000000000..ab8a040e07 --- /dev/null +++ b/docs/cookbook/makeExpandedTemporal.mjs @@ -0,0 +1,177 @@ +function bigIntAbs(n) { + if (n < 0n) return -n; + return n; +} + +// The years are unlimited, but for output purposes we assume 10 digits, +// because ISO 8601 requires the expanded year format to pick a consistent +// number of digits. +function formatExpandedYear(year) { + let yearString; + if (year < 1000 || year > 9999) { + let sign = year < 0 ? '-' : '+'; + let yearNumber = bigIntAbs(year); + yearString = sign + `${yearNumber}`.padStart(10, '0'); + } else { + yearString = `${year}`; + } + return yearString; +} + +function isLeapYear(year) { + const isDiv4 = year % 4n === 0n; + const isDiv100 = year % 100n === 0n; + const isDiv400 = year % 400n === 0n; + return isDiv4 && (!isDiv100 || isDiv400); +} + +// This checks to see if the ISO string matches our 10-digit expanded year +// format, and if so, returns both the expanded year as a BigInt, and a new +// ISO string with an in-range year that can be passed to the original +// Temporal string parsing functions. +// The in-range year is 1972 if the expanded year is a leap year, and +// otherwise 1970, so that the rules for February 29 remain correct. +// See the note about the number of digits in formatExpandedYear(). +function parseExpandedYear(isoString) { + const matchExpandedYear = /^[-+\u2212]\d{10}/; + const result = matchExpandedYear.exec(isoString); + if (!result) return { isoString }; + const expandedYear = BigInt(result[0]); + const isoYear = isLeapYear(expandedYear) ? 1972 : 1970; + return { + expandedYear, + isoString: isoString.replace(matchExpandedYear, isoYear.toString()) + }; +} + +// This is a map of Temporal objects to their expanded year (as BigInt). +// The data model consists of the Temporal object (with the ISO year set +// internally to 1970 or 1972) and the expanded year. This map is used to +// associate Temporal objects with their expanded years, instead of defining +// extra properties on the Temporal object. +const expandedYears = new WeakMap(); + +class ExpandedPlainDate extends Temporal.PlainDate { + // The expanded-year versions of the Temporal types are limited to using the + // ISO calendar. + constructor(year, isoMonth, isoDay) { + year = BigInt(year); + const isoYear = isLeapYear(year) ? 1972 : 1970; + super(isoYear, isoMonth, isoDay, 'iso8601'); + expandedYears.set(this, year); + } + + static _convert(plainDate, expandedYear) { + if (plainDate instanceof ExpandedPlainDate) return plainDate; + const f = plainDate.getISOFields(); + return new this(expandedYear, f.isoMonth, f.isoDay); + } + + static from(item) { + if (typeof item === 'string') { + const { expandedYear, isoString } = parseExpandedYear(item); + item = Temporal.PlainDate.from(isoString); + if (expandedYear) return this._convert(item, expandedYear); + } + if (item instanceof Temporal.PlainDate) { + return this._convert(item, BigInt(item.year)); + } + const calendar = Temporal.Calendar.from('iso8601'); + return calendar.dateFromFields(item, undefined, this); + } + + // This overrides the .year property to return the expanded year instead. If + // you were doing this with a calendar, you would instead need to make a + // separate field. (But Instant doesn't have a calendar, so that solution + // wouldn't be able to completely expand Temporal.) + get year() { + return expandedYears.get(this); + } + + toString() { + const year = formatExpandedYear(this.year); + const { isoMonth, isoDay } = this.getISOFields(); + const month = `${isoMonth}`.padStart(2, '0'); + const day = `${isoDay}`.padStart(2, '0'); + return `${year}-${month}-${day}`; + } +} + +class ExpandedPlainDateTime extends Temporal.PlainDateTime { + constructor(year, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond) { + year = BigInt(year); + const isoYear = isLeapYear(year) ? 1972 : 1970; + super(isoYear, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond, 'iso8601'); + expandedYears.set(this, year); + } + + static _convert(plainDateTime, expandedYear) { + if (plainDateTime instanceof ExpandedPlainDateTime) return plainDateTime; + const f = plainDateTime.getISOFields(); + return new this( + expandedYear, + f.isoMonth, + f.isoDay, + f.isoHour, + f.isoMinute, + f.isoSecond, + f.isoMillisecond, + f.isoMicrosecond, + f.isoNanosecond + ); + } + + static from(item) { + if (typeof item === 'string') { + const { expandedYear, isoString } = parseExpandedYear(item); + item = Temporal.PlainDateTime.from(isoString); + if (expandedYear) return this._convert(item, expandedYear); + } + if (item instanceof Temporal.PlainDateTime) { + return this._convert(item, BigInt(item.year)); + } + const calendar = Temporal.Calendar.from('iso8601'); + return calendar.dateFromFields(item, undefined, this); + } + + get year() { + return expandedYears.get(this); + } + + toString(options = {}) { + const dateString = this.toPlainDate().toString({ + ...options, + showCalendar: 'never' + }); + const timeString = this.toPlainTime().toString(options); + return `${dateString}T${timeString}`; + } + + toPlainDate() { + return ExpandedPlainDate._convert(super.toPlainDate(), this.year); + } +} + +class ExpandedPlainTime extends Temporal.PlainTime { + toPlainDateTime(date) { + return ExpandedPlainDateTime._convert(super.toPlainDateTime(date), date.year); + } +} + +function makeExpandedTemporal() { + return { + ...Temporal, + PlainDate: ExpandedPlainDate, + PlainDateTime: ExpandedPlainDateTime, + PlainTime: ExpandedPlainTime + }; +} + +const ExpandedTemporal = makeExpandedTemporal(); + +const date = ExpandedTemporal.PlainDate.from({ year: 635427810, month: 2, day: 2 }); +assert.equal(date.toString(), '+0635427810-02-02'); +const dateTime = ExpandedTemporal.PlainTime.from('10:23').toPlainDateTime(date); +assert.equal(dateTime.toString(), '+0635427810-02-02T10:23:00'); +const dateFromString = ExpandedTemporal.PlainDateTime.from('-0075529144-02-29T12:53:27.55'); +assert.equal(dateFromString.year, -75529144n);