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);