Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
ptomato committed Nov 9, 2020

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent cf0d12e commit ce097dc
Showing 3 changed files with 240 additions and 22 deletions.
97 changes: 78 additions & 19 deletions polyfill/lib/intl.mjs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ import { ES } from './ecmascript.mjs';
import { GetIntrinsic } from './intrinsicclass.mjs';
import {
GetSlot,
INSTANT,
ISO_YEAR,
ISO_MONTH,
ISO_DAY,
@@ -11,7 +12,8 @@ import {
ISO_MILLISECOND,
ISO_MICROSECOND,
ISO_NANOSECOND,
CALENDAR
CALENDAR,
TIME_ZONE
} from './slots.mjs';
import { TimeZone } from './timezone.mjs';

@@ -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-id-given');
const CAL_ID = Symbol('calendar-id');

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

this[TZ_GIVEN] = options.timeZone ? options.timeZone : 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) {
@@ -75,17 +82,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);
@@ -96,10 +111,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);
@@ -110,10 +129,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);
@@ -197,6 +220,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, {
@@ -231,7 +269,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]
};
}
@@ -248,7 +286,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]
};
}
@@ -265,7 +303,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]
};
}
@@ -282,7 +320,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]
};
}
@@ -319,15 +357,36 @@ 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 = ES.CalendarToString(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.TimeZoneToString(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: main[ZONED],
timeZone
};
}

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

