Skip to content

Commit

Permalink
Refactor calendars to eliminate multiple parent classes
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ptomato committed Nov 16, 2020
1 parent 2c24151 commit ce22f7b
Show file tree
Hide file tree
Showing 17 changed files with 494 additions and 715 deletions.
10 changes: 7 additions & 3 deletions docs/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ date.withCalendar(calendar).add({ months: 1 });
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.
There are two ways to do this.

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.
If you don't implement the optional members, then the base class's default implementations will be used.
The recommended way is to create a class inheriting from `Temporal.Calendar`.
You must use one of the built-in calendars as the "base calendar".
In the class's constructor, call `super()` with the identifier of the base calendar.
The class must override `toString()` to return its own identifier.
Overriding all the other members is optional.
If you don't override the optional members, then they will behave as in the base calendar.

The other, more difficult, way to create a custom calendar is to create a plain object implementing the `Temporal.Calendar` protocol, without subclassing.
The object must implement all of the `Temporal.Calendar` methods except for `fields()`.
The object must implement all of the `Temporal.Calendar` methods except for `id` and `fields()`.
Any object with the required methods will return the correct output from any Temporal property or method.
However, most other code will assume that custom calendars act like built-in `Temporal.Calendar` objects.
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.
Expand Down
442 changes: 186 additions & 256 deletions polyfill/lib/calendar.mjs

Large diffs are not rendered by default.

35 changes: 19 additions & 16 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import ToPrimitive from 'es-abstract/2020/ToPrimitive.js';
import ToString from 'es-abstract/2020/ToString.js';
import Type from 'es-abstract/2020/Type.js';

import { GetISO8601Calendar } from './calendar.mjs';
import { GetIntrinsic } from './intrinsicclass.mjs';
import {
GetSlot,
Expand Down Expand Up @@ -677,7 +676,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(relativeTo) === 'Object') {
if (ES.IsTemporalZonedDateTime(relativeTo) || ES.IsTemporalDateTime(relativeTo)) return relativeTo;
calendar = relativeTo.calendar;
if (calendar === undefined) calendar = GetISO8601Calendar();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, [
'day',
Expand Down Expand Up @@ -721,7 +720,7 @@ export const ES = ObjectAssign({}, ES2020, {
offset
} = ES.ParseISODateTime(ES.ToString(relativeTo), { zoneRequired: false }));
if (ianaName) timeZone = ianaName;
if (!calendar) calendar = GetISO8601Calendar();
if (!calendar) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
}
if (timeZone) {
Expand Down Expand Up @@ -764,7 +763,7 @@ export const ES = ObjectAssign({}, ES2020, {
return new TemporalTime(hour, minute, second, millisecond, microsecond, nanosecond);
}
let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar } = props;
if (!calendar) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (!calendar) calendar = ES.GetISO8601Calendar();
const DATE_ONLY = new RegExp(`^${PARSE.datesplit.source}$`);
const match = DATE_ONLY.exec(str);
if (match) {
Expand Down Expand Up @@ -972,15 +971,15 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalDate(item)) return item;
let calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
const fields = ES.ToTemporalDateFields(item, fieldNames);
return ES.DateFromFields(calendar, fields, constructor, overflow);
}
let { year, month, day, calendar } = ES.ParseTemporalDateString(ES.ToString(item));
({ year, month, day } = ES.RegulateDate(year, month, day, overflow));
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
let result = new constructor(year, month, day, calendar);
if (!ES.IsTemporalDate(result)) throw new TypeError('invalid result');
Expand Down Expand Up @@ -1010,7 +1009,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.IsTemporalDateTime(item)) return item;

calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);

