Skip to content

Commit

Permalink
Normative: Copy options object in {Plain,Zoned}DateTime.{from,p.with}
Browse files Browse the repository at this point in the history
Following the precedent set in #2447, if we're going to pass the options
object to a calendar method we should make a copy of it. Also flatten the
'options' property once it's read and converted to a string in
InterpretTemporalDateTimeFields, so that it doesn't have to be observably
converted to a string again in Calendar.p.dateFromFields().

In PlainDateTime.from, delay validation of the options until after
validation of the ISO string, for consistency with ZonedDateTime.from and
in accordance with our general principle of validating arguments in order.

This affects the following APIs, which are all callers of
InterpretTemporalDateTimeFields:
- Temporal.PlainDateTime.from()
- Temporal.PlainDateTime.prototype.with()
- Temporal.ZonedDateTime.from()
- Temporal.ZonedDateTime.prototype.with()

It does not affect ToRelativeTemporalObject, even though that also calls
InterpretTemporalDateTimeFields, because it does not take an options
object from userland.
  • Loading branch information
ptomato committed Aug 11, 2023
1 parent cc1a782 commit c8e4dab
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 32 deletions.
24 changes: 14 additions & 10 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,7 @@ export function ToTemporalDate(item, options) {
export function InterpretTemporalDateTimeFields(calendar, fields, options) {
let { hour, minute, second, millisecond, microsecond, nanosecond } = ToTemporalTimeRecord(fields);
const overflow = ToTemporalOverflow(options);
options.overflow = overflow; // options is always an internal object, so not observable
const date = CalendarDateFromFields(calendar, fields, options);
const year = GetSlot(date, ISO_YEAR);
const month = GetSlot(date, ISO_MONTH);
Expand All @@ -1210,14 +1211,16 @@ export function InterpretTemporalDateTimeFields(calendar, fields, options) {

export function ToTemporalDateTime(item, options) {
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar;
const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null);

if (Type(item) === 'Object') {
if (IsTemporalDateTime(item)) return item;
if (IsTemporalZonedDateTime(item)) {
ToTemporalOverflow(options); // validate and ignore
ToTemporalOverflow(resolvedOptions); // validate and ignore
return GetPlainDateTimeFor(GetSlot(item, TIME_ZONE), GetSlot(item, INSTANT), GetSlot(item, CALENDAR));
}
if (IsTemporalDate(item)) {
ToTemporalOverflow(options); // validate and ignore
ToTemporalOverflow(resolvedOptions); // validate and ignore
return CreateTemporalDateTime(
GetSlot(item, ISO_YEAR),
GetSlot(item, ISO_MONTH),
Expand All @@ -1239,10 +1242,9 @@ export function ToTemporalDateTime(item, options) {
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields(
calendar,
fields,
options
resolvedOptions
));
} else {
ToTemporalOverflow(options); // validate and ignore
let z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } =
ParseTemporalDateTimeString(RequireString(item)));
Expand All @@ -1251,6 +1253,7 @@ export function ToTemporalDateTime(item, options) {
if (!calendar) calendar = 'iso8601';
if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`);
calendar = ASCIILowercase(calendar);
ToTemporalOverflow(resolvedOptions); // validate and ignore
}
return CreateTemporalDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar);
}
Expand Down Expand Up @@ -1456,6 +1459,7 @@ export function InterpretISODateTimeOffset(

export function ToTemporalZonedDateTime(item, options) {
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timeZone, offset, calendar;
const resolvedOptions = SnapshotOwnProperties(GetOptionsObject(options), null);
let disambiguation, offsetOpt;
let matchMinute = false;
let offsetBehaviour = 'option';
Expand All @@ -1479,12 +1483,12 @@ export function ToTemporalZonedDateTime(item, options) {
if (offset === undefined) {
offsetBehaviour = 'wall';
}
disambiguation = ToTemporalDisambiguation(options);
offsetOpt = ToTemporalOffset(options, 'reject');
disambiguation = ToTemporalDisambiguation(resolvedOptions);
offsetOpt = ToTemporalOffset(resolvedOptions, 'reject');
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = InterpretTemporalDateTimeFields(
calendar,
fields,
options
resolvedOptions
));
} else {
let tzAnnotation, z;
Expand Down Expand Up @@ -1513,9 +1517,9 @@ export function ToTemporalZonedDateTime(item, options) {
if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`);
calendar = ASCIILowercase(calendar);
matchMinute = true; // ISO strings may specify offset with less precision
disambiguation = ToTemporalDisambiguation(options);
offsetOpt = ToTemporalOffset(options, 'reject');
ToTemporalOverflow(options); // validate and ignore
disambiguation = ToTemporalDisambiguation(resolvedOptions);
offsetOpt = ToTemporalOffset(resolvedOptions, 'reject');
ToTemporalOverflow(resolvedOptions); // validate and ignore
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(offset);
Expand Down
4 changes: 2 additions & 2 deletions polyfill/lib/plaindatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class PlainDateTime {
}
ES.RejectTemporalLikeObject(temporalDateTimeLike);

options = ES.GetOptionsObject(options);
const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null);
const calendar = GetSlot(this, CALENDAR);
const fieldNames = ES.CalendarFields(calendar, ['day', 'month', 'monthCode', 'year']);
let fields = ES.PrepareTemporalFields(this, fieldNames, []);
Expand All @@ -168,7 +168,7 @@ export class PlainDateTime {
fields = ES.CalendarMergeFields(calendar, fields, partialDateTime);
fields = ES.PrepareTemporalFields(fields, fieldNames, []);
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
ES.InterpretTemporalDateTimeFields(calendar, fields, resolvedOptions);

return ES.CreateTemporalDateTime(
year,
Expand Down
8 changes: 4 additions & 4 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class ZonedDateTime {
throw new TypeError('invalid zoned-date-time-like');
}
ES.RejectTemporalLikeObject(temporalZonedDateTimeLike);
options = ES.GetOptionsObject(options);
const resolvedOptions = ES.SnapshotOwnProperties(ES.GetOptionsObject(options), null);

const calendar = GetSlot(this, CALENDAR);
const timeZone = GetSlot(this, TIME_ZONE);
Expand Down Expand Up @@ -208,11 +208,11 @@ export class ZonedDateTime {
fields = ES.CalendarMergeFields(calendar, fields, partialZonedDateTime);
fields = ES.PrepareTemporalFields(fields, fieldNames, ['offset']);

const disambiguation = ES.ToTemporalDisambiguation(options);
const offset = ES.ToTemporalOffset(options, 'prefer');
const disambiguation = ES.ToTemporalDisambiguation(resolvedOptions);
const offset = ES.ToTemporalOffset(resolvedOptions, 'prefer');

let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
ES.InterpretTemporalDateTimeFields(calendar, fields, resolvedOptions);
const newOffsetNs = ES.ParseDateTimeUTCOffset(fields.offset);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
Expand Down
16 changes: 10 additions & 6 deletions spec/plaindatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ <h1>Temporal.PlainDateTime.prototype.with ( _temporalDateTimeLike_ [ , _options_
1. If Type(_temporalDateTimeLike_) is not Object, then
1. Throw a *TypeError* exception.
1. Perform ? RejectTemporalLikeObject(_temporalDateTimeLike_).
1. Set _options_ to ? GetOptionsObject(_options_).
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. Let _calendar_ be _dateTime_.[[Calendar]].
1. Let _fieldNames_ be ? CalendarFields(_calendar_, « *"day"*, *"month"*, *"monthCode"*, *"year"* »).
1. Let _fields_ be ? PrepareTemporalFields(_dateTime_, _fieldNames_, «»).
Expand All @@ -412,7 +412,7 @@ <h1>Temporal.PlainDateTime.prototype.with ( _temporalDateTimeLike_ [ , _options_
1. Let _partialDateTime_ be ? PrepareTemporalFields(_temporalDateTimeLike_, _fieldNames_, ~partial~).
1. Set _fields_ to ? CalendarMergeFields(_calendar_, _fields_, _partialDateTime_).
1. Set _fields_ to ? PrepareTemporalFields(_fields_, _fieldNames_, «»).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _resolvedOptions_).
1. Assert: IsValidISODate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
1. Assert: IsValidTime(_result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]) is *true*.
1. Return ? CreateTemporalDateTime(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _calendar_).
Expand Down Expand Up @@ -953,8 +953,11 @@ <h1>InterpretTemporalDateTimeFields ( _calendar_, _fields_, _options_ )</h1>
The abstract operation InterpretTemporalDateTimeFields interprets the date/time fields in the object _fields_ using the given _calendar_, and returns a Record with the fields according to the ISO 8601 calendar.
</p>
<emu-alg>
1. Assert: _options_ is an ordinary extensible Object that is not directly observable from ECMAScript code and for which the value of the [[Prototype]] internal slot is *null* and every property is a configurable data property.
1. Let _timeResult_ be ? ToTemporalTimeRecord(_fields_).
1. Let _overflow_ be ? ToTemporalOverflow(_options_).
1. NOTE: The following step is guaranteed to complete normally despite the *"overflow"* property existing, because of the assertion in the first step.
1. Perform ! CreateDataPropertyOrThrow(_options_, *"overflow"*, _overflow_).
1. Let _temporalDate_ be ? CalendarDateFromFields(_calendar_, _fields_, _options_).
1. Let _timeResult_ be ? RegulateTime(_timeResult_.[[Hour]], _timeResult_.[[Minute]], _timeResult_.[[Second]], _timeResult_.[[Millisecond]], _timeResult_.[[Microsecond]], _timeResult_.[[Nanosecond]], _overflow_).
1. Return the Record {
Expand All @@ -979,23 +982,23 @@ <h1>ToTemporalDateTime ( _item_ [ , _options_ ] )</h1>
<emu-alg>
1. If _options_ is not present, set _options_ to *undefined*.
1. Assert: Type(_options_) is Object or Undefined.
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. If Type(_item_) is Object, then
1. If _item_ has an [[InitializedTemporalDateTime]] internal slot, then
1. Return _item_.
1. If _item_ has an [[InitializedTemporalZonedDateTime]] internal slot, then
1. Perform ? ToTemporalOverflow(_options_).
1. Perform ? ToTemporalOverflow(_resolvedOptions_).
1. Let _instant_ be ! CreateTemporalInstant(_item_.[[Nanoseconds]]).
1. Return ? GetPlainDateTimeFor(_item_.[[TimeZone]], _instant_, _item_.[[Calendar]]).
1. If _item_ has an [[InitializedTemporalDate]] internal slot, then
1. Perform ? ToTemporalOverflow(_options_).
1. Perform ? ToTemporalOverflow(_resolvedOptions_).
1. Return ? CreateTemporalDateTime(_item_.[[ISOYear]], _item_.[[ISOMonth]], _item_.[[ISODay]], 0, 0, 0, 0, 0, 0, _item_.[[Calendar]]).
1. Let _calendar_ be ? GetTemporalCalendarSlotValueWithISODefault(_item_).
1. Let _fieldNames_ be ? CalendarFields(_calendar_, « *"day"*, *"month"*, *"monthCode"*, *"year"* »).
1. Append *"hour"*, *"microsecond"*, *"millisecond"*, *"minute"*, *"nanosecond"*, and *"second"* to _fieldNames_.
1. Let _fields_ be ? PrepareTemporalFields(_item_, _fieldNames_, «»).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _resolvedOptions_).
1. Else,
1. Perform ? ToTemporalOverflow(_options_).
1. If _item_ is not a String, throw a *TypeError* exception.
1. Let _result_ be ? ParseTemporalDateTimeString(_item_).
1. Assert: IsValidISODate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
Expand All @@ -1004,6 +1007,7 @@ <h1>ToTemporalDateTime ( _item_ [ , _options_ ] )</h1>
1. If _calendar_ is *undefined*, set _calendar_ to *"iso8601"*.
1. If IsBuiltinCalendar(_calendar_) is *false*, throw a *RangeError* exception.
1. Set _calendar_ to the ASCII-lowercase of _calendar_.
1. Perform ? ToTemporalOverflow(_resolvedOptions_).
1. Return ? CreateTemporalDateTime(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _calendar_).
</emu-alg>
</emu-clause>
Expand Down
21 changes: 11 additions & 10 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ <h1>Temporal.ZonedDateTime.prototype.with ( _temporalZonedDateTimeLike_ [ , _opt
1. If Type(_temporalZonedDateTimeLike_) is not Object, then
1. Throw a *TypeError* exception.
1. Perform ? RejectTemporalLikeObject(_temporalZonedDateTimeLike_).
1. Set _options_ to ? GetOptionsObject(_options_).
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. Let _calendar_ be _zonedDateTime_.[[Calendar]].
1. Let _timeZone_ be _zonedDateTime_.[[TimeZone]].
1. Let _instant_ be ! CreateTemporalInstant(_zonedDateTime_.[[Nanoseconds]]).
Expand All @@ -604,9 +604,9 @@ <h1>Temporal.ZonedDateTime.prototype.with ( _temporalZonedDateTimeLike_ [ , _opt
1. Set _fields_ to ? CalendarMergeFields(_calendar_, _fields_, _partialZonedDateTime_).
1. Set _fields_ to ? PrepareTemporalFields(_fields_, _fieldNames_, « *"offset"* »).
1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalDisambiguation reads *"disambiguation"*, ToTemporalOffset reads *"offset"*, and InterpretTemporalDateTimeFields reads *"overflow"*).
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_).
1. Let _offset_ be ? ToTemporalOffset(_options_, *"prefer"*).
1. Let _dateTimeResult_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_resolvedOptions_).
1. Let _offset_ be ? ToTemporalOffset(_resolvedOptions_, *"prefer"*).
1. Let _dateTimeResult_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _resolvedOptions_).
1. Let _offsetString_ be ! Get(_fields_, *"offset"*).
1. Assert: Type(_offsetString_) is String.
1. Let _newOffsetNanoseconds_ be ? ParseDateTimeUTCOffset(_offsetString_).
Expand Down Expand Up @@ -1158,6 +1158,7 @@ <h1>
<emu-alg>
1. If _options_ is not present, set _options_ to *undefined*.
1. Assert: Type(_options_) is Object or Undefined.
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. Let _offsetBehaviour_ be ~option~.
1. Let _matchBehaviour_ be ~match exactly~.
1. If Type(_item_) is Object, then
Expand All @@ -1174,9 +1175,9 @@ <h1>
1. If _offsetString_ is *undefined*, then
1. Set _offsetBehaviour_ to ~wall~.
1. NOTE: The following steps read options and perform independent validation in alphabetical order (ToTemporalDisambiguation reads *"disambiguation"*, ToTemporalOffset reads *"offset"*, and InterpretTemporalDateTimeFields reads *"overflow"*).
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_).
1. Let _offsetOption_ be ? ToTemporalOffset(_options_, *"reject"*).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_resolvedOptions_).
1. Let _offsetOption_ be ? ToTemporalOffset(_resolvedOptions_, *"reject"*).
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _resolvedOptions_).
1. Else,
1. If _item_ is not a String, throw a *TypeError* exception.
1. Let _result_ be ? ParseTemporalZonedDateTimeString(_item_).
Expand All @@ -1193,9 +1194,9 @@ <h1>
1. If IsBuiltinCalendar(_calendar_) is *false*, throw a *RangeError* exception.
1. Set _calendar_ to the ASCII-lowercase of _calendar_.
1. Set _matchBehaviour_ to ~match minutes~.
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_options_).
1. Let _offsetOption_ be ? ToTemporalOffset(_options_, *"reject"*).
1. Perform ? ToTemporalOverflow(_options_).
1. Let _disambiguation_ be ? ToTemporalDisambiguation(_resolvedOptions_).
1. Let _offsetOption_ be ? ToTemporalOffset(_resolvedOptions_, *"reject"*).
1. Perform ? ToTemporalOverflow(_resolvedOptions_).
1. Let _offsetNanoseconds_ be 0.
1. If _offsetBehaviour_ is ~option~, then
1. Set _offsetNanoseconds_ to ? ParseDateTimeUTCOffset(_offsetString_).
Expand Down

0 comments on commit c8e4dab

Please sign in to comment.