Skip to content

Commit 515655e

Browse files
ptomatoMs2ger
authored andcommitted
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
1 parent 1c6fab3 commit 515655e

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

docs/cookbook-expandedyears.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Expanded years example
2+
3+
This is an example of an approach to extend Temporal to support arbitrarily-large years (e.g., **+635427810-02-02**) for astronomical purposes.
4+
5+
The code below is just an example to show how this could be approached.
6+
To do this completely would require adding support to Temporal.Instant and Temporal.ZonedDateTime, and overriding more methods.
7+
8+
For example, arithmetic will not work correctly in this example.
9+
10+
> **NOTE**: This is a very specialized use of Temporal and is not something you would normally need to do.
11+
> A dedicated third-party library might be a better solution to this problem.
12+
13+
```javascript
14+
{{cookbook/makeExpandedTemporal.mjs}}
15+
```

docs/cookbook.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,14 @@ The following example calculates this.
454454
```javascript
455455
{{cookbook/bridgePublicHolidays.mjs}}
456456
```
457+
458+
## Advanced use cases
459+
460+
These are not expected to be part of the normal usage of Temporal, but show some unusual things that can be done with Temporal.
461+
Since they are generally larger than these cookbook recipes, they're on their own pages.
462+
463+
### Extra-expanded years
464+
465+
Extend Temporal to support arbitrarily-large years (e.g., **+635427810-02-02**) for astronomical purposes.
466+
467+
[Extra-expanded years](cookbook-expandedyears.md)

