Skip to content

Commit

Permalink
Implement ZonedDateTime.toLocaleString()
Browse files Browse the repository at this point in the history
The ZonedDateTime's time zone wins over the system's default time zone.
If you explicitly give a time zone in the formatter options, and you try
to format a ZonedDateTime with a time zone which is different from the
explicitly given time zone, then toLocaleString() will throw an exception.

formatRange() and formatRangeToParts() require ZonedDateTimes with the
same time zone.

See: #569
  • Loading branch information
ptomato committed Nov 6, 2020
1 parent 0124109 commit e574365
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 22 deletions.
101 changes: 82 additions & 19 deletions polyfill/lib/intl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ES } from './ecmascript.mjs';
import { GetIntrinsic } from './intrinsicclass.mjs';
import {
GetSlot,
INSTANT,
ISO_YEAR,
ISO_MONTH,
ISO_DAY,
Expand All @@ -11,7 +12,8 @@ import {
ISO_MILLISECOND,
ISO_MICROSECOND,
ISO_NANOSECOND,
CALENDAR
CALENDAR,
TIME_ZONE
} from './slots.mjs';
import { TimeZone } from './timezone.mjs';

Expand All @@ -20,9 +22,11 @@ const YM = Symbol('ym');
const MD = Symbol('md');
const TIME = Symbol('time');
const DATETIME = Symbol('datetime');
const INSTANT = Symbol('instant');
const ZONED = Symbol('zoneddatetime');
const INST = Symbol('instant');
const ORIGINAL = Symbol('original');
const TIMEZONE = Symbol('timezone');
const TZ_RESOLVED = Symbol('timezone');
const TZ_GIVEN = Symbol('timezone-given');
const CAL_ID = Symbol('calendar-id');

const descriptor = (value) => {
Expand All @@ -40,15 +44,19 @@ const ObjectAssign = Object.assign;
export function DateTimeFormat(locale = IntlDateTimeFormat().resolvedOptions().locale, options = {}) {
if (!(this instanceof DateTimeFormat)) return new DateTimeFormat(locale, options);

const givenTimeZone = options.timeZone;
this[TZ_GIVEN] = givenTimeZone ? new TimeZone(givenTimeZone) : null;

this[ORIGINAL] = new IntlDateTimeFormat(locale, options);
this[TIMEZONE] = new TimeZone(this.resolvedOptions().timeZone);
this[TZ_RESOLVED] = new TimeZone(this.resolvedOptions().timeZone);
this[CAL_ID] = this.resolvedOptions().calendar;
this[DATE] = new IntlDateTimeFormat(locale, dateAmend(options));
this[YM] = new IntlDateTimeFormat(locale, yearMonthAmend(options));
this[MD] = new IntlDateTimeFormat(locale, monthDayAmend(options));
this[TIME] = new IntlDateTimeFormat(locale, timeAmend(options));
this[DATETIME] = new IntlDateTimeFormat(locale, datetimeAmend(options));
this[INSTANT] = new IntlDateTimeFormat(locale, instantAmend(options));
this[ZONED] = new IntlDateTimeFormat(locale, zonedDateTimeAmend(options));
this[INST] = new IntlDateTimeFormat(locale, instantAmend(options));
}

DateTimeFormat.supportedLocalesOf = function (...args) {
Expand All @@ -75,17 +83,25 @@ function resolvedOptions() {
return this[ORIGINAL].resolvedOptions();
}

function adjustFormatterTimeZone(formatter, timeZone) {
if (!timeZone) return formatter;
const options = formatter.resolvedOptions();
return new IntlDateTimeFormat(options.locale, { ...options, timeZone });
}

function format(datetime, ...rest) {
const { instant, formatter } = extractOverrides(datetime, this);
let { instant, formatter, timeZone } = 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) {
const { instant, formatter } = extractOverrides(datetime, this);
let { instant, formatter, timeZone } = extractOverrides(datetime, this);
if (instant && formatter) {
formatter = adjustFormatterTimeZone(formatter, timeZone);
return formatter.formatToParts(instant.epochMilliseconds);
}
return this[ORIGINAL].formatToParts(datetime, ...rest);
Expand All @@ -96,10 +112,14 @@ function formatRange(a, b) {
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
throw new TypeError('Intl.DateTimeFormat accepts two values of the same type');
}
const { instant: aa, formatter: aformatter } = extractOverrides(a, this);
const { instant: bb, formatter: bformatter } = extractOverrides(b, this);
const { instant: aa, formatter: aformatter, timeZone: atz } = extractOverrides(a, this);
const { instant: bb, formatter: bformatter, timeZone: btz } = extractOverrides(b, this);
if (atz && btz && ES.TimeZoneToString(atz) !== ES.TimeZoneToString(btz)) {
throw new RangeError('cannot format range between different time zones');
}
if (aa && bb && aformatter && bformatter && aformatter === bformatter) {
return aformatter.formatRange(aa.epochMilliseconds, bb.epochMilliseconds);
const formatter = adjustFormatterTimeZone(aformatter, atz);
return formatter.formatRange(aa.epochMilliseconds, bb.epochMilliseconds);
}
}
return this[ORIGINAL].formatRange(a, b);
Expand All @@ -110,10 +130,14 @@ function formatRangeToParts(a, b) {
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
throw new TypeError('Intl.DateTimeFormat accepts two values of the same type');
}
const { instant: aa, formatter: aformatter } = extractOverrides(a, this);
const { instant: bb, formatter: bformatter } = extractOverrides(b, this);
const { instant: aa, formatter: aformatter, timeZone: atz } = extractOverrides(a, this);
const { instant: bb, formatter: bformatter, timeZone: btz } = extractOverrides(b, this);
if (atz && btz && ES.TimeZoneToString(atz) !== ES.TimeZoneToString(btz)) {
throw new RangeError('cannot format range between different time zones');
}
if (aa && bb && aformatter && bformatter && aformatter === bformatter) {
return aformatter.formatRangeToParts(aa.epochMilliseconds, bb.epochMilliseconds);
const formatter = adjustFormatterTimeZone(aformatter, atz);
return formatter.formatRangeToParts(aa.epochMilliseconds, bb.epochMilliseconds);
}
}
return this[ORIGINAL].formatRangeToParts(a, b);
Expand Down Expand Up @@ -197,6 +221,21 @@ 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 @@ -231,7 +270,7 @@ function extractOverrides(temporalObj, main) {
const nanosecond = GetSlot(temporalObj, ISO_NANOSECOND);
const datetime = new DateTime(1970, 1, 1, hour, minute, second, millisecond, microsecond, nanosecond, main[CAL_ID]);
return {
instant: main[TIMEZONE].getInstantFor(datetime),
instant: main[TZ_RESOLVED].getInstantFor(datetime),
formatter: main[TIME]
};
}
Expand All @@ -248,7 +287,7 @@ function extractOverrides(temporalObj, main) {
}
const datetime = new DateTime(isoYear, isoMonth, referenceISODay, 12, 0, 0, 0, 0, 0, calendar);
return {
instant: main[TIMEZONE].getInstantFor(datetime),
instant: main[TZ_RESOLVED].getInstantFor(datetime),
formatter: main[YM]
};
}
Expand All @@ -265,7 +304,7 @@ function extractOverrides(temporalObj, main) {
}
const datetime = new DateTime(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0, calendar);
return {
instant: main[TIMEZONE].getInstantFor(datetime),
instant: main[TZ_RESOLVED].getInstantFor(datetime),
formatter: main[MD]
};
}
Expand All @@ -282,7 +321,7 @@ function extractOverrides(temporalObj, main) {
}
const datetime = new DateTime(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0, main[CAL_ID]);
return {
instant: main[TIMEZONE].getInstantFor(datetime),
instant: main[TZ_RESOLVED].getInstantFor(datetime),
formatter: main[DATE]
};
}
Expand Down Expand Up @@ -319,15 +358,39 @@ function extractOverrides(temporalObj, main) {
);
}
return {
instant: main[TIMEZONE].getInstantFor(datetime),
instant: main[TZ_RESOLVED].getInstantFor(datetime),
formatter: main[DATETIME]
};
}

if (ES.IsTemporalZonedDateTime(temporalObj)) {
const calendar = GetSlot(temporalObj, CALENDAR);
if (calendar.id !== 'iso8601' && calendar.id !== main[CAL_ID]) {
throw new RangeError(
`cannot format ZonedDateTime with calendar ${calendar.id} in locale with calendar ${main[CAL_ID]}`
);
}

let timeZone = GetSlot(temporalObj, TIME_ZONE);
const objTimeZone = ES.TimeZoneToString(timeZone);
if (main[TZ_GIVEN]) {
const givenTimeZone = ES.TimeZoneToString(main[TZ_GIVEN]);
if (givenTimeZone !== objTimeZone) {
throw new RangeError(`timeZone option ${givenTimeZone} doesn't match actual time zone ${objTimeZone}`);
}
}

return {
instant: GetSlot(temporalObj, INSTANT),
formatter: main[ZONED],
timeZone
};
}

if (ES.IsTemporalInstant(temporalObj)) {
return {
instant: temporalObj,
formatter: main[INSTANT]
formatter: main[INST]
};
}

Expand Down
Loading

0 comments on commit e574365

Please sign in to comment.