const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
Expand Down Expand Up @@ -1039,7 +1038,7 @@ export const ES = ObjectAssign({}, ES2020, {
nanosecond,
calendar
} = ES.ParseTemporalDateTimeString(ES.ToString(item)));
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
}
const result = new constructor(
Expand Down Expand Up @@ -1113,7 +1112,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalMonthDay(item)) return item;
let calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month']);
const fields = ES.ToTemporalMonthDayFields(item, fieldNames);
Expand All @@ -1122,7 +1121,7 @@ export const ES = ObjectAssign({}, ES2020, {

let { month, day, referenceISOYear, calendar } = ES.ParseTemporalMonthDayString(ES.ToString(item));
({ month, day } = ES.RegulateMonthDay(month, day, overflow));
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
if (referenceISOYear === undefined) referenceISOYear = 1972;
let result = new constructor(month, day, calendar, referenceISOYear);
Expand All @@ -1133,7 +1132,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalTime(item)) return item;
let calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, [
'hour',
Expand All @@ -1159,7 +1158,7 @@ export const ES = ObjectAssign({}, ES2020, {
nanosecond,
overflow
));
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
let result = new constructor(hour, minute, second, millisecond, microsecond, nanosecond, calendar);
if (!ES.IsTemporalTime(result)) throw new TypeError('invalid result');
Expand All @@ -1169,7 +1168,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalYearMonth(item)) return item;
let calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, ['month', 'year']);
const fields = ES.ToTemporalYearMonthFields(item, fieldNames);
Expand All @@ -1178,7 +1177,7 @@ export const ES = ObjectAssign({}, ES2020, {

let { year, month, referenceISODay = 1, calendar } = ES.ParseTemporalYearMonthString(ES.ToString(item));
({ year, month } = ES.RegulateYearMonth(year, month, overflow));
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
let result = new constructor(year, month, calendar, referenceISODay);
if (!ES.IsTemporalYearMonth(result)) throw new TypeError('invalid result');
Expand Down Expand Up @@ -1258,7 +1257,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalZonedDateTime(item)) return item;
calendar = item.calendar;
if (calendar === undefined) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (calendar === undefined) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'year']);
const fields = ES.ToTemporalZonedDateTimeFields(item, fieldNames);
Expand Down Expand Up @@ -1294,7 +1293,7 @@ export const ES = ObjectAssign({}, ES2020, {
} = ES.ParseTemporalZonedDateTimeString(ES.ToString(item)));
if (!ianaName) throw new RangeError('time zone ID required in brackets');
timeZone = ES.TimeZoneFrom(ianaName);
if (!calendar) calendar = new (GetIntrinsic('%Temporal.ISO8601Calendar%'))();
if (!calendar) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
}
let offsetNs = null;
Expand All @@ -1319,6 +1318,10 @@ export const ES = ObjectAssign({}, ES2020, {
return result;
},

GetISO8601Calendar: () => {
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
return new TemporalCalendar('iso8601');
},
CalendarFrom: (calendarLike) => {
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
let from = TemporalCalendar.from;
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/instant.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { DateTimeFormat } from './intl.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
Expand Down Expand Up @@ -259,7 +258,7 @@ export class Instant {
}
}
const timeZone = ES.ToTemporalTimeZone(item);
const calendar = GetISO8601Calendar();
const calendar = ES.GetISO8601Calendar();
const TemporalZonedDateTime = GetIntrinsic('%Temporal.ZonedDateTime%');
return new TemporalZonedDateTime(GetSlot(this, EPOCHNANOSECONDS), timeZone, calendar);
}
Expand Down
5 changes: 2 additions & 3 deletions polyfill/lib/now.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { GetIntrinsic } from './intrinsicclass.mjs';

Expand Down Expand Up @@ -27,7 +26,7 @@ function plainDateTime(calendarLike, temporalTimeZoneLike = timeZone()) {
}
function plainDateTimeISO(temporalTimeZoneLike = timeZone()) {
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
const calendar = GetISO8601Calendar();
const calendar = ES.GetISO8601Calendar();
const inst = instant();
return ES.GetTemporalDateTimeFor(timeZone, inst, calendar);
}
Expand All @@ -39,7 +38,7 @@ function zonedDateTime(calendarLike, temporalTimeZoneLike = timeZone()) {
}
function zonedDateTimeISO(temporalTimeZoneLike = timeZone()) {
const timeZone = ES.ToTemporalTimeZone(temporalTimeZoneLike);
const calendar = GetISO8601Calendar();
const calendar = ES.GetISO8601Calendar();
const ZonedDateTime = GetIntrinsic('%Temporal.ZonedDateTime%');
return new ZonedDateTime(ES.SystemUTCEpochNanoSeconds(), timeZone, calendar);
}
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/plaindate.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { DateTimeFormat } from './intl.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
Expand Down Expand Up @@ -34,7 +33,7 @@ function TemporalDateToString(date, showCalendar = 'auto') {
}

