Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Normative: Update calendar data consistency validation #2500

Merged
merged 7 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 40 additions & 14 deletions polyfill/lib/calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const MapPrototypeEntries = Map.prototype.entries;
const MapPrototypeGet = Map.prototype.get;
const MapPrototypeSet = Map.prototype.set;
const SetPrototypeAdd = Set.prototype.add;
const SetPrototypeDelete = Set.prototype.delete;
const SetPrototypeHas = Set.prototype.has;
const SetPrototypeValues = Set.prototype.values;
const SymbolIterator = Symbol.iterator;
const WeakMapPrototypeGet = WeakMap.prototype.get;
Expand Down Expand Up @@ -109,12 +111,24 @@ export class Calendar {
fields(fields) {
if (!ES.IsTemporalCalendar(this)) throw new TypeError('invalid receiver');
const fieldsArray = [];
const allowed = new Set(['year', 'month', 'monthCode', 'day']);
for (const name of fields) {
if (ES.Type(name) !== 'String') throw new TypeError('invalid fields');
if (!allowed.has(name)) throw new RangeError(`invalid field name ${name}`);
allowed.delete(name);
ES.Call(ArrayPrototypePush, fieldsArray, [name]);
const allowed = new OriginalSet(['year', 'month', 'monthCode', 'day']);
const iteratorRecord = ES.GetIterator(fields, 'sync');
const abort = (err) => {
const completion = new ES.CompletionRecord('throw', err);
return ES.IteratorClose(iteratorRecord, completion)['?']();
};
let next = true;
while (next !== false) {
next = ES.IteratorStep(iteratorRecord);
if (next !== false) {
let name = ES.IteratorValue(next);
if (ES.Type(name) !== 'String') return abort(new TypeError('invalid fields'));
if (!ES.Call(SetPrototypeHas, allowed, [name])) {
return abort(new RangeError(`invalid or duplicate field name ${name}`));
}
ES.Call(SetPrototypeDelete, allowed, [name]);
ES.Call(ArrayPrototypePush, fieldsArray, [name]);
}
}
return impl[GetSlot(this, CALENDAR_ID)].fields(fieldsArray);
}
Expand Down Expand Up @@ -311,11 +325,10 @@ impl['iso8601'] = {
if (fields.month !== undefined && fields.year === undefined && fields.monthCode === undefined) {
throw new TypeError('either year or monthCode required with month');
}
const useYear = fields.monthCode === undefined;
const referenceISOYear = 1972;
fields = resolveNonLunisolarMonth(fields);
let { month, day, year } = fields;
({ month, day } = ES.RegulateISODate(useYear ? year : referenceISOYear, month, day, overflow));
({ month, day } = ES.RegulateISODate(year !== undefined ? year : referenceISOYear, month, day, overflow));
return ES.CreateTemporalMonthDay(month, day, calendarSlotValue, referenceISOYear);
},
fields(fields) {
Expand Down Expand Up @@ -1922,10 +1935,21 @@ const helperDangi = ObjectAssign({}, { ...helperChinese, id: 'dangi' });
* ISO and non-ISO implementations vs. code that was very different.
*/
const nonIsoGeneralImpl = {
CalendarFieldDescriptors(type) {
let fieldDescriptors = [];
if (type !== 'month-day') {
fieldDescriptors = [
{ property: 'era', conversion: ES.ToString, required: false },
{ property: 'eraYear', conversion: ES.ToIntegerOrInfinity, required: false }
];
}
return fieldDescriptors;
},
dateFromFields(fields, options, calendarSlotValue) {
const cache = new OneObjectCache();
const fieldNames = this.fields(['day', 'month', 'monthCode', 'year']);
fields = ES.PrepareTemporalFields(fields, fieldNames, []);
const fieldNames = ['day', 'month', 'monthCode', 'year'];
const extraFieldDescriptors = this.CalendarFieldDescriptors('date');
fields = ES.PrepareTemporalFields(fields, fieldNames, [], extraFieldDescriptors);
const overflow = ES.ToTemporalOverflow(options);
const { year, month, day } = this.helper.calendarToIsoDate(fields, overflow, cache);
const result = ES.CreateTemporalDate(year, month, day, calendarSlotValue);
Expand All @@ -1934,8 +1958,9 @@ const nonIsoGeneralImpl = {
},
yearMonthFromFields(fields, options, calendarSlotValue) {
const cache = new OneObjectCache();
const fieldNames = this.fields(['month', 'monthCode', 'year']);
fields = ES.PrepareTemporalFields(fields, fieldNames, []);
const fieldNames = ['month', 'monthCode', 'year'];
const extraFieldDescriptors = this.CalendarFieldDescriptors('year-month');
fields = ES.PrepareTemporalFields(fields, fieldNames, [], extraFieldDescriptors);
const overflow = ES.ToTemporalOverflow(options);
const { year, month, day } = this.helper.calendarToIsoDate({ ...fields, day: 1 }, overflow, cache);
const result = ES.CreateTemporalYearMonth(year, month, calendarSlotValue, /* referenceISODay = */ day);
Expand All @@ -1946,8 +1971,9 @@ const nonIsoGeneralImpl = {
const cache = new OneObjectCache();
// For lunisolar calendars, either `monthCode` or `year` must be provided
// because `month` is ambiguous without a year or a code.
const fieldNames = this.fields(['day', 'month', 'monthCode', 'year']);
fields = ES.PrepareTemporalFields(fields, fieldNames, []);
const fieldNames = ['day', 'month', 'monthCode', 'year'];
const extraFieldDescriptors = this.CalendarFieldDescriptors('date');
fields = ES.PrepareTemporalFields(fields, fieldNames, [], extraFieldDescriptors);
const overflow = ES.ToTemporalOverflow(options);
const { year, month, day } = this.helper.monthDayFromFields(fields, overflow, cache);
// `year` is a reference year where this month/day exists in this calendar
Expand Down
35 changes: 32 additions & 3 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const ArrayIncludes = Array.prototype.includes;
const ArrayPrototypePush = Array.prototype.push;
const ArrayPrototypeSort = Array.prototype.sort;
const ArrayPrototypeFind = Array.prototype.find;
const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat;
const IntlSupportedValuesOf = globalThis.Intl.supportedValuesOf;
const MathAbs = Math.abs;
Expand All @@ -28,13 +29,18 @@ const StringPrototypeSlice = String.prototype.slice;
import bigInt from 'big-integer';
import callBound from 'call-bind/callBound';
import Call from 'es-abstract/2022/Call.js';
import CompletionRecord from 'es-abstract/2022/CompletionRecord.js';
import CreateDataPropertyOrThrow from 'es-abstract/2022/CreateDataPropertyOrThrow.js';
import Get from 'es-abstract/2022/Get.js';
import GetIterator from 'es-abstract/2022/GetIterator.js';
import GetMethod from 'es-abstract/2022/GetMethod.js';
import HasOwnProperty from 'es-abstract/2022/HasOwnProperty.js';
import IsArray from 'es-abstract/2022/IsArray.js';
import IsIntegralNumber from 'es-abstract/2022/IsIntegralNumber.js';
import IsPropertyKey from 'es-abstract/2022/IsPropertyKey.js';
import IteratorClose from 'es-abstract/2022/IteratorClose.js';
import IteratorStep from 'es-abstract/2022/IteratorStep.js';
import IteratorValue from 'es-abstract/2022/IteratorValue.js';
import SameValue from 'es-abstract/2022/SameValue.js';
import ToIntegerOrInfinity from 'es-abstract/2022/ToIntegerOrInfinity.js';
import ToNumber from 'es-abstract/2022/ToNumber.js';
Expand Down Expand Up @@ -186,8 +192,6 @@ const BUILTIN_CASTS = new Map([
['milliseconds', ToIntegerIfIntegral],
['microseconds', ToIntegerIfIntegral],
['nanoseconds', ToIntegerIfIntegral],
['era', ToPrimitiveAndRequireString],
['eraYear', ToIntegerOrInfinity],
['offset', ToPrimitiveAndRequireString]
]);

Expand Down Expand Up @@ -234,9 +238,14 @@ import * as PARSE from './regex.mjs';

export {
Call,
CompletionRecord,
GetIterator,
GetMethod,
HasOwnProperty,
IsIntegralNumber,
IteratorClose,
IteratorStep,
IteratorValue,
ToIntegerOrInfinity,
ToNumber,
ToObject,
Expand Down Expand Up @@ -1105,11 +1114,21 @@ export function PrepareTemporalFields(
bag,
fields,
requiredFields,
extraFieldDescriptors = [],
duplicateBehaviour = 'throw',
{ emptySourceErrorMessage = 'no supported properties found' } = {}
) {
const result = ObjectCreate(null);
let any = false;
if (extraFieldDescriptors) {
for (let index = 0; index < extraFieldDescriptors.length; index++) {
let desc = extraFieldDescriptors[index];
Call(ArrayPrototypePush, fields, [desc.property]);
if (desc.required === true && requiredFields !== 'partial') {
Call(ArrayPrototypePush, requiredFields, [desc.property]);
}
}
}
Call(ArrayPrototypeSort, fields, []);
let previousProperty = undefined;
for (let index = 0; index < fields.length; index++) {
Expand All @@ -1123,6 +1142,14 @@ export function PrepareTemporalFields(
any = true;
if (BUILTIN_CASTS.has(property)) {
value = BUILTIN_CASTS.get(property)(value);
} else if (extraFieldDescriptors) {
const matchingDescriptor = Call(ArrayPrototypeFind, extraFieldDescriptors, [
(desc) => desc.property === property
]);
if (matchingDescriptor) {
const convertor = matchingDescriptor.conversion;
value = convertor(value);
}
}
result[property] = value;
} else if (requiredFields !== 'partial') {
Expand All @@ -1145,7 +1172,9 @@ export function PrepareTemporalFields(

export function ToTemporalTimeRecord(bag, completeness = 'complete') {
const fields = ['hour', 'microsecond', 'millisecond', 'minute', 'nanosecond', 'second'];
const partial = PrepareTemporalFields(bag, fields, 'partial', { emptySourceErrorMessage: 'invalid time-like' });
const partial = PrepareTemporalFields(bag, fields, 'partial', undefined, undefined, {
emptySourceErrorMessage: 'invalid time-like'
});
const result = {};
for (let index = 0; index < fields.length; index++) {
const field = fields[index];
Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/plainmonthday.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class PlainMonthDay {
const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []);
let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields);
const concatenatedFieldNames = ES.Call(ArrayPrototypeConcat, receiverFieldNames, inputFieldNames);
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore');
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], [], 'ignore');
const options = ObjectCreate(null);
options.overflow = 'reject';
return ES.CalendarDateFromFields(calendar, mergedFields, options);
Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/plainyearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class PlainYearMonth {
const inputFields = ES.PrepareTemporalFields(item, inputFieldNames, []);
let mergedFields = ES.CalendarMergeFields(calendar, fields, inputFields);
const concatenatedFieldNames = ES.Call(ArrayPrototypeConcat, receiverFieldNames, inputFieldNames);
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], 'ignore');
mergedFields = ES.PrepareTemporalFields(mergedFields, concatenatedFieldNames, [], [], 'ignore');
const options = ObjectCreate(null);
options.overflow = 'reject';
return ES.CalendarDateFromFields(calendar, mergedFields, options);
Expand Down
51 changes: 41 additions & 10 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,7 @@ <h1>
_fields_: an Object,
_fieldNames_: a List of property names,
_requiredFields_: ~partial~ or a List of property names,
optional _extraFieldDescriptors_: a List of Calendar Field Descriptor Records,
optional _duplicateBehaviour_: ~throw~ or ~ignore~,
): either a normal completion containing an Object, or an abrupt completion
</h1>
Expand All @@ -1813,12 +1814,19 @@ <h1>
The returned Object has a null prototype, and an own data property for each element of _fieldNames_ that corresponds with a non-*undefined* property of the same name on _fields_ used as the input for relevant conversion.
When _requiredFields_ is ~partial~, this operation throws if none of the properties are present with a non-*undefined* value.
When _requiredFields_ is a List, this operation throws if any of the properties named by it are absent or undefined, and otherwise substitutes a relevant default for any absent or undefined non-required property (ensuring that the returned object has a property for each element of _fieldNames_).
When _extraFieldDescriptors_ is present, its contents are treated as an extension of <emu-xref href="#table-temporal-field-requirements"></emu-xref>.
</dd>
</dl>
<emu-alg>
1. If _duplicateBehaviour_ is not present, set _duplicateBehaviour_ to ~throw~.
1. Let _result_ be OrdinaryObjectCreate(*null*).
1. Let _any_ be *false*.
1. If _extraFieldDescriptors_ is present, then
1. For each Calendar Field Descriptor Record _desc_ of _extraFieldDescriptors_, do
1. Assert: _fieldNames_ does not contain _desc_.[[Property]].
1. Append _desc_.[[Property]] to _fieldNames_.
1. If _desc_.[[Required]] is *true* and _requiredFields_ is a List, then
1. Append _desc_.[[Property]] to _requiredFields_.
1. Let _sortedFieldNames_ be SortStringListByCodeUnit(_fieldNames_).
1. Let _previousProperty_ be *undefined*.
1. For each property name _property_ of _sortedFieldNames_, do
Expand All @@ -1841,6 +1849,9 @@ <h1>
1. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
1. Set _value_ to ? ToPrimitive(_value_, ~string~).
1. If _value_ is not a String, throw a *TypeError* exception.
1. Else if _extraFieldDescriptors_ is present and _extraFieldDescriptors_ contains a Calendar Field Descriptor Record _desc_ such that _desc_.[[Property]] is _property_, then
1. Let _converter_ be _desc_.[[Conversion]].
1. Set _value_ to ? _converter_(_value_).
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
1. Else if _requiredFields_ is a List, then
1. If _requiredFields_ contains _property_, then
Expand Down Expand Up @@ -1921,16 +1932,6 @@ <h1>
<td>~ToPrimitiveAndRequireString~</td>
<td>*undefined*</td>
</tr>
<tr>
<td>*"era"*</td>
<td>~ToPrimitiveAndRequireString~</td>
<td>*undefined*</td>
</tr>
<tr>
<td>*"eraYear"*</td>
<td>~ToIntegerWithTruncation~</td>
<td>*undefined*</td>
</tr>
<tr>
<td>*"timeZone"*</td>
<td></td>
Expand All @@ -1939,6 +1940,36 @@ <h1>
</tbody>
</table>
</emu-table>

<emu-clause id="sec-temporal-calendar-field-descriptor-record">
<h1>Calendar Field Descriptor Record</h1>
<p>A <dfn variants="Calendar Field Descriptor Records">Calendar Field Descriptor Record</dfn> is a Record value used to describe a calendar-specific field for use in creating and interacting with instances of Temporal types.</p>
<p>Calendar Field Descriptor Records have the fields listed in <emu-xref href="#table-temporal-calendar-field-descriptor-record"></emu-xref>.</p>
<emu-table id="table-temporal-calendar-field-descriptor-record" caption="Calendar Field Descriptor Record Fields">
<table>
<tr>
<th>Field Name</th>
<th>Value</th>
<th>Meaning</th>
</tr>
<tr>
<td>[[Property]]</td>
<td>a String</td>
<td>The property name associated with the field, analogous to the Property column of <emu-xref href="#table-temporal-field-requirements"></emu-xref>.</td>
</tr>
<tr>
<td>[[Conversion]]</td>
<td>an Abstract Closure accepting a single ECMAScript language value and returning either a normal completion containing an ECMAScript language value representing purely static data or a throw completion.</td>
<td>The means by which purported values are coerced to a static representation of the correct type (or rejected if that fails), analogous to steps indicated by the Conversion column of <emu-xref href="#table-temporal-field-requirements"></emu-xref>.</td>
</tr>
<tr>
<td>[[Required]]</td>
<td>a Boolean</td>
<td>Whether PrepareTemporalFields should consider the field as required when not accepting partial data.</td>
</tr>
</table>
</emu-table>
</emu-clause>
</emu-clause>

<emu-clause id="sec-temporal-getdifferencesettings" type="abstract operation">
Expand Down
Loading