diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index a0ee6899a5..610bcbabec 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -2,7 +2,6 @@ import { ES } from './ecmascript.mjs'; import { GetIntrinsic } from './intrinsicclass.mjs'; import { GetSlot, - INSTANT, ISO_YEAR, ISO_MONTH, ISO_DAY, @@ -12,8 +11,7 @@ import { ISO_MILLISECOND, ISO_MICROSECOND, ISO_NANOSECOND, - CALENDAR, - TIME_ZONE + CALENDAR } from './slots.mjs'; const DATE = Symbol('date'); @@ -21,11 +19,9 @@ const YM = Symbol('ym'); const MD = Symbol('md'); const TIME = Symbol('time'); const DATETIME = Symbol('datetime'); -const ZONED = Symbol('zoneddatetime'); const INST = Symbol('instant'); const ORIGINAL = Symbol('original'); const TZ_RESOLVED = Symbol('timezone'); -const TZ_GIVEN = Symbol('timezone-id-given'); const CAL_ID = Symbol('calendar-id'); const LOCALE = Symbol('locale'); const OPTIONS = Symbol('options'); @@ -83,7 +79,6 @@ export function DateTimeFormat(locale = undefined, options = undefined) { this[OPTIONS] = options; } - this[TZ_GIVEN] = options.timeZone ? options.timeZone : null; this[LOCALE] = ro.locale; this[ORIGINAL] = original; this[TZ_RESOLVED] = ro.timeZone; @@ -93,7 +88,6 @@ export function DateTimeFormat(locale = undefined, options = undefined) { this[MD] = monthDayAmend; this[TIME] = timeAmend; this[DATETIME] = datetimeAmend; - this[ZONED] = zonedDateTimeAmend; this[INST] = instantAmend; } @@ -127,26 +121,17 @@ function resolvedOptions() { return this[ORIGINAL].resolvedOptions(); } -function adjustFormatterTimeZone(formatter, timeZone) { - if (!timeZone) return formatter; - const options = formatter.resolvedOptions(); - if (options.timeZone === timeZone) return formatter; - return new IntlDateTimeFormat(options.locale, { ...options, timeZone }); -} - function format(datetime, ...rest) { - let { instant, formatter, timeZone } = extractOverrides(datetime, this); + let { instant, formatter } = extractOverrides(datetime, this); if (instant && formatter) { - formatter = adjustFormatterTimeZone(formatter, timeZone); return formatter.format(instant.epochMilliseconds); } return this[ORIGINAL].format(datetime, ...rest); } function formatToParts(datetime, ...rest) { - let { instant, formatter, timeZone } = extractOverrides(datetime, this); + let { instant, formatter } = extractOverrides(datetime, this); if (instant && formatter) { - formatter = adjustFormatterTimeZone(formatter, timeZone); return formatter.formatToParts(instant.epochMilliseconds); } return this[ORIGINAL].formatToParts(datetime, ...rest); @@ -157,14 +142,10 @@ function formatRange(a, b) { if (!sameTemporalType(a, b)) { throw new TypeError('Intl.DateTimeFormat.formatRange accepts two values of the same type'); } - const { instant: aa, formatter: aformatter, timeZone: atz } = extractOverrides(a, this); - const { instant: bb, formatter: bformatter, timeZone: btz } = extractOverrides(b, this); - if (atz && btz && atz !== btz) { - throw new RangeError('cannot format range between different time zones'); - } + const { instant: aa, formatter: aformatter } = extractOverrides(a, this); + const { instant: bb, formatter: bformatter } = extractOverrides(b, this); if (aa && bb && aformatter && bformatter && aformatter === bformatter) { - const formatter = adjustFormatterTimeZone(aformatter, atz); - return formatter.formatRange(aa.epochMilliseconds, bb.epochMilliseconds); + return aformatter.formatRange(aa.epochMilliseconds, bb.epochMilliseconds); } } return this[ORIGINAL].formatRange(a, b); @@ -175,14 +156,10 @@ function formatRangeToParts(a, b) { if (!sameTemporalType(a, b)) { throw new TypeError('Intl.DateTimeFormat.formatRangeToParts accepts two values of the same type'); } - const { instant: aa, formatter: aformatter, timeZone: atz } = extractOverrides(a, this); - const { instant: bb, formatter: bformatter, timeZone: btz } = extractOverrides(b, this); - if (atz && btz && atz !== btz) { - throw new RangeError('cannot format range between different time zones'); - } + const { instant: aa, formatter: aformatter } = extractOverrides(a, this); + const { instant: bb, formatter: bformatter } = extractOverrides(b, this); if (aa && bb && aformatter && bformatter && aformatter === bformatter) { - const formatter = adjustFormatterTimeZone(aformatter, atz); - return formatter.formatRangeToParts(aa.epochMilliseconds, bb.epochMilliseconds); + return aformatter.formatRangeToParts(aa.epochMilliseconds, bb.epochMilliseconds); } } return this[ORIGINAL].formatRangeToParts(a, b); @@ -298,21 +275,6 @@ function datetimeAmend(options) { return options; } -function zonedDateTimeAmend(options) { - if (!hasTimeOptions(options) && !hasDateOptions(options)) { - options = ObjectAssign({}, options, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric' - }); - if (options.timeZoneName === undefined) options.timeZoneName = 'short'; - } - return options; -} - function instantAmend(options) { if (!hasTimeOptions(options) && !hasDateOptions(options)) { options = ObjectAssign({}, options, { @@ -465,24 +427,9 @@ function extractOverrides(temporalObj, main) { } if (ES.IsTemporalZonedDateTime(temporalObj)) { - const calendar = ES.ToTemporalCalendarIdentifier(GetSlot(temporalObj, CALENDAR)); - if (calendar !== 'iso8601' && calendar !== main[CAL_ID]) { - throw new RangeError( - `cannot format ZonedDateTime with calendar ${calendar} in locale with calendar ${main[CAL_ID]}` - ); - } - - let timeZone = GetSlot(temporalObj, TIME_ZONE); - const objTimeZone = ES.ToTemporalTimeZoneIdentifier(timeZone); - if (main[TZ_GIVEN] && main[TZ_GIVEN] !== objTimeZone) { - throw new RangeError(`timeZone option ${main[TZ_GIVEN]} doesn't match actual time zone ${objTimeZone}`); - } - - return { - instant: GetSlot(temporalObj, INSTANT), - formatter: getPropLazy(main, ZONED), - timeZone: objTimeZone - }; + throw new TypeError( + 'Temporal.ZonedDateTime not supported in DateTimeFormat methods. Use toLocaleString() instead.' + ); } if (ES.IsTemporalInstant(temporalObj)) { diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index a665b836cb..41b9d83d23 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -21,6 +21,7 @@ import { import bigInt from 'big-integer'; const ArrayPrototypePush = Array.prototype.push; +const customResolvedOptions = DateTimeFormat.prototype.resolvedOptions; const ObjectCreate = Object.create; export class ZonedDateTime { @@ -441,7 +442,60 @@ export class ZonedDateTime { } toLocaleString(locales = undefined, options = undefined) { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); - return new DateTimeFormat(locales, options).format(this); + options = ES.GetOptionsObject(options); + + const optionsCopy = ObjectCreate(null); + // This is not quite per specification, but this polyfill's DateTimeFormat + // already doesn't match the InitializeDateTimeFormat operation, and the + // access order might change anyway; + // see https://github.com/tc39/ecma402/issues/747 + ES.CopyDataProperties(optionsCopy, options, ['timeZone']); + + if (options.timeZone !== undefined) { + throw new TypeError('ZonedDateTime toLocaleString does not accept a timeZone option'); + } + + if ( + optionsCopy.year === undefined && + optionsCopy.month === undefined && + optionsCopy.day === undefined && + optionsCopy.weekday === undefined && + optionsCopy.dateStyle === undefined && + optionsCopy.hour === undefined && + optionsCopy.minute === undefined && + optionsCopy.second === undefined && + optionsCopy.timeStyle === undefined && + optionsCopy.dayPeriod === undefined && + optionsCopy.timeZoneName === undefined + ) { + optionsCopy.timeZoneName = 'short'; + // The rest of the defaults will be filled in by formatting the Instant + } + + let timeZone = ES.ToTemporalTimeZoneIdentifier(GetSlot(this, TIME_ZONE)); + if (ES.IsTimeZoneOffsetString(timeZone)) { + // Note: https://github.com/tc39/ecma402/issues/683 will remove this + throw new RangeError('toLocaleString does not support offset string time zones'); + } + timeZone = ES.GetCanonicalTimeZoneIdentifier(timeZone); + optionsCopy.timeZone = timeZone; + + const formatter = new DateTimeFormat(locales, optionsCopy); + + const localeCalendarIdentifier = ES.Call(customResolvedOptions, formatter, []).calendar; + const calendarIdentifier = ES.ToTemporalCalendarIdentifier(GetSlot(this, CALENDAR)); + if ( + calendarIdentifier !== 'iso8601' && + localeCalendarIdentifier !== 'iso8601' && + localeCalendarIdentifier !== calendarIdentifier + ) { + throw new RangeError( + `cannot format ZonedDateTime with calendar ${calendarIdentifier}` + + ` in locale with calendar ${localeCalendarIdentifier}` + ); + } + + return formatter.format(GetSlot(this, INSTANT)); } toJSON() { if (!ES.IsTemporalZonedDateTime(this)) throw new TypeError('invalid receiver'); diff --git a/spec/intl.html b/spec/intl.html index 4e4952ef38..6d05e957b9 100644 --- a/spec/intl.html +++ b/spec/intl.html @@ -189,10 +189,13 @@

Abstract Operations For DateTimeFormat Objects

-

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )

+

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ [ , _toLocaleStringTimeZone_ ] )

- The abstract operation InitializeDateTimeFormat accepts the arguments _dateTimeFormat_ (which must be an object), _locales_, and _options_. It initializes _dateTimeFormat_ as a DateTimeFormat object. This abstract operation functions as follows: + The abstract operation InitializeDateTimeFormat accepts the arguments _dateTimeFormat_ (which must be an object), _locales_, and _options_. + It initializes _dateTimeFormat_ as a DateTimeFormat object. + If an additional _toLocaleStringTimeZone_ argument is provided (which, if present, must be a canonical time zone name string), the time zone will be overridden and some adjustments will be made to the defaults in order to implement the behaviour of `Temporal.ZonedDateTime.prototype.toLocaleString`. + This abstract operation functions as follows:

@@ -238,8 +241,12 @@

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )

1. Set _dateTimeFormat_.[[HourCycle]] to _hc_. 1. Let _timeZone_ be ? Get(_options_, *"timeZone"*). 1. If _timeZone_ is *undefined*, then - 1. Set _timeZone_ to DefaultTimeZone(). + 1. If _toLocaleStringTimeZone_ is present, then + 1. Set _timeZone_ to _toLocaleStringTimeZone_. + 1. Else, + 1. Set _timeZone_ to DefaultTimeZone(). 1. Else, + 1. If _toLocaleStringTimeZone_ is present, throw a *TypeError* exception. 1. Set _timeZone_ to ? ToString(_timeZone_). 1. If the result of IsValidTimeZoneName(_timeZone_)IsAvailableTimeZoneName(_timeZone_) is *false*, then 1. Throw a *RangeError* exception. @@ -308,8 +315,9 @@

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )

1. Set _limitedOptions_.[[<_field_>]] to _formatOptions_.[[<_field_>]]. 1. If _needDefaults_ is *true*, then 1. Let _defaultFields_ be the list of fields in the Default fields column of the row. + 1. If the Pattern column of the row is [[TemporalInstantPattern]], and _toLocaleStringTimeZone_ is present, append [[timeZoneName]] to _defaultFields_. 1. For each element _field_ of _defaultFields_, do - 1. If _field_ is *"timeZoneName"*, then + 1. If _field_ is [[timeZoneName]], then 1. Let _defaultValue_ be *"short"*. 1. Else, 1. Let _defaultValue_ be *"numeric"*. @@ -360,13 +368,8 @@

InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )

[[TemporalInstantPattern]] - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[dayPeriod]], [[fractionalSecondDigits]] - [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]] - - - [[TemporalZonedDateTimePattern]] [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[dayPeriod]], [[fractionalSecondDigits]], [[timeZoneName]] - [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[timeZoneName]] + [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]] @@ -889,33 +892,6 @@

HandleDateTimeTemporalInstant ( _dateTimeFormat_, _instant_ )

- - -

HandleDateTimeTemporalZonedDateTime ( _dateTimeFormat_, _zonedDateTime_ )

- -

- The abstract operation HandleDateTimeTemporalZonedDateTime accepts the arguments _dateTimeFormat_ (which must be an object initialized as a DateTimeFormat) and _zonedDateTime_ (which must be an ECMAScript value has an [[InitializedTemporalDateTime]] internal slot). It returns a record which contains the appropriate pattern and epochNanoseconds values for the input. This abstract operation functions as follows: -

- - - 1. Assert: _zonedDateTime_ has an [[InitializedTemporalZonedDateTime]] internal slot. - 1. Let _pattern_ be _dateTimeFormat_.[[TemporalZonedDateTimePattern]]. - 1. Let _calendar_ be ? ToTemporalCalendarIdentifier(_zonedDateTime_.[[Calendar]]). - 1. If _calendar_ is not *"iso8601"* and not equal to _dateTimeFormat_.[[Calendar]], then - 1. Throw a *RangeError* exception. - 1. Let _timeZone_ be ? ToTemporalTimeZoneIdentifier(_zonedDateTime_.[[TimeZone]]). - 1. If _dateTimeFormat_.[[TimeZone]] is not equal to DefaultTimeZone(), and _timeZone_ is not equal to _dateTimeFormat_.[[TimeZone]], then - 1. Throw a *RangeError* exception. - 1. Let _instant_ be ! CreateTemporalInstant(_zonedDateTime_.[[Nanoseconds]]). - 1. If _pattern_ is *null*, throw a *TypeError* exception. - 1. Return the Record { - [[pattern]]: _pattern_.[[pattern]], - [[rangePatterns]]: _pattern_.[[rangePatterns]], - [[epochNanoseconds]]: _instant_.[[Nanoseconds]] - }. - -
-
@@ -967,7 +943,7 @@

HandleDateTimeValue ( _dateTimeFormat_, _x_ )

1. If _x_ has an [[InitializedTemporalInstant]] internal slot, then 1. Return ? HandleDateTimeTemporalInstant(_dateTimeFormat_, _x_). 1. Assert: _x_ has an [[InitializedTemporalZonedDateTime]] internal slot. - 1. Return ? HandleDateTimeTemporalZonedDateTime(_dateTimeFormat_, _x_). + 1. Throw a *TypeError* exception. 1. Return ? HandleDateTimeOthers(_dateTimeFormat_, _x_).
@@ -1333,7 +1309,7 @@

  • [[Pattern]] is a String value as described in .
  • [[RangePatterns]] is a Record as described in .
  • -
  • [[TemporalPlainDatePattern]], [[TemporalPlainYearMonthPattern]], [[TemporalPlainMonthDayPattern]], [[TemporalPlainTimePattern]], [[TemporalPlainDateTimePattern]], [[TemporalInstantPattern]], and [[TemporalZonedDateTimePattern]] are records containing at least a [[pattern]] field as described in .
  • +
  • [[TemporalPlainDatePattern]], [[TemporalPlainYearMonthPattern]], [[TemporalPlainMonthDayPattern]], [[TemporalPlainTimePattern]], [[TemporalPlainDateTimePattern]], and [[TemporalInstantPattern]] are records containing at least a [[pattern]] field as described in .
  • @@ -2489,8 +2465,17 @@

    Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_ 1. Let _zonedDateTime_ be the *this* value. 1. Perform ? RequireInternalSlot(_zonedDateTime_, [[InitializedTemporalZonedDateTime]]). - 1. Let _dateFormat_ be ? Construct(%DateTimeFormat%, « _locales_, _options_ »). - 1. Return ? FormatDateTime(_dateFormat_, _zonedDateTime_). + 1. Let _dateTimeFormat_ be ! OrdinaryCreateFromConstructor(%DateTimeFormat%, %DateTimeFormat.protoytpe%, « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »). + 1. Let _timeZone_ be ? ToTemporalTimeZoneIdentifier(_zonedDateTime_.[[TimeZone]]). + 1. If IsTimeZoneOffsetString(_timeZone_) is *true*, throw a *RangeError* exception. + 1. If IsAvailableTimeZoneName(_timeZone_) is *false*, throw a *RangeError* exception. + 1. Set _timeZone_ to CanonicalizeTimeZoneName(_timeZone_). + 1. Perform ? InitializeDateTimeFormat(_dateTimeFormat_, _locales_, _options_, _timeZone_). + 1. Let _calendar_ be ? ToTemporalCalendarIdentifier(_zonedDateTime_.[[Calendar]]). + 1. If _calendar_ is not *"iso8601"* and not equal to _dateTimeFormat_.[[Calendar]], then + 1. Throw a *RangeError* exception. + 1. Let _instant_ be ! CreateTemporalInstant(_zonedDateTime_.[[Nanoseconds]]). + 1. Return ? FormatDateTime(_dateTimeFormat_, _instant_).