Skip to content

Commit

Permalink
cookbook: Add "Expanded Temporal" example
Browse files Browse the repository at this point in the history
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
ptomato committed Feb 11, 2021
1 parent faceafb commit 6ae165e
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
15 changes: 15 additions & 0 deletions docs/cookbook-expandedyears.md
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}}
```
11 changes: 11 additions & 0 deletions docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions docs/cookbook/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
177 changes: 177 additions & 0 deletions docs/cookbook/makeExpandedTemporal.mjs
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);

0 comments on commit 6ae165e

Please sign in to comment.