Skip to content

Commit 5364a09

Browse files
committed
Refactor calendars to eliminate multiple parent classes
We had originally decided to go with a scheme where the value of the [[Identifier]] internal slot determines the behaviour of the calendar methods, and there are not individual parent classes for each built-in calendar. This implements that scheme. Merges most of the spec text of ISO8601Calendar into Calendar. Closes: #847 See: #300
1 parent ecc61fd commit 5364a09

17 files changed

+494
-715
lines changed

docs/calendar.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ date.withCalendar(calendar).add({ months: 1 });
4141
For specialized applications where you need to do calculations in a calendar system that is not supported by Intl, you can implement a custom calendar.
4242
There are two ways to do this.
4343

44-
The recommended way is to create a class inheriting from `Temporal.Calendar`, call `super()` in the constructor with a calendar identifier string, and implement all the members except `id`, `toString()`, and `fields()`, which are optional.
45-
If you don't implement the optional members, then the base class's default implementations will be used.
44+
The recommended way is to create a class inheriting from `Temporal.Calendar`.
45+
You must use one of the built-in calendars as the "base calendar".
46+
In the class's constructor, call `super()` with the identifier of the base calendar.
47+
The class must override `toString()` to return its own identifier.
48+
Overriding all the other members is optional.
49+
If you don't override the optional members, then they will behave as in the base calendar.
4650

4751
The other, more difficult, way to create a custom calendar is to create a plain object implementing the `Temporal.Calendar` protocol, without subclassing.
48-
The object must implement all of the `Temporal.Calendar` methods except for `fields()`.
52+
The object must implement all of the `Temporal.Calendar` methods except for `id` and `fields()`.
4953
Any object with the required methods will return the correct output from any Temporal property or method.
5054
However, most other code will assume that custom calendars act like built-in `Temporal.Calendar` objects.
5155
To interoperate with libraries or other code that you didn't write, then you should implement the `id` property and the `fields()` method as well.

polyfill/lib/calendar.mjs

+186-256
Large diffs are not rendered by default.

polyfill/lib/ecmascript.mjs

+19-16
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import ToPrimitive from 'es-abstract/2020/ToPrimitive.js';
2121
import ToString from 'es-abstract/2020/ToString.js';
2222
import Type from 'es-abstract/2020/Type.js';
2323