export class PlainDate {
constructor(isoYear, isoMonth, isoDay, calendar = GetISO8601Calendar()) {
constructor(isoYear, isoMonth, isoDay, calendar = ES.GetISO8601Calendar()) {
isoYear = ES.ToInteger(isoYear);
isoMonth = ES.ToInteger(isoMonth);
isoDay = ES.ToInteger(isoDay);
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/plaindatetime.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { DateTimeFormat } from './intl.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
Expand Down Expand Up @@ -75,7 +74,7 @@ export class PlainDateTime {
millisecond = 0,
microsecond = 0,
nanosecond = 0,
calendar = GetISO8601Calendar()
calendar = ES.GetISO8601Calendar()
) {
isoYear = ES.ToInteger(isoYear);
isoMonth = ES.ToInteger(isoMonth);
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/plainmonthday.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

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

export class PlainMonthDay {
constructor(isoMonth, isoDay, calendar = GetISO8601Calendar(), referenceISOYear = 1972) {
constructor(isoMonth, isoDay, calendar = ES.GetISO8601Calendar(), referenceISOYear = 1972) {
isoMonth = ES.ToInteger(isoMonth);
isoDay = ES.ToInteger(isoDay);
calendar = ES.ToTemporalCalendar(calendar);
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/plaintime.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { DateTimeFormat } from './intl.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
Expand Down Expand Up @@ -63,7 +62,7 @@ export class PlainTime {
isoMillisecond = 0,
isoMicrosecond = 0,
isoNanosecond = 0,
calendar = GetISO8601Calendar()
calendar = ES.GetISO8601Calendar()
) {
isoHour = ES.ToInteger(isoHour);
isoMinute = ES.ToInteger(isoMinute);
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/plainyearmonth.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

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

export class PlainYearMonth {
constructor(isoYear, isoMonth, calendar = GetISO8601Calendar(), referenceISODay = 1) {
constructor(isoYear, isoMonth, calendar = ES.GetISO8601Calendar(), referenceISODay = 1) {
isoYear = ES.ToInteger(isoYear);
isoMonth = ES.ToInteger(isoMonth);
calendar = ES.ToTemporalCalendar(calendar);
Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { GetIntrinsic, MakeIntrinsicClass, DefineIntrinsic } from './intrinsicclass.mjs';
import {
Expand Down Expand Up @@ -62,7 +61,7 @@ export class TimeZone {
const offsetNs = ES.GetOffsetNanosecondsFor(this, instant);
return ES.FormatTimeZoneOffsetString(offsetNs);
}
getPlainDateTimeFor(instant, calendar = GetISO8601Calendar()) {
getPlainDateTimeFor(instant, calendar = ES.GetISO8601Calendar()) {
instant = ES.ToTemporalInstant(instant, GetIntrinsic('%Temporal.Instant%'));
calendar = ES.ToTemporalCalendar(calendar);

Expand Down
3 changes: 1 addition & 2 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* global __debug__ */

import { GetISO8601Calendar } from './calendar.mjs';
import { ES } from './ecmascript.mjs';
import { DateTimeFormat } from './intl.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
Expand Down Expand Up @@ -29,7 +28,7 @@ const ArrayPrototypePush = Array.prototype.push;
const ObjectAssign = Object.assign;

export class ZonedDateTime {
constructor(epochNanoseconds, timeZone, calendar = GetISO8601Calendar()) {
constructor(epochNanoseconds, timeZone, calendar = ES.GetISO8601Calendar()) {
epochNanoseconds = ES.ToBigInt(epochNanoseconds);
timeZone = ES.ToTemporalTimeZone(timeZone);
calendar = ES.ToTemporalCalendar(calendar);
Expand Down
27 changes: 16 additions & 11 deletions polyfill/test/regex.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Demitasse from '@pipobscure/demitasse';
const { describe, it, report } = Demitasse;
const { after, before, describe, it, report } = Demitasse;

import Pretty from '@pipobscure/demitasse-pretty';
const { reporter } = Pretty;
Expand Down Expand Up @@ -649,30 +649,35 @@ describe('fromString regex', () => {
});

describe('calendar ID', () => {
function makeCustomCalendar(id) {
return class extends Temporal.Calendar {
constructor() {
super(id);
}
let oldTemporalCalendarFrom = Temporal.Calendar.from;
let fromCalledWith;
before(() => {
Temporal.Calendar.from = function (item) {
fromCalledWith = item;
return new Temporal.Calendar('iso8601');
};
});
function testCalendarID(id) {
return Temporal.PlainDateTime.from(`1970-01-01T00:00[c=${id}]`);
}
describe('valid', () => {
['aaa', 'aaa-aaa', 'eightZZZ', 'eightZZZ-eightZZZ'].forEach((id) => {
it(id, () => {
const Custom = makeCustomCalendar(id);
const calendar = new Custom();
equal(calendar.id, id);
testCalendarID(id);
equal(fromCalledWith, id);
});
});
});
describe('not valid', () => {
['a', 'a-a', 'aa', 'aa-aa', 'foo_', 'foo.', 'ninechars', 'ninechars-ninechars'].forEach((id) => {
it(id, () => {
const Custom = makeCustomCalendar(id);
throws(() => new Custom(), RangeError);
throws(() => testCalendarID(id), RangeError);
});
});
});
after(() => {
Temporal.Calendar.from = oldTemporalCalendarFrom;
});
});
});

Expand Down
Loading

0 comments on commit ce22f7b

Please sign in to comment.