-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
4 changed files
with
204 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |