Skip to content

Commit

Permalink
Add Calendar.fields method
Browse files Browse the repository at this point in the history
The Calendar.fields method is called whenever Temporal needs to determine
if a calendar object requires extra fields to uniquely identify its date.

Closes: #666
  • Loading branch information
ptomato committed Oct 19, 2020
1 parent cb7d1e6 commit 744711a
Show file tree
Hide file tree
Showing 15 changed files with 435 additions and 323 deletions.
7 changes: 7 additions & 0 deletions docs/calendar-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ class Temporal.Calendar {
/** A string identifier for this calendar */
id : string;

fields(
fields: array<string>
) : array<string>;

//////////////////
// Arithmetic //
//////////////////
Expand Down Expand Up @@ -151,6 +155,9 @@ get foo(...args) {
Calendars can add additional *calendar-specific accessors*, such as the year type ("kesidran", "chaser", "maleh") in the Hebrew calendar, and may add conforming accessor methods to Temporal.Date.prototype.
If any of these accessors are needed for constructing a Temporal.Date from fields, then the calendar should implement `fields()` which, given an array of field names in the ISO calendar, returns an array of equivalent field names in the calendar.
We are not aware of this being necessary for any built-in calendars.
An instance of `MyCalendar` is *expected* to have stateless behavior; i.e., calling a method with the same arguments should return the same result each time. There would be no mechanism for enforcing that user-land calendars are stateless; the calendar author should test this expectation on their own in order to prevent unexpected behavior such as the lack of round-tripping.
### Enumerable Properties
Expand Down
22 changes: 22 additions & 0 deletions docs/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,28 @@ Temporal.Calendar.from('chinese').dateDifference(
) // => P1M2D
```

### calendar.**fields**(fields: array<string>) : array<string>

**Parameters:**

- `fields` (array of strings): A list of field names.

**Returns:** a new list of field names.

This method does not need to be called directly except in specialized code.
It is called indirectly when using the `from()` static methods and `with()` methods of `Temporal.DateTime`, `Temporal.Date`, and `Temporal.YearMonth`.

Custom calendars should override this method if they require more fields with which to denote the date than the standard `era`, `year`, `month`, and `day`.
The input array contains the field names that are necessary for a particular operation (for example, `'month'` and `'day'` for `Temporal.MonthDay.prototype.with()`), and the method should make a copy of the array and add whichever extra fields are necessary.

Usage example:

```js
// In built-in calendars, this method just makes a copy of the input array
Temporal.Calendar.from('iso8601').fields(['month', 'day']);
// => ['month', 'day']
```

### calendar.**toString**() : string

**Returns:** The string given by `calendar.id`.
Expand Down
2 changes: 2 additions & 0 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ export namespace Temporal {
| /** @deprecated */ 'day'
>
): Temporal.Duration;
fields?(fields: Array<string>): Array<string>;
}

/**
Expand Down Expand Up @@ -465,6 +466,7 @@ export namespace Temporal {
| /** @deprecated */ 'day'
>
): Temporal.Duration;
fields(fields: Array<string>): Array<string>;
toString(): string;
}

Expand Down
10 changes: 7 additions & 3 deletions polyfill/lib/calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class Calendar {
void constructor;
throw new Error('not implemented');
}
fields(fields) {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
return ES.CreateListFromArrayLike(fields, ['String']);
}
dateAdd(date, duration, options, constructor) {
void date;
void duration;
Expand Down Expand Up @@ -137,23 +141,23 @@ class ISO8601Calendar extends Calendar {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
options = ES.NormalizeOptionsObject(options);
const overflow = ES.ToTemporalOverflow(options);
let { year, month, day } = ES.ToTemporalDateRecord(fields);
let { year, month, day } = ES.ToRecord(fields, [['day'], ['month'], ['year']]);
({ year, month, day } = ES.RegulateDate(year, month, day, overflow));
return new constructor(year, month, day, this);
}
yearMonthFromFields(fields, options, constructor) {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
options = ES.NormalizeOptionsObject(options);
const overflow = ES.ToTemporalOverflow(options);
let { year, month } = ES.ToTemporalYearMonthRecord(fields);
let { year, month } = ES.ToRecord(fields, [['month'], ['year']]);
({ year, month } = ES.RegulateYearMonth(year, month, overflow));
return new constructor(year, month, this, /* referenceISODay = */ 1);
}
monthDayFromFields(fields, options, constructor) {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
options = ES.NormalizeOptionsObject(options);
const overflow = ES.ToTemporalOverflow(options);
let { month, day } = ES.ToTemporalMonthDayRecord(fields);
let { month, day } = ES.ToRecord(fields, [['day'], ['month']]);
({ month, day } = ES.RegulateMonthDay(month, day, overflow));
return new constructor(month, day, this, /* referenceISOYear = */ 1972);
}
Expand Down
18 changes: 10 additions & 8 deletions polyfill/lib/date.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,12 @@ export class Date {
calendar = GetSlot(this, CALENDAR);
source = this;
}
const props = ES.ToPartialRecord(temporalDateLike, ['day', 'era', 'month', 'year']);
const fieldNames = calendar.fields(['day', 'era', 'month', 'year']);
const props = ES.ToPartialRecord(temporalDateLike, fieldNames);
if (!props) {
throw new TypeError('invalid date-like');
}
const fields = ES.ToTemporalDateRecord(source);
const fields = ES.ToTemporalDateFields(source, calendar);
ObjectAssign(fields, props);
const Construct = ES.SpeciesConstructor(this, Date);
const result = calendar.dateFromFields(fields, options, Construct);
Expand Down Expand Up @@ -269,20 +270,21 @@ export class Date {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const YearMonth = GetIntrinsic('%Temporal.YearMonth%');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateRecord(this);
const fields = ES.ToTemporalDateFields(this, calendar);
return calendar.yearMonthFromFields(fields, {}, YearMonth);
}
toMonthDay() {
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const MonthDay = GetIntrinsic('%Temporal.MonthDay%');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateRecord(this);
const fields = ES.ToTemporalDateFields(this, calendar);
return calendar.monthDayFromFields(fields, {}, MonthDay);
}
getFields() {
const fields = ES.ToTemporalDateRecord(this);
if (!fields) throw new TypeError('invalid receiver');
fields.calendar = GetSlot(this, CALENDAR);
if (!ES.IsTemporalDate(this)) throw new TypeError('invalid receiver');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateFields(this, calendar);
fields.calendar = calendar;
return fields;
}
getISOFields() {
Expand Down Expand Up @@ -310,7 +312,7 @@ export class Date {
let calendar = item.calendar;
if (calendar === undefined) calendar = GetISO8601Calendar();
calendar = TemporalCalendar.from(calendar);
const fields = ES.ToTemporalDateRecord(item);
const fields = ES.ToTemporalDateFields(item, calendar);
result = calendar.dateFromFields(fields, options, this);
}
} else {
Expand Down
18 changes: 10 additions & 8 deletions polyfill/lib/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export class DateTime {
calendar = GetSlot(this, CALENDAR);
source = this;
}
const props = ES.ToPartialRecord(temporalDateTimeLike, [
const fieldNames = calendar.fields([
'day',
'era',
'hour',
Expand All @@ -200,10 +200,11 @@ export class DateTime {
'second',
'year'
]);
const props = ES.ToPartialRecord(temporalDateTimeLike, fieldNames);
if (!props) {
throw new TypeError('invalid date-time-like');
}
const fields = ES.ToTemporalDateTimeRecord(source);
const fields = ES.ToTemporalDateTimeFields(source, calendar);
ObjectAssign(fields, props);
const date = calendar.dateFromFields(fields, options, GetIntrinsic('%Temporal.Date%'));
let year = GetSlot(date, ISO_YEAR);
Expand Down Expand Up @@ -591,24 +592,25 @@ export class DateTime {
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
const YearMonth = GetIntrinsic('%Temporal.YearMonth%');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateTimeRecord(this);
const fields = ES.ToTemporalDateTimeFields(this, calendar);
return calendar.yearMonthFromFields(fields, {}, YearMonth);
}
toMonthDay() {
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
const MonthDay = GetIntrinsic('%Temporal.MonthDay%');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateTimeRecord(this);
const fields = ES.ToTemporalDateTimeFields(this, calendar);
return calendar.monthDayFromFields(fields, {}, MonthDay);
}
toTime() {
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
return ES.TemporalDateTimeToTime(this);
}
getFields() {
const fields = ES.ToTemporalDateTimeRecord(this);
if (!fields) throw new TypeError('invalid receiver');
fields.calendar = GetSlot(this, CALENDAR);
if (!ES.IsTemporalDateTime(this)) throw new TypeError('invalid receiver');
const calendar = GetSlot(this, CALENDAR);
const fields = ES.ToTemporalDateTimeFields(this, calendar);
fields.calendar = calendar;
return fields;
}
getISOFields() {
Expand Down Expand Up @@ -649,7 +651,7 @@ export class DateTime {
let calendar = item.calendar;
if (calendar === undefined) calendar = GetISO8601Calendar();
calendar = TemporalCalendar.from(calendar);
const fields = ES.ToTemporalDateTimeRecord(item);
const fields = ES.ToTemporalDateTimeFields(item, calendar);
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const date = calendar.dateFromFields(fields, options, TemporalDate);
const year = GetSlot(date, ISO_YEAR);
Expand Down
102 changes: 91 additions & 11 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const ArrayIsArray = Array.isArray;
const ArrayPrototypeIndexOf = Array.prototype.indexOf;
const ArrayPrototypePush = Array.prototype.push;
const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat;
const MathAbs = Math.abs;
const MathCeil = Math.ceil;
Expand All @@ -10,8 +13,10 @@ const ObjectCreate = Object.create;

import bigInt from 'big-integer';
import Call from 'es-abstract/2020/Call.js';
import IsPropertyKey from 'es-abstract/2020/IsPropertyKey.js';
import SpeciesConstructor from 'es-abstract/2020/SpeciesConstructor.js';
import ToInteger from 'es-abstract/2020/ToInteger.js';
import ToLength from 'es-abstract/2020/ToLength.js';
import ToNumber from 'es-abstract/2020/ToNumber.js';
import ToPrimitive from 'es-abstract/2020/ToPrimitive.js';
import ToString from 'es-abstract/2020/ToString.js';
Expand Down Expand Up @@ -61,8 +66,10 @@ import * as PARSE from './regex.mjs';

const ES2020 = {
Call,
IsPropertyKey,
SpeciesConstructor,
ToInteger,
ToLength,
ToNumber,
ToPrimitive,
ToString,
Expand Down Expand Up @@ -581,7 +588,7 @@ export const ES = ObjectAssign({}, ES2020, {
calendar = relativeTo.calendar;
if (calendar === undefined) calendar = GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
const fields = ES.ToTemporalDateTimeRecord(relativeTo);
const fields = ES.ToTemporalDateTimeFields(relativeTo, calendar);
const TemporalDate = GetIntrinsic('%Temporal.Date%');
const date = calendar.dateFromFields(fields, {}, TemporalDate);
year = GetSlot(date, ISO_YEAR);
Expand Down Expand Up @@ -679,11 +686,20 @@ export const ES = ObjectAssign({}, ES2020, {
return result;
},
// field access in the following operations is intentionally alphabetical
ToTemporalDateRecord: (bag) => {
return ES.ToRecord(bag, [['day'], ['era', undefined], ['month'], ['year']]);
ToTemporalDateFields: (bag, calendar) => {
const fieldNames = calendar.fields(['day', 'era', 'month', 'year']);
const entries = [['day'], ['era', undefined], ['month'], ['year']];
// Add extra fields from the calendar at the end
fieldNames.forEach((fieldName) => {
if (!entries.some(([name]) => name === fieldName)) {
entries.push([fieldName, undefined]);
}
});
return ES.ToRecord(bag, entries);
},
ToTemporalDateTimeRecord: (bag) => {
return ES.ToRecord(bag, [
ToTemporalDateTimeFields: (bag, calendar) => {
const fieldNames = calendar.fields(['day', 'era', 'month', 'year']);
const entries = [
['day'],
['era', undefined],
['hour', 0],
Expand All @@ -694,19 +710,42 @@ export const ES = ObjectAssign({}, ES2020, {
['nanosecond', 0],
['second', 0],
['year']
]);
},
ToTemporalMonthDayRecord: (bag) => {
return ES.ToRecord(bag, [['day'], ['month']]);
];
// Add extra fields from the calendar at the end
fieldNames.forEach((fieldName) => {
if (!entries.some(([name]) => name === fieldName)) {
entries.push([fieldName, undefined]);
}
});
return ES.ToRecord(bag, entries);
},
ToTemporalMonthDayFields: (bag, calendar) => {
const fieldNames = calendar.fields(['day', 'month']);
const entries = [['day'], ['month']];
// Add extra fields from the calendar at the end
fieldNames.forEach((fieldName) => {
if (!entries.some(([name]) => name === fieldName)) {
entries.push([fieldName, undefined]);
}
});
return ES.ToRecord(bag, entries);
},
ToTemporalTimeRecord: (bag) => {
const props = ES.ToPartialRecord(bag, ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second']);
if (!props) throw new TypeError('invalid time-like');
const { hour = 0, minute = 0, second = 0, millisecond = 0, microsecond = 0, nanosecond = 0 } = props;
return { hour, minute, second, millisecond, microsecond, nanosecond };
},
ToTemporalYearMonthRecord: (bag) => {
return ES.ToRecord(bag, [['era', undefined], ['month'], ['year']]);
ToTemporalYearMonthFields: (bag, calendar) => {
const fieldNames = calendar.fields(['era', 'month', 'year']);
const entries = [['era', undefined], ['month'], ['year']];
// Add extra fields from the calendar at the end
fieldNames.forEach((fieldName) => {
if (!entries.some(([name]) => name === fieldName)) {
entries.push([fieldName, undefined]);
}
});
return ES.ToRecord(bag, entries);
},
CalendarFrom: (calendarLike) => {
const TemporalCalendar = GetIntrinsic('%Temporal.Calendar%');
Expand Down Expand Up @@ -2237,6 +2276,47 @@ export const ES = ObjectAssign({}, ES2020, {
throw new RangeError(`${property} must be between ${minimum} and ${maximum}, not ${value}`);
}
return MathFloor(value);
},
// Overridden because the es-abstract version unconditionally uses util.inspect
Get: (O, P) => {
if (Type(O) !== 'Object') {
throw new TypeError('Assertion failed: Type(O) is not Object');
}
if (!ES.IsPropertyKey(P)) {
throw new TypeError(`Assertion failed: IsPropertyKey(P) is not true, got ${P}`);
}
return O[P];
},
LengthOfArrayLike: (obj) => {
if (Type(obj) !== 'Object') {
throw new TypeError('Assertion failed: `obj` must be an Object');
}
return ES.ToLength(ES.Get(obj, 'length'));
},
CreateListFromArrayLike: (
obj,
elementTypes = ['Undefined', 'Null', 'Boolean', 'String', 'Symbol', 'Number', 'Object']
) => {
if (ES.Type(obj) !== 'Object') {
throw new TypeError('Assertion failed: `obj` must be an Object');
}
if (!ArrayIsArray(elementTypes)) {
throw new TypeError('Assertion failed: `elementTypes`, if provided, must be an array');
}
var len = ES.LengthOfArrayLike(obj);
var list = [];
var index = 0;
while (index < len) {
var indexName = ToString(index);
var next = ES.Get(obj, indexName);
var nextType = Type(next);
if (ArrayPrototypeIndexOf.call(elementTypes, nextType) < 0) {
throw new TypeError(`item type ${nextType} is not a valid elementType`);
}
ArrayPrototypePush.call(list, next);
index += 1;
}
return list;
}
});

Expand Down
Loading

0 comments on commit 744711a

Please sign in to comment.