Skip to content
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
79 changes: 53 additions & 26 deletions polyfill/lib/intl.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
TypeError as TypeErrorCtor,

// class static functions and methods
ArrayPrototypeIncludes,
ArrayPrototypeSlice,
IntlDateTimeFormat,
IntlDateTimeFormatPrototypeGetFormat,
Expand All @@ -22,6 +23,7 @@ import {
IntlNumberFormat,
IntlNumberFormatPrototypeGetFormat,
MathCeil,
NumberIsFinite,
NumberIsNaN,
ObjectAssign,
ObjectCreate,
Expand Down Expand Up @@ -131,37 +133,62 @@ function adjustRenderedWeekday(polyfilledFormatter, realFormatter, originalEpoch
function internalCreateDateTimeFormat(dtf, locale, options, required) {
const hasOptions = typeof options !== 'undefined';
if (hasOptions) {
// Read all the options in the expected order and copy them to a
// null-prototype object with which we can do further operations
// unobservably
// Read all the options in the expected order, perform expected conversions
// and validations, and copy them to a null-prototype object with which we
// can do further operations unobservably
const props = [
'localeMatcher',
'calendar',
'numberingSystem',
'hour12',
'hourCycle',
'timeZone',
'weekday',
'era',
'year',
'month',
'day',
'dayPeriod',
'hour',
'minute',
'second',
'fractionalSecondDigits',
'timeZoneName',
'formatMatcher',
'dateStyle',
'timeStyle'
['localeMatcher', 'string', ['lookup', 'best fit']],
['calendar', 'string', null],
['numberingSystem', 'string', null],
['hour12', 'boolean'],
['hourCycle', 'string', ['h11', 'h12', 'h23', 'h24']],
['timeZone', 'string', null],
['weekday', 'string', ['narrow', 'short', 'long']],
['era', 'string', ['narrow', 'short', 'long']],
['year', 'string', ['2-digit', 'numeric']],
['month', 'string', ['2-digit', 'numeric', 'narrow', 'short', 'long']],
['day', 'string', ['2-digit', 'numeric']],
['dayPeriod', 'string', ['narrow', 'short', 'long']],
['hour', 'string', ['2-digit', 'numeric']],
['minute', 'string', ['2-digit', 'numeric']],
['second', 'string', ['2-digit', 'numeric']],
['fractionalSecondDigits', 'number', 1, 3],
['timeZoneName', 'string', ['short', 'long', 'shortOffset', 'longOffset', 'shortGeneric', 'longGeneric']],
['formatMatcher', 'string', ['basic', 'best fit']],
['dateStyle', 'string', ['full', 'long', 'medium', 'short']],
['timeStyle', 'string', ['full', 'long', 'medium', 'short']]
];
options = ES.ToObject(options);
const newOptions = ObjectCreate(null);
for (let i = 0; i < props.length; i++) {
const prop = props[i];
if (ES.HasOwnProperty(options, prop)) {
newOptions[prop] = options[prop];
const entry = props[i];
const prop = entry[0];
let value = options[prop];
if (value !== undefined) {
const conversionHint = entry[1];
switch (conversionHint) {
case 'string':
{
value = ES.ToString(value);
const allowed = entry[2];
if (allowed !== null && !ES.Call(ArrayPrototypeIncludes, allowed, [value])) {
throw new RangeError(`Value ${value} out of range for Intl.DateTimeFormat options property ${prop}`);
}
}
break;
case 'number':
{
value = ES.ToNumber(value);
const minimum = entry[2];
const maximum = entry[3];
if (!NumberIsFinite(value) || value < minimum || value > maximum) {
throw new RangeError(`${prop} value is out of range`);
}
}
break;
case 'boolean': // nothing observable, handle in real DTF constructor
}
newOptions[prop] = value;
}
}
options = newOptions;
Expand Down
10 changes: 8 additions & 2 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ export class ZonedDateTime {

// first, round the underlying DateTime fields
const timeZone = GetSlot(this, TIME_ZONE);
const thisNs = GetSlot(this, EPOCHNANOSECONDS);
let thisNs = GetSlot(this, EPOCHNANOSECONDS);
const iso = dateTime(this);
let epochNanoseconds;

Expand All @@ -301,7 +301,13 @@ export class ZonedDateTime {
assert(thisNs.geq(startNs), 'cannot produce an instant during a day that occurs before start-of-day instant');

const endNs = ES.GetStartOfDay(timeZone, dateEnd);
assert(thisNs.lt(endNs), 'cannot produce an instant during a day that occurs on or after end-of-day instant');
// Handle the case where a transition starts after midnight and falls back
// to before midnight, and pieces of two calendar days are interleaved.
// endNs is the start of the first piece of the second calendar day, so if
// thisNs is inside the second piece of the first calendar day, it can be
// outside of the box defined by start-of-day and end-of-day.
// https://github.com/tc39/proposal-temporal/issues/3312
if (thisNs.geq(endNs)) thisNs = endNs.minus(bigInt.one);

const dayLengthNs = endNs.subtract(startNs);
const dayProgressNs = TimeDuration.fromEpochNsDiff(thisNs, startNs);
Expand Down
37 changes: 35 additions & 2 deletions polyfill/test/thorough/startofday.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { getProgressBar, makeZonedCases, time, withSnapshotsFromFile } from './support.mjs';
import {
assertEqual,
assertTemporalEqual,
getProgressBar,
makeZonedCases,
roundingModes,
time,
withSnapshotsFromFile
} from './support.mjs';

const interestingCases = makeZonedCases().filter(
([datetime]) =>
Expand All @@ -13,7 +21,32 @@ await time(async (start) => {
await withSnapshotsFromFile('./startofday.snapshot.json', (matchSnapshot) => {
for (const [datetime, str] of interestingCases) {
progress.tick(1, { test: str });
matchSnapshot(datetime.startOfDay().toString(), str);
const startToday = datetime.startOfDay();
matchSnapshot(startToday.toString(), str);

// Can't get tomorrow's startOfDay() if this is the last representable day
if (datetime.epochMilliseconds > 86400_0000_0000 - 86400) continue;

const hoursInDay = datetime.hoursInDay;
matchSnapshot(hoursInDay, str + 'h');

// Invariant: startOfDay + hoursInDay = following day's startOfDay
const startTomorrow = datetime.add({ days: 1 }).startOfDay();
assertTemporalEqual(
startToday.add({ seconds: hoursInDay * 3600 }),
startTomorrow,
`${startToday} + ${hoursInDay}h = ${startTomorrow}`
);

// Invariant: round({ smallestUnit: 'day' }) is start of today or tomorrow
for (const roundingMode of roundingModes) {
const rounded = datetime.round({ smallestUnit: 'day', roundingMode });
assertEqual(
rounded.equals(startToday) || rounded.equals(startTomorrow),
true,
`${rounded} = ${startToday} or ${startTomorrow}`
);
}
}
});

Expand Down
Loading
Loading