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: Make ZonedDateTime.toLocaleString work without DateTimeFormat #2522

Merged
merged 2 commits into from
Apr 13, 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
77 changes: 12 additions & 65 deletions polyfill/lib/intl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { ES } from './ecmascript.mjs';
import { GetIntrinsic } from './intrinsicclass.mjs';
import {
GetSlot,
INSTANT,
ISO_YEAR,
ISO_MONTH,
ISO_DAY,
Expand All @@ -12,20 +11,17 @@ import {
ISO_MILLISECOND,
ISO_MICROSECOND,
ISO_NANOSECOND,
CALENDAR,
TIME_ZONE
CALENDAR
} from './slots.mjs';

const DATE = Symbol('date');
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');
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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)) {
Expand Down
56 changes: 55 additions & 1 deletion polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
ptomato marked this conversation as resolved.
Show resolved Hide resolved
}
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');
Expand Down
2 changes: 1 addition & 1 deletion polyfill/test262
Submodule test262 updated 22 files
+17 −0 test/intl402/DateTimeFormat/prototype/format/temporal-zoneddatetime-not-supported.js
+18 −0 test/intl402/DateTimeFormat/prototype/formatRange/temporal-zoneddatetime-not-supported.js
+18 −0 test/intl402/DateTimeFormat/prototype/formatRangeToParts/temporal-zoneddatetime-not-supported.js
+17 −0 test/intl402/DateTimeFormat/prototype/formatToParts/temporal-zoneddatetime-not-supported.js
+27 −0 test/intl402/Temporal/PlainDate/prototype/toLocaleString/calendar-mismatch.js
+27 −0 test/intl402/Temporal/PlainDateTime/prototype/toLocaleString/calendar-mismatch.js
+27 −0 test/intl402/Temporal/PlainMonthDay/prototype/toLocaleString/calendar-mismatch.js
+27 −0 test/intl402/Temporal/PlainYearMonth/prototype/toLocaleString/calendar-mismatch.js
+27 −0 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/calendar-mismatch.js
+17 −0 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/custom-time-zone-name-not-supported.js
+11 −2 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/locales-undefined.js
+11 −0 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/offset-time-zone-not-supported.js
+15 −0 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/options-timeZone.js
+16 −0 .../intl402/Temporal/ZonedDateTime/prototype/toLocaleString/options-timeZoneName-affects-instance-time-zone.js
+11 −2 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/options-undefined.js
+24 −0 test/intl402/Temporal/ZonedDateTime/prototype/toLocaleString/time-zone-canonicalized.js
+0 −435 test/staging/Intl402/Temporal/old/date-time-format.js
+0 −25 test/staging/Intl402/Temporal/old/date-toLocaleString.js
+0 −31 test/staging/Intl402/Temporal/old/datetime-toLocaleString.js
+0 −15 test/staging/Intl402/Temporal/old/monthday-toLocaleString.js
+0 −17 test/staging/Intl402/Temporal/old/yearmonth-toLocaleString.js
+0 −69 test/staging/Intl402/Temporal/old/zoneddatetime-toLocaleString.js
67 changes: 26 additions & 41 deletions spec/intl.html
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,13 @@ <h1>
<h1><a href="https://tc39.es/ecma402/#sec-datetimeformat-abstracts">Abstract Operations For DateTimeFormat Objects</a></h1>

<emu-clause id="sec-temporal-initializedatetimeformat" aoid="InitializeDateTimeFormat">
<h1>InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )</h1>
<h1>InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ [ , <ins>_toLocaleStringTimeZone_</ins> ] )</h1>

