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

Polyfill: Throw RangeError if there's an invalid offset string in ZonedDateTime-representing property bags #1976

Merged
merged 1 commit into from
Dec 15, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 16 additions & 8 deletions polyfill/lib/ecmascript.mjs
Expand Up @@ -379,7 +379,7 @@ export const ES = ObjectAssign({}, ES2020, {
let canonicalIdent = ES.GetCanonicalTimeZoneIdentifier(stringIdent);
if (canonicalIdent) {
canonicalIdent = canonicalIdent.toString();
if (ES.ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
if (ES.TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
return { ianaName: canonicalIdent };
}
} catch {
Expand Down Expand Up @@ -447,7 +447,8 @@ export const ES = ObjectAssign({}, ES2020, {
nanosecond
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = z ? 0 : ES.ParseOffsetString(offset);
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ES.ParseTimeZoneOffsetString(offset);
return epochNs.subtract(offsetNs);
},
RegulateISODateTime: (year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, overflow) => {
Expand Down Expand Up @@ -792,7 +793,7 @@ export const ES = ObjectAssign({}, ES2020, {
if (timeZone) {
timeZone = ES.ToTemporalTimeZone(timeZone);
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(ES.ToString(offset));
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(ES.ToString(offset));
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1373,7 +1374,7 @@ export const ES = ObjectAssign({}, ES2020, {
matchMinute = true; // ISO strings may specify offset with less precision
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ES.ParseTimeZoneOffsetString(offset);
const disambiguation = ES.ToTemporalDisambiguation(options);
const offsetOpt = ES.ToTemporalOffset(options, 'reject');
const epochNanoseconds = ES.InterpretISODateTimeOffset(
Expand Down Expand Up @@ -2177,9 +2178,14 @@ export const ES = ObjectAssign({}, ES2020, {
return result;
},

ParseOffsetString: (string) => {
TestTimeZoneOffsetString: (string) => {
return OFFSET.test(String(string));
},
ParseTimeZoneOffsetString: (string) => {
const match = OFFSET.exec(String(string));
if (!match) return null;
if (!match) {
throw new RangeError(`invalid time zone offset: ${string}`);
}
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
const hours = +match[2];
const minutes = +(match[3] || 0);
Expand All @@ -2188,8 +2194,10 @@ export const ES = ObjectAssign({}, ES2020, {
return sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds);
},
GetCanonicalTimeZoneIdentifier: (timeZoneIdentifier) => {
const offsetNs = ES.ParseOffsetString(timeZoneIdentifier);
if (offsetNs !== null) return ES.FormatTimeZoneOffsetString(offsetNs);
if (ES.TestTimeZoneOffsetString(timeZoneIdentifier)) {
const offsetNs = ES.ParseTimeZoneOffsetString(timeZoneIdentifier);
return ES.FormatTimeZoneOffsetString(offsetNs);
}
const formatter = getIntlDateTimeFormatEnUsForTimeZone(String(timeZoneIdentifier));
return formatter.resolvedOptions().timeZone;
},
Expand Down
13 changes: 7 additions & 6 deletions polyfill/lib/timezone.mjs
Expand Up @@ -48,8 +48,9 @@ export class TimeZone {
instant = ES.ToTemporalInstant(instant);
const id = GetSlot(this, TIMEZONE_ID);

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) return offsetNs;
if (ES.TestTimeZoneOffsetString(id)) {
return ES.ParseTimeZoneOffsetString(id);
}

return ES.GetIANATimeZoneOffsetNanoseconds(GetSlot(instant, EPOCHNANOSECONDS), id);
}
Expand All @@ -76,8 +77,7 @@ export class TimeZone {
const Instant = GetIntrinsic('%Temporal.Instant%');
const id = GetSlot(this, TIMEZONE_ID);

const offsetNs = ES.ParseOffsetString(id);
if (offsetNs !== null) {
if (ES.TestTimeZoneOffsetString(id)) {
const epochNs = ES.GetEpochFromISOParts(
GetSlot(dateTime, ISO_YEAR),
GetSlot(dateTime, ISO_MONTH),
Expand All @@ -90,6 +90,7 @@ export class TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
return [new Instant(epochNs.minus(offsetNs))];
}

Expand All @@ -113,7 +114,7 @@ export class TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

Expand All @@ -128,7 +129,7 @@ export class TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

// Offset time zones or UTC have no transitions
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/zoneddatetime.mjs
Expand Up @@ -223,7 +223,7 @@ export class ZonedDateTime {
fields = ES.PrepareTemporalFields(fields, entries);
let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
const offsetNs = ES.ParseOffsetString(fields.offset);
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand Down
47 changes: 47 additions & 0 deletions polyfill/test/duration.mjs
Expand Up @@ -685,6 +685,15 @@ describe('Duration', () => {
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, month: 11 } }), TypeError);
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, day: 3 } }), TypeError);
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P2D').add('P1M', {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.subtract()', () => {
const duration = Duration.from({ days: 3, hours: 1, minutes: 10 });
Expand Down Expand Up @@ -928,6 +937,15 @@ describe('Duration', () => {
equal(zero2.microseconds, 0);
equal(zero2.nanoseconds, 0);
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P2D').subtract('P1M', {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.abs()', () => {
it('makes a copy of a positive duration', () => {
Expand Down Expand Up @@ -1512,6 +1530,16 @@ describe('Duration', () => {
equal(`${yearAndHalf.round({ relativeTo: '2020-01-01', smallestUnit: 'years' })}`, 'P1Y');
equal(`${yearAndHalf.round({ relativeTo: '2020-07-01', smallestUnit: 'years' })}`, 'P2Y');
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P1M280D').round({
smallestUnit: 'month',
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.total()', () => {
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
Expand Down Expand Up @@ -1851,6 +1879,16 @@ describe('Duration', () => {
equal(d.total({ unit: 'microsecond', relativeTo }), d.total({ unit: 'microseconds', relativeTo }));
equal(d.total({ unit: 'nanosecond', relativeTo }), d.total({ unit: 'nanoseconds', relativeTo }));
});
it('throws with invalid offset in relativeTo', () => {
throws(
() =>
Temporal.Duration.from('P1M280D').total({
unit: 'month',
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
}),
RangeError
);
});
});
describe('Duration.compare', () => {
describe('time units only', () => {
Expand Down Expand Up @@ -1947,6 +1985,15 @@ describe('Duration', () => {
it('does not lose precision when totaling everything down to nanoseconds', () => {
notEqual(Duration.compare({ days: 200 }, { days: 200, nanoseconds: 1 }), 0);
});
it('throws with invalid offset in relativeTo', () => {
throws(() => {
const d1 = Temporal.Duration.from('P1M280D');
const d2 = Temporal.Duration.from('P1M281D');
Temporal.Duration.compare(d1, d2, {
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
});
}, RangeError);
});
});
});

Expand Down
71 changes: 71 additions & 0 deletions polyfill/test/zoneddatetime.mjs
Expand Up @@ -589,6 +589,23 @@ describe('ZonedDateTime', () => {
});
equal(`${zdt}`, '1976-11-18T00:00:00-10:30[-10:30]');
});
it('throws with invalid offset', () => {
const offsets = ['use', 'prefer', 'ignore', 'reject'];
offsets.forEach((offset) => {
throws(() => {
Temporal.ZonedDateTime.from(
{
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
},
{ offset }
);
}, RangeError);
});
});
describe('Overflow option', () => {
const bad = { year: 2019, month: 1, day: 32, timeZone: lagos };
it('reject', () => throws(() => ZonedDateTime.from(bad, { overflow: 'reject' }), RangeError));
Expand Down Expand Up @@ -1025,6 +1042,14 @@ describe('ZonedDateTime', () => {
throws(() => zdt.with('12:00'), TypeError);
throws(() => zdt.with('invalid'), TypeError);
});
it('throws with invalid offset', () => {
const offsets = ['use', 'prefer', 'ignore', 'reject'];
offsets.forEach((offset) => {
throws(() => {
Temporal.ZonedDateTime.from('2022-11-26[Europe/London]').with({ offset: '+088:00' }, { offset });
}, RangeError);
});
});
});

describe('.withPlainTime manipulation', () => {
Expand Down Expand Up @@ -1617,6 +1642,18 @@ describe('ZonedDateTime', () => {
equal(`${dt1.until(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P2Y');
equal(`${dt2.until(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P1Y');
});
it('throws with invalid offset', () => {
throws(() => {
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
zdt.until({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
describe('ZonedDateTime.since()', () => {
const zdt = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
Expand Down Expand Up @@ -1948,6 +1985,18 @@ describe('ZonedDateTime', () => {
equal(`${dt2.since(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P1Y');
equal(`${dt1.since(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P2Y');
});
it('throws with invalid offset', () => {
throws(() => {
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
zdt.since({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});

describe('ZonedDateTime.round()', () => {
Expand Down Expand Up @@ -2188,6 +2237,17 @@ describe('ZonedDateTime', () => {
TypeError
);
});
it('throws with invalid offset', () => {
throws(() => {
zdt.equals({
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
describe('ZonedDateTime.toString()', () => {
const zdt1 = ZonedDateTime.from('1976-11-18T15:23+01:00[Europe/Vienna]');
Expand Down Expand Up @@ -2894,6 +2954,17 @@ describe('ZonedDateTime', () => {
equal(ZonedDateTime.compare(clockBefore, clockAfter), 1);
equal(Temporal.PlainDateTime.compare(clockBefore.toPlainDateTime(), clockAfter.toPlainDateTime()), -1);
});
it('throws with invalid offset', () => {
throws(() => {
Temporal.ZonedDateTime.compare(zdt1, {
year: 2021,
month: 11,
day: 26,
offset: '+099:00',
timeZone: 'Europe/London'
});
}, RangeError);
});
});
});

Expand Down