156 changes: 155 additions & 1 deletion polyfill/test/intl.mjs
Original file line number Diff line number Diff line change
@@ -40,6 +40,39 @@ describe('Intl', () => {
assert(str.includes('EST'));
});
});
describe('zoneddatetime.toLocaleString()', () => {
const zdt = Temporal.ZonedDateTime.from('1976-11-18T15:23:30+01:00[Europe/Vienna]');
it(`(${zdt}).toLocaleString('en-US')`, () => equal(zdt.toLocaleString('en'), '11/18/1976, 3:23:30 PM GMT+1'));
it(`(${zdt}).toLocaleString('de-AT')`, () => equal(zdt.toLocaleString('de'), '18.11.1976, 15:23:30 MEZ'));
const fmt = maybeGetWeekdayOnlyFormat();
if (fmt) it('uses only the options in resolvedOptions', () => equal(fmt.format(zdt), 'Thursday'));
it('can override the style of the time zone name', () => {
equal(
zdt.toLocaleString('en', { timeZoneName: 'long' }),
'11/18/1976, 3:23:30 PM Central European Standard Time'
);
});
it("works if the time zone given in options agrees with the object's time zone", () => {
equal(zdt.toLocaleString('en', { timeZone: 'Europe/Vienna' }), '11/18/1976, 3:23:30 PM GMT+1');
});
it("throws if the time zone given in options disagrees with the object's time zone", () => {
throws(() => zdt.toLocaleString('en', { timeZone: 'America/New_York' }), RangeError);
});
it("works when the object's calendar is the same as the locale's calendar", () => {
const zdt = new Temporal.ZonedDateTime(0n, 'UTC', 'japanese');
const result = zdt.toLocaleString('en-US-u-ca-japanese');
assert(result === '1/1/45, 12:00:00 AM UTC' || result === '1/1/45 S, 12:00:00 AM UTC');
});
it("adopts the locale's calendar when the object's calendar is ISO", () => {
const zdt = Temporal.ZonedDateTime.from('1976-11-18T15:23:30+00:00[UTC]');
const result = zdt.toLocaleString('en-US-u-ca-japanese');
assert(result === '11/18/51, 3:23:30 PM UTC' || result === '11/18/51 S, 3:23:30 PM UTC');
});
it('throws when the calendars are different and not ISO', () => {
const zdt = new Temporal.ZonedDateTime(0n, 'UTC', 'gregory');
throws(() => zdt.toLocaleString('en-US-u-ca-japanese'));
});
});
describe('datetime.toLocaleString()', () => {
const datetime = Temporal.PlainDateTime.from('1976-11-18T15:23:30');
it(`(${datetime.toString()}).toLocaleString('en-US', { timeZone: 'America/New_York' })`, () =>
@@ -187,9 +220,11 @@ describe('Intl', () => {

const us = new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York' });
const at = new Intl.DateTimeFormat('de-AT', { timeZone: 'Europe/Vienna' });
const us2 = new Intl.DateTimeFormat('en-US');
const at2 = new Intl.DateTimeFormat('de-AT');
const usCalendar = us.resolvedOptions().calendar;
const atCalendar = at.resolvedOptions().calendar;
const t1 = '1976-11-18T14:23:30Z';
const t1 = '1976-11-18T14:23:30+00:00[UTC]';
const t2 = '2020-02-20T15:44:56-05:00[America/New_York]';
const start = new Date('1922-12-30'); // ☭
const end = new Date('1991-12-26');
@@ -199,6 +234,10 @@ describe('Intl', () => {
equal(us.format(Temporal.Instant.from(t1)), '11/18/1976, 9:23:30 AM');
equal(at.format(Temporal.Instant.from(t1)), '18.11.1976, 15:23:30');
});
it('should work for ZonedDateTime', () => {
equal(us2.format(Temporal.ZonedDateTime.from(t1)), '11/18/1976, 2:23:30 PM UTC');
equal(at2.format(Temporal.ZonedDateTime.from(t1)), '18.11.1976, 14:23:30 UTC');
});
it('should work for DateTime', () => {
equal(us.format(Temporal.PlainDateTime.from(t1)), '11/18/1976, 2:23:30 PM');
equal(at.format(Temporal.PlainDateTime.from(t1)), '18.11.1976, 14:23:30');
@@ -257,6 +296,40 @@ describe('Intl', () => {
{ type: 'second', value: '56' }
]);
});
it('should work for ZonedDateTime', () => {
deepEqual(us2.formatToParts(Temporal.ZonedDateTime.from(t2)), [
{ type: 'month', value: '2' },
{ type: 'literal', value: '/' },
{ type: 'day', value: '20' },
{ type: 'literal', value: '/' },
{ type: 'year', value: '2020' },
{ type: 'literal', value: ', ' },
{ type: 'hour', value: '3' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '44' },
{ type: 'literal', value: ':' },
{ type: 'second', value: '56' },
{ type: 'literal', value: ' ' },
{ type: 'dayPeriod', value: 'PM' },
{ type: 'literal', value: ' ' },
{ type: 'timeZoneName', value: 'EST' }
]);
deepEqual(at2.formatToParts(Temporal.ZonedDateTime.from(t2)), [
{ type: 'day', value: '20' },
{ type: 'literal', value: '.' },
{ type: 'month', value: '2' },
{ type: 'literal', value: '.' },
{ type: 'year', value: '2020' },
{ type: 'literal', value: ', ' },
{ type: 'hour', value: '15' },
{ type: 'literal', value: ':' },
{ type: 'minute', value: '44' },
{ type: 'literal', value: ':' },
{ type: 'second', value: '56' },
{ type: 'literal', value: ' ' },
{ type: 'timeZoneName', value: 'GMT-5' }
]);
});
it('should work for DateTime', () => {
deepEqual(us.formatToParts(Temporal.PlainDateTime.from(t2)), [
{ type: 'month', value: '2' },
@@ -376,6 +449,12 @@ describe('Intl', () => {
'18.11.1976, 15:23:30 – 20.2.2020, 21:44:56'
);
});
it('should work for ZonedDateTime', () => {
const zdt1 = Temporal.ZonedDateTime.from(t1);
const zdt2 = Temporal.ZonedDateTime.from(t2).withTimeZone(zdt1.timeZone);
equal(us2.formatRange(zdt1, zdt2), '11/18/1976, 2:23:30 PM UTC – 2/20/2020, 8:44:56 PM UTC');
equal(at2.formatRange(zdt1, zdt2), '18.11.1976, 14:23:30 UTC – 20.2.2020, 20:44:56 UTC');
});
it('should work for DateTime', () => {
equal(
us.formatRange(Temporal.PlainDateTime.from(t1), Temporal.PlainDateTime.from(t2)),
@@ -447,6 +526,9 @@ describe('Intl', () => {
RangeError
);
});
it('throws for two ZonedDateTimes with different time zones', () => {
throws(() => us2.formatRange(Temporal.ZonedDateTime.from(t1), Temporal.ZonedDateTime.from(t2)), RangeError);
});
});
describe('formatRangeToParts', () => {
it('should work for Instant', () => {
@@ -505,6 +587,72 @@ describe('Intl', () => {
{ type: 'second', value: '56', source: 'endRange' }
]);
});
it('should work for ZonedDateTime', () => {
const zdt1 = Temporal.ZonedDateTime.from(t1);
const zdt2 = Temporal.ZonedDateTime.from(t2).withTimeZone(zdt1.timeZone);
deepEqual(us2.formatRangeToParts(zdt1, zdt2), [
{ type: 'month', value: '11', source: 'startRange' },
{ type: 'literal', value: '/', source: 'startRange' },
{ type: 'day', value: '18', source: 'startRange' },
{ type: 'literal', value: '/', source: 'startRange' },
{ type: 'year', value: '1976', source: 'startRange' },
{ type: 'literal', value: ', ', source: 'startRange' },
{ type: 'hour', value: '2', source: 'startRange' },
{ type: 'literal', value: ':', source: 'startRange' },
{ type: 'minute', value: '23', source: 'startRange' },
{ type: 'literal', value: ':', source: 'startRange' },
{ type: 'second', value: '30', source: 'startRange' },
{ type: 'literal', value: ' ', source: 'startRange' },
{ type: 'dayPeriod', value: 'PM', source: 'startRange' },
{ type: 'literal', value: ' ', source: 'startRange' },
{ type: 'timeZoneName', value: 'UTC', source: 'startRange' },
{ type: 'literal', value: ' – ', source: 'shared' },
{ type: 'month', value: '2', source: 'endRange' },
{ type: 'literal', value: '/', source: 'endRange' },
{ type: 'day', value: '20', source: 'endRange' },
{ type: 'literal', value: '/', source: 'endRange' },
{ type: 'year', value: '2020', source: 'endRange' },
{ type: 'literal', value: ', ', source: 'endRange' },
{ type: 'hour', value: '8', source: 'endRange' },
{ type: 'literal', value: ':', source: 'endRange' },
{ type: 'minute', value: '44', source: 'endRange' },
{ type: 'literal', value: ':', source: 'endRange' },
{ type: 'second', value: '56', source: 'endRange' },
{ type: 'literal', value: ' ', source: 'endRange' },
{ type: 'dayPeriod', value: 'PM', source: 'endRange' },
{ type: 'literal', value: ' ', source: 'endRange' },
{ type: 'timeZoneName', value: 'UTC', source: 'endRange' }
]);
deepEqual(at2.formatRangeToParts(zdt1, zdt2), [
{ type: 'day', value: '18', source: 'startRange' },
{ type: 'literal', value: '.', source: 'startRange' },
{ type: 'month', value: '11', source: 'startRange' },
{ type: 'literal', value: '.', source: 'startRange' },
{ type: 'year', value: '1976', source: 'startRange' },
{ type: 'literal', value: ', ', source: 'startRange' },
{ type: 'hour', value: '14', source: 'startRange' },
{ type: 'literal', value: ':', source: 'startRange' },
{ type: 'minute', value: '23', source: 'startRange' },
{ type: 'literal', value: ':', source: 'startRange' },
{ type: 'second', value: '30', source: 'startRange' },
{ type: 'literal', value: ' ', source: 'startRange' },
{ type: 'timeZoneName', value: 'UTC', source: 'startRange' },
{ type: 'literal', value: ' – ', source: 'shared' },
{ type: 'day', value: '20', source: 'endRange' },
{ type: 'literal', value: '.', source: 'endRange' },
{ type: 'month', value: '2', source: 'endRange' },
{ type: 'literal', value: '.', source: 'endRange' },
{ type: 'year', value: '2020', source: 'endRange' },
{ type: 'literal', value: ', ', source: 'endRange' },
{ type: 'hour', value: '20', source: 'endRange' },
{ type: 'literal', value: ':', source: 'endRange' },
{ type: 'minute', value: '44', source: 'endRange' },
{ type: 'literal', value: ':', source: 'endRange' },
{ type: 'second', value: '56', source: 'endRange' },
{ type: 'literal', value: ' ', source: 'endRange' },
{ type: 'timeZoneName', value: 'UTC', source: 'endRange' }
]);
});
it('should work for DateTime', () => {
deepEqual(us.formatRangeToParts(Temporal.PlainDateTime.from(t1), Temporal.PlainDateTime.from(t2)), [
{ type: 'month', value: '11', source: 'startRange' },
@@ -735,6 +883,12 @@ describe('Intl', () => {
RangeError
);
});
it('throws for two ZonedDateTimes with different time zones', () => {
throws(
() => us2.formatRangeToParts(Temporal.ZonedDateTime.from(t1), Temporal.ZonedDateTime.from(t2)),
RangeError
);
});
});
});
});
9 changes: 7 additions & 2 deletions spec/intl.html
Original file line number Diff line number Diff line change
@@ -315,11 +315,16 @@ <h1>PartitionDateTimePattern ( _dateTimeFormat_, _x_ )</h1>
1. <ins>Let _pattern_ be _dateTimeFormat_.[[TemporalDateTimePattern]].</ins>
1. <ins>Let _tm_ be { [[weekday]]: ! ToDayOfWeek(_x_.[[ISOYear]], _x_.[[ISOMonth]], _x_.[[ISODay]]), [[year]]: _x_.[[ISOYear]], [[month]]: _x_.[[ISOMonth]], [[day]]: _x_.[[ISODay]], [[hour]]: _x_.[[ISOHour]], [[minute]]: _x_.[[ISOMinute]], [[second]]: _x_.[[ISOSecond]] }.</ins>
1. <ins>If _date_ has an [[InitializedTemporalZonedDateTime]] internal slot, then</ins>
1. <ins>Let _calendar_ be ? CalendarToString(_x_).</ins>
1. <ins>Let _calendar_ be ? CalendarToString(_x_.[[Calendar]]).</ins>
1. <ins>If _calendar_ is not *"iso8601"* and not equal to _dateTimeFormat_.[[Calendar]], then</ins>
1. <ins>Throw a *RangeError* exception.</ins>
1. <ins>Let _timeZone_ be ? TimeZoneToString(_x_.[[TimeZone]]).</ins>
1. <ins>If _dateTimeFormat_.[[TimeZone]] is not equal to DefaultTimeZone(), and _timeZone_ is not equal to _dateTimeFormat_.[[TimeZone]], then</ins>
1. <ins>Throw a *RangeError* exception.</ins>
1. <ins>Let _instant_ be ? CreateTemporalInstant(_x_.[[Nanoseconds]]).</ins>
1. <ins>Set _x_ to ? GetTemporalDateTimeFor(_x_.[[TimeZone]], _instant_, _calendar_).</ins>
1. <ins>Let _pattern_ be _dateTimeFormat_.[[TemporalZonedDateTimePattern]].</ins>
1. <ins>Let _tm_ be { [[weekday]]: <mark>TODO</mark>, [[year]]: <mark>TODO</mark>, [[month]]: <mark>TODO</mark>, [[day]]: <mark>TODO</mark>, [[hour]]: <mark>TODO</mark>, [[minute]]: <mark>TODO</mark>, [[second]]: <mark>TODO</mark>, [[timeZoneName]]: ? TimeZoneToString(_date_) }.
1. <ins>Let _tm_ be { [[weekday]]: ! ToDayOfWeek(_x_.[[ISOYear]], _x_.[[ISOMonth]], _x_.[[ISODay]]), [[year]]: _x_.[[ISOYear]], [[month]]: _x_.[[ISOMonth]], [[day]]: _x_.[[ISODay]], [[hour]]: _x_.[[ISOHour]], [[minute]]: _x_.[[ISOMinute]], [[second]]: _x_.[[ISOSecond]], [[timeZoneName]]: _timeZone_ }.</ins>
1. <ins>If _pattern_ is *null*, throw a *TypeError* exception.</ins>
1. <ins>Else,</ins>
1. <ins>Let _pattern_ be _dateTimeFormat_.[[Pattern]].</ins>

0 comments on commit ce097dc

Please sign in to comment.