<p>
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.
<ins>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`.</ins>
This abstract operation functions as follows:
</p>

<p>
Expand Down Expand Up @@ -238,8 +241,12 @@ <h1>InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )</h1>
1. <del>Set _dateTimeFormat_.[[HourCycle]] to _hc_.</del>
1. Let _timeZone_ be ? Get(_options_, *"timeZone"*).
1. If _timeZone_ is *undefined*, then
1. Set _timeZone_ to DefaultTimeZone().
1. <ins>If _toLocaleStringTimeZone_ is present, then</ins>
ptomato marked this conversation as resolved.
Show resolved Hide resolved
1. <ins>Set _timeZone_ to _toLocaleStringTimeZone_.</ins>
1. <ins>Else,</ins>
1. Set _timeZone_ to DefaultTimeZone().
1. Else,
1. <ins>If _toLocaleStringTimeZone_ is present, throw a *TypeError* exception.</ins>
1. Set _timeZone_ to ? ToString(_timeZone_).
1. If <del>the result of IsValidTimeZoneName(_timeZone_)</del><ins>IsAvailableTimeZoneName(_timeZone_)</ins> is *false*, then
1. Throw a *RangeError* exception.
Expand Down Expand Up @@ -308,8 +315,9 @@ <h1>InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )</h1>
1. <ins>Set _limitedOptions_.[[&lt;_field_&gt;]] to _formatOptions_.[[&lt;_field_&gt;]].</ins>
1. <ins>If _needDefaults_ is *true*, then</ins>
1. <ins>Let _defaultFields_ be the list of fields in the Default fields column of the row.</ins>
1. <ins>If the Pattern column of the row is [[TemporalInstantPattern]], and _toLocaleStringTimeZone_ is present, append [[timeZoneName]] to _defaultFields_.</ins>
1. <ins>For each element _field_ of _defaultFields_, do</ins>
1. <ins>If _field_ is *"timeZoneName"*, then</ins>
1. <ins>If _field_ is [[timeZoneName]], then</ins>
1. <ins>Let _defaultValue_ be *"short"*.</ins>
1. <ins>Else,</ins>
1. <ins>Let _defaultValue_ be *"numeric"*.</ins>
Expand Down Expand Up @@ -360,13 +368,8 @@ <h1>InitializeDateTimeFormat ( _dateTimeFormat_, _locales_, _options_ )</h1>
</tr>
<tr>
<th>[[TemporalInstantPattern]]</th>
<td>[[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[dayPeriod]], [[fractionalSecondDigits]]</td>
<td>[[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]]</td>
</tr>
<tr>
<th>[[TemporalZonedDateTimePattern]]</th>
<td>[[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[dayPeriod]], [[fractionalSecondDigits]], [[timeZoneName]]</td>
<td>[[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]], [[timeZoneName]]</td>
<td>[[year]], [[month]], [[day]], [[hour]], [[minute]], [[second]]</td>
</tr>
</tbody>
</table>
Expand Down Expand Up @@ -889,33 +892,6 @@ <h1>HandleDateTimeTemporalInstant ( _dateTimeFormat_, _instant_ )</h1>
</emu-alg>
</emu-clause>
</ins>
<ins class="block">
<emu-clause id="sec-temporal-handledatetimevaluetemporalzoneddatetime" aoid="HandleDateTimeTemporalZonedDateTime">
<h1>HandleDateTimeTemporalZonedDateTime ( _dateTimeFormat_, _zonedDateTime_ )</h1>

<p>
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:
</p>

<emu-alg>
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]]
}.
</emu-alg>
</emu-clause>
</ins>

<ins class="block">
<emu-clause id="sec-temporal-handledatetimeothers" aoid="HandleDateTimeOthers">
Expand Down Expand Up @@ -967,7 +943,7 @@ <h1>HandleDateTimeValue ( _dateTimeFormat_, _x_ )</h1>
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_).
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -1333,7 +1309,7 @@ <h1><a href="https://tc39.es/ecma402/#sec-properties-of-intl-datetimeformat-inst
<li>[[DateStyle]], [[TimeStyle]] are each either *undefined*, or a String value with values *"full"*, *"long"*, *"medium"*, or *"short"*.</li>
<li>[[Pattern]] is a String value as described in <emu-xref href="#sec-intl.datetimeformat-internal-slots"></emu-xref>.</li>
<li>[[RangePatterns]] is a Record as described in <emu-xref href="#sec-intl.datetimeformat-internal-slots"></emu-xref>.</li>
<li><ins>[[TemporalPlainDatePattern]], [[TemporalPlainYearMonthPattern]], [[TemporalPlainMonthDayPattern]], [[TemporalPlainTimePattern]], [[TemporalPlainDateTimePattern]], [[TemporalInstantPattern]], and [[TemporalZonedDateTimePattern]] are records containing at least a [[pattern]] field as described in <emu-xref href="#sec-intl.datetimeformat-internal-slots"></emu-xref>.</ins></li>
<li><ins>[[TemporalPlainDatePattern]], [[TemporalPlainYearMonthPattern]], [[TemporalPlainMonthDayPattern]], [[TemporalPlainTimePattern]], [[TemporalPlainDateTimePattern]], and [[TemporalInstantPattern]] are records containing at least a [[pattern]] field as described in <emu-xref href="#sec-intl.datetimeformat-internal-slots"></emu-xref>.</ins></li>
</ul>

<p>
Expand Down Expand Up @@ -2489,8 +2465,17 @@ <h1>Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_
<emu-alg>
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_).
ptomato marked this conversation as resolved.
Show resolved Hide resolved
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_).
</emu-alg>
</emu-clause>

Expand Down