diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 8330ab3d01..d0738402a1 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -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 { @@ -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) => { @@ -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, @@ -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( @@ -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); @@ -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; }, diff --git a/polyfill/lib/timezone.mjs b/polyfill/lib/timezone.mjs index 55c2f7c117..ef006ecf76 100644 --- a/polyfill/lib/timezone.mjs +++ b/polyfill/lib/timezone.mjs @@ -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); } @@ -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), @@ -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))]; } @@ -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; } @@ -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; } diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index 3fc2780016..643d508a82 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -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, diff --git a/polyfill/test/duration.mjs b/polyfill/test/duration.mjs index 824a118b77..a2adf81648 100644 --- a/polyfill/test/duration.mjs +++ b/polyfill/test/duration.mjs @@ -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 }); @@ -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', () => { @@ -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); @@ -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', () => { @@ -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); + }); }); }); diff --git a/polyfill/test/zoneddatetime.mjs b/polyfill/test/zoneddatetime.mjs index d8f3e96322..206da2c27d 100644 --- a/polyfill/test/zoneddatetime.mjs +++ b/polyfill/test/zoneddatetime.mjs @@ -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)); @@ -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', () => { @@ -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]'); @@ -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()', () => { @@ -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]'); @@ -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); + }); }); });