docs/cookbook/all.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import './getUtcOffsetStringAtInstant.mjs';
2525
import './getWeeklyDaysInMonth.mjs';
2626
import './legacyDateFromDateTime.mjs';
2727
import './localTimeForFutureEvents.mjs';
28+
import './makeExpandedTemporal.mjs';
2829
import './nextWeeklyOccurrence.mjs';
2930
import './noonOnDate.mjs';
3031
import './plusAndRoundToMonthStart.mjs';
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
function bigIntAbs(n) {
2+
if (n < 0n) return -n;
3+
return n;
4+
}
5+
6+
// The years are unlimited, but for output purposes we assume 10 digits,
7+
// because ISO 8601 requires the expanded year format to pick a consistent
8+
// number of digits.
9+
function formatExpandedYear(year) {
10+
let yearString;
11+
if (year < 1000 || year > 9999) {
12+
let sign = year < 0 ? '-' : '+';
13+
let yearNumber = bigIntAbs(year);
14+
yearString = sign + `${yearNumber}`.padStart(10, '0');
15+
} else {
16+
yearString = `${year}`;
17+
}
18+
return yearString;
19+
}
20+
21+
function isLeapYear(year) {
22+
const isDiv4 = year % 4n === 0n;
23+
const isDiv100 = year % 100n === 0n;
24+
const isDiv400 = year % 400n === 0n;
25+
return isDiv4 && (!isDiv100 || isDiv400);
26+
}
27+
28+
// This checks to see if the ISO string matches our 10-digit expanded year
29+
// format, and if so, returns both the expanded year as a BigInt, and a new
30+
// ISO string with an in-range year that can be passed to the original
31+
// Temporal string parsing functions.
32+
// The in-range year is 1972 if the expanded year is a leap year, and
33+
// otherwise 1970, so that the rules for February 29 remain correct.
34+
// See the note about the number of digits in formatExpandedYear().
35+
function parseExpandedYear(isoString) {
36+
const matchExpandedYear = /^[-+\u2212]\d{10}/;
37+
const result = matchExpandedYear.exec(isoString);
38+
if (!result) return { isoString };
39+
const expandedYear = BigInt(result[0]);
40+
const isoYear = isLeapYear(expandedYear) ? 1972 : 1970;
41+
return {
42+
expandedYear,
43+
isoString: isoString.replace(matchExpandedYear, isoYear.toString())
44+
};
45+
}
46+
47+
// This is a map of Temporal objects to their expanded year (as BigInt).
48+
// The data model consists of the Temporal object (with the ISO year set
49+
// internally to 1970 or 1972) and the expanded year. This map is used to
50+
// associate Temporal objects with their expanded years, instead of defining
51+
// extra properties on the Temporal object.
52+
const expandedYears = new WeakMap();
53+
54+
class ExpandedPlainDate extends Temporal.PlainDate {
55+
// The expanded-year versions of the Temporal types are limited to using the
56+
// ISO calendar.
57+
constructor(year, isoMonth, isoDay) {
58+
year = BigInt(year);
59+
const isoYear = isLeapYear(year) ? 1972 : 1970;
60+
super(isoYear, isoMonth, isoDay, 'iso8601');
61+
expandedYears.set(this, year);
62+
}
63+
64+
static _convert(plainDate, expandedYear) {
65+
if (plainDate instanceof ExpandedPlainDate) return plainDate;
66+
const f = plainDate.getISOFields();
67+
return new this(expandedYear, f.isoMonth, f.isoDay);
68+
}
69+
70+
static from(item) {
71+
if (typeof item === 'string') {
72+
const { expandedYear, isoString } = parseExpandedYear(item);
73+
item = Temporal.PlainDate.from(isoString);
74+
if (expandedYear) return this._convert(item, expandedYear);
75+
}
76+
if (item instanceof Temporal.PlainDate) {
77+
return this._convert(item, BigInt(item.year));
78+
}
79+
const calendar = Temporal.Calendar.from('iso8601');
80+
return calendar.dateFromFields(item, undefined, this);
81+
}
82+
83+
// This overrides the .year property to return the expanded year instead. If
84+
// you were doing this with a calendar, you would instead need to make a
85+
// separate field. (But Instant doesn't have a calendar, so that solution
86+
// wouldn't be able to completely expand Temporal.)
87+
get year() {
88+
return expandedYears.get(this);
89+
}
90+
91+
toString() {
92+
const year = formatExpandedYear(this.year);
93+
const { isoMonth, isoDay } = this.getISOFields();
94+
const month = `${isoMonth}`.padStart(2, '0');
95+
const day = `${isoDay}`.padStart(2, '0');
96+
return `${year}-${month}-${day}`;
97+
}
98+
}
99+
100+
class ExpandedPlainDateTime extends Temporal.PlainDateTime {
101+
constructor(year, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond) {
102+
year = BigInt(year);
103+
const isoYear = isLeapYear(year) ? 1972 : 1970;
104+
super(isoYear, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond, 'iso8601');
105+
expandedYears.set(this, year);
106+
}
107+
108+
static _convert(plainDateTime, expandedYear) {
109+
if (plainDateTime instanceof ExpandedPlainDateTime) return plainDateTime;
110+
const f = plainDateTime.getISOFields();
111+
return new this(
112+
expandedYear,
113+
f.isoMonth,
114+
f.isoDay,
115+
f.isoHour,
116+
f.isoMinute,
117+
f.isoSecond,
118+
f.isoMillisecond,
119+
f.isoMicrosecond,
120+
f.isoNanosecond
121+
);
122+
}
123+
124+
static from(item) {
125+
if (typeof item === 'string') {
126+
const { expandedYear, isoString } = parseExpandedYear(item);
127+
item = Temporal.PlainDateTime.from(isoString);
128+
if (expandedYear) return this._convert(item, expandedYear);
129+
}
130+
if (item instanceof Temporal.PlainDateTime) {
131+
return this._convert(item, BigInt(item.year));
132+
}
133+
const calendar = Temporal.Calendar.from('iso8601');
134+
return calendar.dateFromFields(item, undefined, this);
135+
}
136+
137+
get year() {
138+
return expandedYears.get(this);
139+
}
140+
141+
toString(options = {}) {
142+
const dateString = this.toPlainDate().toString({
143+
...options,
144+
showCalendar: 'never'
145+
});
146+
const timeString = this.toPlainTime().toString(options);
147+
return `${dateString}T${timeString}`;
148+
}
149+
150+
toPlainDate() {
151+
return ExpandedPlainDate._convert(super.toPlainDate(), this.year);
152+
}
153+
}
154+
155+
class ExpandedPlainTime extends Temporal.PlainTime {
156+
toPlainDateTime(date) {
157+
return ExpandedPlainDateTime._convert(super.toPlainDateTime(date), date.year);
158+
}
159+
}
160+
161+
function makeExpandedTemporal() {
162+
return {
163+
...Temporal,
164+
PlainDate: ExpandedPlainDate,
165+
PlainDateTime: ExpandedPlainDateTime,
166+
PlainTime: ExpandedPlainTime
167+
};
168+
}
169+
170+
const ExpandedTemporal = makeExpandedTemporal();
171+
172+
const date = ExpandedTemporal.PlainDate.from({ year: 635427810, month: 2, day: 2 });
173+
assert.equal(date.toString(), '+0635427810-02-02');
174+
const dateTime = ExpandedTemporal.PlainTime.from('10:23').toPlainDateTime(date);
175+
assert.equal(dateTime.toString(), '+0635427810-02-02T10:23:00');
176+
const dateFromString = ExpandedTemporal.PlainDateTime.from('-0075529144-02-29T12:53:27.55');
177+
assert.equal(dateFromString.year, -75529144n);

0 commit comments

Comments
 (0)