From c8e4dab54463e4821e756df18fde74ac7f77fdce Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Thu, 2 Mar 2023 17:18:25 -0800 Subject: [PATCH] Normative: Copy options object in {Plain,Zoned}DateTime.{from,p.with} 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. --- polyfill/lib/ecmascript.mjs | 24 ++++++++++++++---------- polyfill/lib/plaindatetime.mjs | 4 ++-- polyfill/lib/zoneddatetime.mjs | 8 ++++---- spec/plaindatetime.html | 16 ++++++++++------ spec/zoneddatetime.html | 21 +++++++++++---------- 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index a0e32d51d0..ca303e4341 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -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); @@ -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), @@ -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))); @@ -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); } @@ -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'; @@ -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; @@ -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); diff --git a/polyfill/lib/plaindatetime.mjs b/polyfill/lib/plaindatetime.mjs index 464af91002..e9b05497c1 100644 --- a/polyfill/lib/plaindatetime.mjs +++ b/polyfill/lib/plaindatetime.mjs @@ -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, []); @@ -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, diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index 63d6e9f840..73af21b15d 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -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); @@ -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, diff --git a/spec/plaindatetime.html b/spec/plaindatetime.html index 21a4791836..eaa9c80d82 100644 --- a/spec/plaindatetime.html +++ b/spec/plaindatetime.html @@ -398,7 +398,7 @@

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_, «»). @@ -412,7 +412,7 @@

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_). @@ -953,8 +953,11 @@

InterpretTemporalDateTimeFields ( _calendar_, _fields_, _options_ )

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.

+ 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 { @@ -979,23 +982,23 @@

ToTemporalDateTime ( _item_ [ , _options_ ] )

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*. @@ -1004,6 +1007,7 @@

ToTemporalDateTime ( _item_ [ , _options_ ] )

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_).
diff --git a/spec/zoneddatetime.html b/spec/zoneddatetime.html index 54488830ff..45f74faa27 100644 --- a/spec/zoneddatetime.html +++ b/spec/zoneddatetime.html @@ -584,7 +584,7 @@

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]]). @@ -604,9 +604,9 @@

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_). @@ -1158,6 +1158,7 @@

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 @@ -1174,9 +1175,9 @@

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_). @@ -1193,9 +1194,9 @@

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_).