24-
import { GetISO8601Calendar } from './calendar.mjs';
2524
import { GetIntrinsic } from './intrinsicclass.mjs';
2625
import {
2726
GetSlot,
@@ -677,7 +676,7 @@ export const ES = ObjectAssign({}, ES2020, {
677676
if (ES.Type(relativeTo) === 'Object') {
678677
if (ES.IsTemporalZonedDateTime(relativeTo) || ES.IsTemporalDateTime(relativeTo)) return relativeTo;
679678
calendar = relativeTo.calendar;
680-
if (calendar === undefined) calendar = GetISO8601Calendar();
679+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
681680
calendar = ES.ToTemporalCalendar(calendar);
682681
const fieldNames = ES.CalendarFields(calendar, [
683682
'day',
@@ -721,7 +720,7 @@ export const ES = ObjectAssign({}, ES2020, {
721720
offset
722721
} = ES.ParseISODateTime(ES.ToString(relativeTo), { zoneRequired: false }));
723722
if (ianaName) timeZone = ianaName;
724-
if (!calendar) calendar = GetISO8601Calendar();
723+
if (!calendar) calendar = ES.GetISO8601Calendar();
725724
calendar = ES.ToTemporalCalendar(calendar);
726725
}
727726
if (timeZone) {
@@ -764,7 +763,7 @@ export const ES = ObjectAssign({}, ES2020, {
764763
return new TemporalTime(hour, minute, second, millisecond, microsecond, nanosecond);
765764
}
766765
let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar } = props;
767-
if (!calendar) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
766+
if (!calendar) calendar = ES.GetISO8601Calendar();
768767
const DATE_ONLY = new RegExp(`^${PARSE.datesplit.source}$`);
769768
const match = DATE_ONLY.exec(str);
770769
if (match) {
@@ -972,15 +971,15 @@ export const ES = ObjectAssign({}, ES2020, {
972971
if (ES.Type(item) === 'Object') {
973972
if (ES.IsTemporalDate(item)) return item;
974973
let calendar = item.calendar;
975-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
974+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
976975
calendar = ES.ToTemporalCalendar(calendar);
977976
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
978977
const fields = ES.ToTemporalDateFields(item, fieldNames);
979978
return ES.DateFromFields(calendar, fields, constructor, overflow);
980979
}
981980
let { year, month, day, calendar } = ES.ParseTemporalDateString(ES.ToString(item));
982981
({ year, month, day } = ES.RegulateDate(year, month, day, overflow));
983-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
982+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
984983
calendar = ES.ToTemporalCalendar(calendar);
985984
let result = new constructor(year, month, day, calendar);
986985
if (!ES.IsTemporalDate(result)) throw new TypeError('invalid result');
@@ -1010,7 +1009,7 @@ export const ES = ObjectAssign({}, ES2020, {
10101009
if (ES.IsTemporalDateTime(item)) return item;
10111010

10121011
calendar = item.calendar;
1013-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1012+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
10141013
calendar = ES.ToTemporalCalendar(calendar);
10151014

10161015
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
@@ -1039,7 +1038,7 @@ export const ES = ObjectAssign({}, ES2020, {
10391038
nanosecond,
10401039
calendar
10411040
} = ES.ParseTemporalDateTimeString(ES.ToString(item)));
1042-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1041+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
10431042
calendar = ES.ToTemporalCalendar(calendar);
10441043
}
10451044
const result = new constructor(
@@ -1113,7 +1112,7 @@ export const ES = ObjectAssign({}, ES2020, {
11131112
if (ES.Type(item) === 'Object') {
11141113
if (ES.IsTemporalMonthDay(item)) return item;
11151114
let calendar = item.calendar;
1116-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1115+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11171116
calendar = ES.ToTemporalCalendar(calendar);
11181117
const fieldNames = ES.CalendarFields(calendar, ['day', 'month']);
11191118
const fields = ES.ToTemporalMonthDayFields(item, fieldNames);
@@ -1122,7 +1121,7 @@ export const ES = ObjectAssign({}, ES2020, {
11221121

11231122
let { month, day, referenceISOYear, calendar } = ES.ParseTemporalMonthDayString(ES.ToString(item));
11241123
({ month, day } = ES.RegulateMonthDay(month, day, overflow));
1125-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1124+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11261125
calendar = ES.ToTemporalCalendar(calendar);
11271126
if (referenceISOYear === undefined) referenceISOYear = 1972;
11281127
let result = new constructor(month, day, calendar, referenceISOYear);
@@ -1133,7 +1132,7 @@ export const ES = ObjectAssign({}, ES2020, {
11331132
if (ES.Type(item) === 'Object') {
11341133
if (ES.IsTemporalTime(item)) return item;
11351134
let calendar = item.calendar;
1136-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1135+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11371136
calendar = ES.ToTemporalCalendar(calendar);
11381137
const fieldNames = ES.CalendarFields(calendar, [
11391138
'hour',
@@ -1159,7 +1158,7 @@ export const ES = ObjectAssign({}, ES2020, {
11591158
nanosecond,
11601159
overflow
11611160
));
1162-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1161+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11631162
calendar = ES.ToTemporalCalendar(calendar);
11641163
let result = new constructor(hour, minute, second, millisecond, microsecond, nanosecond, calendar);
11651164
if (!ES.IsTemporalTime(result)) throw new TypeError('invalid result');
@@ -1169,7 +1168,7 @@ export const ES = ObjectAssign({}, ES2020, {
11691168
if (ES.Type(item) === 'Object') {
11701169
if (ES.IsTemporalYearMonth(item)) return item;
11711170
let calendar = item.calendar;
1172-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1171+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11731172
calendar = ES.ToTemporalCalendar(calendar);
11741173
const fieldNames = ES.CalendarFields(calendar, ['month', 'year']);
11751174
const fields = ES.ToTemporalYearMonthFields(item, fieldNames);
@@ -1178,7 +1177,7 @@ export const ES = ObjectAssign({}, ES2020, {
11781177

11791178
let { year, month, referenceISODay = 1, calendar } = ES.ParseTemporalYearMonthString(ES.ToString(item));
11801179
({ year, month } = ES.RegulateYearMonth(year, month, overflow));
1181-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1180+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
11821181
calendar = ES.ToTemporalCalendar(calendar);
11831182
let result = new constructor(year, month, calendar, referenceISODay);
11841183
if (!ES.IsTemporalYearMonth(result)) throw new TypeError('invalid result');
@@ -1258,7 +1257,7 @@ export const ES = ObjectAssign({}, ES2020, {
12581257
if (ES.Type(item) === 'Object') {
12591258
if (ES.IsTemporalZonedDateTime(item)) return item;
12601259
calendar = item.calendar;
1261-
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1260+
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
12621261
calendar = ES.ToTemporalCalendar(calendar);
12631262
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
12641263
const fields = ES.ToTemporalZonedDateTimeFields(item, fieldNames);
@@ -1294,7 +1293,7 @@ export const ES = ObjectAssign({}, ES2020, {
12941293
} = ES.ParseTemporalZonedDateTimeString(ES.ToString(item)));
12951294
if (!ianaName) throw new RangeError('time zone ID required in brackets');
12961295
timeZone = ES.TimeZoneFrom(ianaName);
1297-
if (!calendar) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
1296+
if (!calendar) calendar = ES.GetISO8601Calendar();
12981297
calendar = ES.ToTemporalCalendar(calendar);
12991298
}
13001299
let offsetNs = null;
@@ -1319,6 +1318,10 @@ export const ES = ObjectAssign({}, ES2020, {
13191318
return result;
13201319
},
13211320

1321+
GetISO8601Calendar: () => {
1322+
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
1323+
return new TemporalCalendar('iso8601');
1324+
},
13221325
CalendarFrom: (calendarLike) => {
13231326
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
13241327
let from = TemporalCalendar.from;

polyfill/lib/instant.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -259,7 +258,7 @@ export class Instant {
259258
}
260259
}
261260
const timeZone = ES.ToTemporalTimeZone(item);
262-
const calendar = GetISO8601Calendar();
261+
const calendar = ES.GetISO8601Calendar();
263262
const TemporalZonedDateTime = GetIntrinsic('%Temporal.ZonedDateTime%');
264263
return new TemporalZonedDateTime(GetSlot(this, EPOCHNANOSECONDS), timeZone, calendar);
265264
}

polyfill/lib/now.mjs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { GetISO8601Calendar } from './calendar.mjs';
21
import { ES } from './ecmascript.mjs';
32
import { GetIntrinsic } from './intrinsicclass.mjs';
43

@@ -27,7 +26,7 @@ function plainDateTime(calendarLike, temporalTimeZoneLike = timeZone()) {
2726
}
2827
function plainDateTimeISO(temporalTimeZoneLike = timeZone()) {
2928
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
30-
const calendar = GetISO8601Calendar();
29+
const calendar = ES.GetISO8601Calendar();
3130
const inst = instant();
3231
return ES.GetTemporalDateTimeFor(timeZone, inst, calendar);
3332
}
@@ -39,7 +38,7 @@ function zonedDateTime(calendarLike, temporalTimeZoneLike = timeZone()) {
3938
}
4039
function zonedDateTimeISO(temporalTimeZoneLike = timeZone()) {
4140
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
42-
const calendar = GetISO8601Calendar();
41+
const calendar = ES.GetISO8601Calendar();
4342
const ZonedDateTime = GetIntrinsic('%Temporal.ZonedDateTime%');
4443
return new ZonedDateTime(ES.SystemUTCEpochNanoSeconds(), timeZone, calendar);
4544
}

polyfill/lib/plaindate.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -34,7 +33,7 @@ function TemporalDateToString(date, showCalendar = 'auto') {
3433
}
3534

3635
export class PlainDate {
37-
constructor(isoYear, isoMonth, isoDay, calendar = GetISO8601Calendar()) {
36+
constructor(isoYear, isoMonth, isoDay, calendar = ES.GetISO8601Calendar()) {
3837
isoYear = ES.ToInteger(isoYear);
3938
isoMonth = ES.ToInteger(isoMonth);
4039
isoDay = ES.ToInteger(isoDay);

polyfill/lib/plaindatetime.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -75,7 +74,7 @@ export class PlainDateTime {
7574
millisecond = 0,
7675
microsecond = 0,
7776
nanosecond = 0,
78-
calendar = GetISO8601Calendar()
77+
calendar = ES.GetISO8601Calendar()
7978
) {
8079
isoYear = ES.ToInteger(isoYear);
8180
isoMonth = ES.ToInteger(isoMonth);

polyfill/lib/plainmonthday.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -24,7 +23,7 @@ function MonthDayToString(monthDay, showCalendar = 'auto') {
2423
}
2524

2625
export class PlainMonthDay {
27-
constructor(isoMonth, isoDay, calendar = GetISO8601Calendar(), referenceISOYear = 1972) {
26+
constructor(isoMonth, isoDay, calendar = ES.GetISO8601Calendar(), referenceISOYear = 1972) {
2827
isoMonth = ES.ToInteger(isoMonth);
2928
isoDay = ES.ToInteger(isoDay);
3029
calendar = ES.ToTemporalCalendar(calendar);

polyfill/lib/plaintime.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -63,7 +62,7 @@ export class PlainTime {
6362
isoMillisecond = 0,
6463
isoMicrosecond = 0,
6564
isoNanosecond = 0,
66-
calendar = GetISO8601Calendar()
65+
calendar = ES.GetISO8601Calendar()
6766
) {
6867
isoHour = ES.ToInteger(isoHour);
6968
isoMinute = ES.ToInteger(isoMinute);

polyfill/lib/plainyearmonth.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -24,7 +23,7 @@ function YearMonthToString(yearMonth, showCalendar = 'auto') {
2423
}
2524

2625
export class PlainYearMonth {
27-
constructor(isoYear, isoMonth, calendar = GetISO8601Calendar(), referenceISODay = 1) {
26+
constructor(isoYear, isoMonth, calendar = ES.GetISO8601Calendar(), referenceISODay = 1) {
2827
isoYear = ES.ToInteger(isoYear);
2928
isoMonth = ES.ToInteger(isoMonth);
3029
calendar = ES.ToTemporalCalendar(calendar);

polyfill/lib/timezone.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { GetIntrinsic, MakeIntrinsicClass, DefineIntrinsic } from './intrinsicclass.mjs';
65
import {
@@ -62,7 +61,7 @@ export class TimeZone {
6261
const offsetNs = ES.GetOffsetNanosecondsFor(this, instant);
6362
return ES.FormatTimeZoneOffsetString(offsetNs);
6463
}
65-
getPlainDateTimeFor(instant, calendar = GetISO8601Calendar()) {
64+
getPlainDateTimeFor(instant, calendar = ES.GetISO8601Calendar()) {
6665
instant = ES.ToTemporalInstant(instant, GetIntrinsic('%Temporal.Instant%'));
6766
calendar = ES.ToTemporalCalendar(calendar);
6867

polyfill/lib/zoneddatetime.mjs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* global __debug__ */
22

3-
import { GetISO8601Calendar } from './calendar.mjs';
43
import { ES } from './ecmascript.mjs';
54
import { DateTimeFormat } from './intl.mjs';
65
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
@@ -29,7 +28,7 @@ const ArrayPrototypePush = Array.prototype.push;
2928
const ObjectAssign = Object.assign;
3029

3130
export class ZonedDateTime {
32-
constructor(epochNanoseconds, timeZone, calendar = GetISO8601Calendar()) {
31+
constructor(epochNanoseconds, timeZone, calendar = ES.GetISO8601Calendar()) {
3332
epochNanoseconds = ES.ToBigInt(epochNanoseconds);
3433
timeZone = ES.ToTemporalTimeZone(timeZone);
3534
calendar = ES.ToTemporalCalendar(calendar);

polyfill/test/regex.mjs

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Demitasse from '@pipobscure/demitasse';
2-
const { describe, it, report } = Demitasse;
2+
const { after, before, describe, it, report } = Demitasse;
33

44
import Pretty from '@pipobscure/demitasse-pretty';
55
const { reporter } = Pretty;
@@ -649,30 +649,35 @@ describe('fromString regex', () => {
649649
});
650650

651651
describe('calendar ID', () => {
652-
function makeCustomCalendar(id) {
653-
return class extends Temporal.Calendar {
654-
constructor() {
655-
super(id);
656-
}
652+
let oldTemporalCalendarFrom = Temporal.Calendar.from;
653+
let fromCalledWith;
654+
before(() => {
655+
Temporal.Calendar.from = function (item) {
656+
fromCalledWith = item;
657+
return new Temporal.Calendar('iso8601');
657658
};
659+
});
660+
function testCalendarID(id) {
661+
return Temporal.PlainDateTime.from(`1970-01-01T00:00[c=${id}]`);
658662
}
659663
describe('valid', () => {
660664
['aaa', 'aaa-aaa', 'eightZZZ', 'eightZZZ-eightZZZ'].forEach((id) => {
661665
it(id, () => {
662-
const Custom = makeCustomCalendar(id);
663-
const calendar = new Custom();
664-
equal(calendar.id, id);
666+
testCalendarID(id);
667+
equal(fromCalledWith, id);
665668
});
666669
});
667670
});
668671
describe('not valid', () => {
669672
['a', 'a-a', 'aa', 'aa-aa', 'foo_', 'foo.', 'ninechars', 'ninechars-ninechars'].forEach((id) => {
670673
it(id, () => {
671-
const Custom = makeCustomCalendar(id);
672-
throws(() => new Custom(), RangeError);
674+
throws(() => testCalendarID(id), RangeError);
673675
});
674676
});
675677
});
678+
after(() => {
679+
Temporal.Calendar.from = oldTemporalCalendarFrom;
680+
});
676681
});
677682
});
678683

0 commit comments

Comments
 (0)