Skip to content

Commit d5ada8b

Browse files
committed
Throw RangeError for invalid offset strings
Port of tc39/proposal-temporal#1976
1 parent 500b4c9 commit d5ada8b

File tree

5 files changed

+142
-16
lines changed

5 files changed

+142
-16
lines changed

lib/ecmascript.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ function ParseTemporalTimeZoneString(stringIdent: string): Partial<{
458458
let canonicalIdent = GetCanonicalTimeZoneIdentifier(stringIdent);
459459
if (canonicalIdent) {
460460
canonicalIdent = canonicalIdent.toString();
461-
if (ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
461+
if (TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
462462
return { ianaName: canonicalIdent };
463463
}
464464
} catch {
@@ -515,7 +515,7 @@ function ParseTemporalInstant(isoString: string) {
515515
const epochNs = GetEpochFromISOParts(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
516516
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
517517
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
518-
const offsetNs = z ? 0 : ParseOffsetString(offset);
518+
const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset);
519519
return JSBI.subtract(epochNs, JSBI.BigInt(offsetNs));
520520
}
521521

@@ -996,7 +996,7 @@ export function ToRelativeTemporalObject(options: {
996996
if (timeZone) {
997997
timeZone = ToTemporalTimeZone(timeZone);
998998
let offsetNs = 0;
999-
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(ToString(offset));
999+
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(ToString(offset));
10001000
const epochNanoseconds = InterpretISODateTimeOffset(
10011001
year,
10021002
month,
@@ -1663,7 +1663,7 @@ export function ToTemporalZonedDateTime(
16631663
matchMinute = true; // ISO strings may specify offset with less precision
16641664
}
16651665
let offsetNs = 0;
1666-
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(offset);
1666+
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset);
16671667
const disambiguation = ToTemporalDisambiguation(options);
16681668
const offsetOpt = ToTemporalOffset(options, 'reject');
16691669
const epochNanoseconds = InterpretISODateTimeOffset(
@@ -2682,9 +2682,15 @@ export function TemporalZonedDateTimeToString(
26822682
return result;
26832683
}
26842684

2685-
export function ParseOffsetString(string: string): number {
2685+
export function TestTimeZoneOffsetString(string: string) {
2686+
return OFFSET.test(StringCtor(string));
2687+
}
2688+
2689+
export function ParseTimeZoneOffsetString(string: string): number {
26862690
const match = OFFSET.exec(StringCtor(string));
2687-
if (!match) return null;
2691+
if (!match) {
2692+
throw new RangeError(`invalid time zone offset: ${string}`);
2693+
}
26882694
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
26892695
const hours = +match[2];
26902696
const minutes = +(match[3] || 0);
@@ -2694,8 +2700,10 @@ export function ParseOffsetString(string: string): number {
26942700
}
26952701

26962702
export function GetCanonicalTimeZoneIdentifier(timeZoneIdentifier: string): string {
2697-
const offsetNs = ParseOffsetString(timeZoneIdentifier);
2698-
if (offsetNs !== null) return FormatTimeZoneOffsetString(offsetNs);
2703+
if (TestTimeZoneOffsetString(timeZoneIdentifier)) {
2704+
const offsetNs = ParseTimeZoneOffsetString(timeZoneIdentifier);
2705+
return FormatTimeZoneOffsetString(offsetNs);
2706+
}
26992707
const formatter = getIntlDateTimeFormatEnUsForTimeZone(StringCtor(timeZoneIdentifier));
27002708
return formatter.resolvedOptions().timeZone;
27012709
}

lib/timezone.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ export class TimeZone implements Temporal.TimeZone {
5050
const instant = ES.ToTemporalInstant(instantParam);
5151
const id = GetSlot(this, TIMEZONE_ID);
5252

53-
const offsetNs = ES.ParseOffsetString(id);
54-
if (offsetNs !== null) return offsetNs;
55-
53+
if (ES.TestTimeZoneOffsetString(id)) {
54+
return ES.ParseTimeZoneOffsetString(id);
55+
}
5656
return ES.GetIANATimeZoneOffsetNanoseconds(GetSlot(instant, EPOCHNANOSECONDS), id);
5757
}
5858
getOffsetStringFor(instantParam: Params['getOffsetStringFor'][0]): Return['getOffsetStringFor'] {
@@ -84,8 +84,7 @@ export class TimeZone implements Temporal.TimeZone {
8484
const Instant = GetIntrinsic('%Temporal.Instant%');
8585
const id = GetSlot(this, TIMEZONE_ID);
8686

87-
const offsetNs = ES.ParseOffsetString(id);
88-
if (offsetNs !== null) {
87+
if (ES.TestTimeZoneOffsetString(id)) {
8988
const epochNs = ES.GetEpochFromISOParts(
9089
GetSlot(dateTime, ISO_YEAR),
9190
GetSlot(dateTime, ISO_MONTH),
@@ -98,6 +97,7 @@ export class TimeZone implements Temporal.TimeZone {
9897
GetSlot(dateTime, ISO_NANOSECOND)
9998
);
10099
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
100+
const offsetNs = ES.ParseTimeZoneOffsetString(id);
101101
return [new Instant(JSBI.subtract(epochNs, JSBI.BigInt(offsetNs)))];
102102
}
103103

@@ -121,7 +121,7 @@ export class TimeZone implements Temporal.TimeZone {
121121
const id = GetSlot(this, TIMEZONE_ID);
122122

123123
// Offset time zones or UTC have no transitions
124-
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
124+
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
125125
return null;
126126
}
127127

@@ -136,7 +136,7 @@ export class TimeZone implements Temporal.TimeZone {
136136
const id = GetSlot(this, TIMEZONE_ID);
137137

138138
// Offset time zones or UTC have no transitions
139-
if (ES.ParseOffsetString(id) !== null || id === 'UTC') {
139+
if (ES.TestTimeZoneOffsetString(id) || id === 'UTC') {
140140
return null;
141141
}
142142

lib/zoneddatetime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
230230
fields = ES.PrepareTemporalFields(fields, entries as any);
231231
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
232232
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
233-
const offsetNs = ES.ParseOffsetString(fields.offset);
233+
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
234234
const epochNanoseconds = ES.InterpretISODateTimeOffset(
235235
year,
236236
month,

test/duration.mjs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,15 @@ describe('Duration', () => {
687687
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, month: 11 } }), TypeError);
688688
throws(() => oneDay.add(hours24, { relativeTo: { year: 2019, day: 3 } }), TypeError);
689689
});
690+
it('throws with invalid offset in relativeTo', () => {
691+
throws(
692+
() =>
693+
Temporal.Duration.from('P2D').add('P1M', {
694+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
695+
}),
696+
RangeError
697+
);
698+
});
690699
});
691700
describe('Duration.subtract()', () => {
692701
const duration = Duration.from({ days: 3, hours: 1, minutes: 10 });
@@ -930,6 +939,15 @@ describe('Duration', () => {
930939
equal(zero2.microseconds, 0);
931940
equal(zero2.nanoseconds, 0);
932941
});
942+
it('throws with invalid offset in relativeTo', () => {
943+
throws(
944+
() =>
945+
Temporal.Duration.from('P2D').subtract('P1M', {
946+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
947+
}),
948+
RangeError
949+
);
950+
});
933951
});
934952
describe('Duration.abs()', () => {
935953
it('makes a copy of a positive duration', () => {
@@ -1514,6 +1532,16 @@ describe('Duration', () => {
15141532
equal(`${yearAndHalf.round({ relativeTo: '2020-01-01', smallestUnit: 'years' })}`, 'P1Y');
15151533
equal(`${yearAndHalf.round({ relativeTo: '2020-07-01', smallestUnit: 'years' })}`, 'P2Y');
15161534
});
1535+
it('throws with invalid offset in relativeTo', () => {
1536+
throws(
1537+
() =>
1538+
Temporal.Duration.from('P1M280D').round({
1539+
smallestUnit: 'month',
1540+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1541+
}),
1542+
RangeError
1543+
);
1544+
});
15171545
});
15181546
describe('Duration.total()', () => {
15191547
const d = new Duration(5, 5, 5, 5, 5, 5, 5, 5, 5, 5);
@@ -1853,6 +1881,16 @@ describe('Duration', () => {
18531881
equal(d.total({ unit: 'microsecond', relativeTo }), d.total({ unit: 'microseconds', relativeTo }));
18541882
equal(d.total({ unit: 'nanosecond', relativeTo }), d.total({ unit: 'nanoseconds', relativeTo }));
18551883
});
1884+
it('throws with invalid offset in relativeTo', () => {
1885+
throws(
1886+
() =>
1887+
Temporal.Duration.from('P1M280D').total({
1888+
unit: 'month',
1889+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1890+
}),
1891+
RangeError
1892+
);
1893+
});
18561894
});
18571895
describe('Duration.compare', () => {
18581896
describe('time units only', () => {
@@ -1949,6 +1987,15 @@ describe('Duration', () => {
19491987
it('does not lose precision when totaling everything down to nanoseconds', () => {
19501988
notEqual(Duration.compare({ days: 200 }, { days: 200, nanoseconds: 1 }), 0);
19511989
});
1990+
it('throws with invalid offset in relativeTo', () => {
1991+
throws(() => {
1992+
const d1 = Temporal.Duration.from('P1M280D');
1993+
const d2 = Temporal.Duration.from('P1M281D');
1994+
Temporal.Duration.compare(d1, d2, {
1995+
relativeTo: { year: 2021, month: 11, day: 26, offset: '+088:00', timeZone: 'Europe/London' }
1996+
});
1997+
}, RangeError);
1998+
});
19521999
});
19532000
});
19542001

test/zoneddatetime.mjs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,23 @@ describe('ZonedDateTime', () => {
589589
});
590590
equal(`${zdt}`, '1976-11-18T00:00:00-10:30[-10:30]');
591591
});
592+
it('throws with invalid offset', () => {
593+
const offsets = ['use', 'prefer', 'ignore', 'reject'];
594+
offsets.forEach((offset) => {
595+
throws(() => {
596+
Temporal.ZonedDateTime.from(
597+
{
598+
year: 2021,
599+
month: 11,
600+
day: 26,
601+
offset: '+099:00',
602+
timeZone: 'Europe/London'
603+
},
604+
{ offset }
605+
);
606+
}, RangeError);
607+
});
608+
});
592609
describe('Overflow option', () => {
593610
const bad = { year: 2019, month: 1, day: 32, timeZone: lagos };
594611
it('reject', () => throws(() => ZonedDateTime.from(bad, { overflow: 'reject' }), RangeError));
@@ -1025,6 +1042,14 @@ describe('ZonedDateTime', () => {
10251042
throws(() => zdt.with('12:00'), TypeError);
10261043
throws(() => zdt.with('invalid'), TypeError);
10271044
});
1045+
it('throws with invalid offset', () => {
1046+
const offsets = ['use', 'prefer', 'ignore', 'reject'];
1047+
offsets.forEach((offset) => {
1048+
throws(() => {
1049+
Temporal.ZonedDateTime.from('2022-11-26[Europe/London]').with({ offset: '+088:00' }, { offset });
1050+
}, RangeError);
1051+
});
1052+
});
10281053
});
10291054

10301055
describe('.withPlainTime manipulation', () => {
@@ -1617,6 +1642,18 @@ describe('ZonedDateTime', () => {
16171642
equal(`${dt1.until(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P2Y');
16181643
equal(`${dt2.until(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P1Y');
16191644
});
1645+
it('throws with invalid offset', () => {
1646+
throws(() => {
1647+
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
1648+
zdt.until({
1649+
year: 2021,
1650+
month: 11,
1651+
day: 26,
1652+
offset: '+099:00',
1653+
timeZone: 'Europe/London'
1654+
});
1655+
}, RangeError);
1656+
});
16201657
});
16211658
describe('ZonedDateTime.since()', () => {
16221659
const zdt = ZonedDateTime.from('1976-11-18T15:23:30.123456789+01:00[Europe/Vienna]');
@@ -1948,6 +1985,18 @@ describe('ZonedDateTime', () => {
19481985
equal(`${dt2.since(dt1, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, 'P1Y');
19491986
equal(`${dt1.since(dt2, { smallestUnit: 'years', roundingMode: 'halfExpand' })}`, '-P2Y');
19501987
});
1988+
it('throws with invalid offset', () => {
1989+
throws(() => {
1990+
const zdt = ZonedDateTime.from('2019-01-01T00:00+00:00[UTC]');
1991+
zdt.since({
1992+
year: 2021,
1993+
month: 11,
1994+
day: 26,
1995+
offset: '+099:00',
1996+
timeZone: 'Europe/London'
1997+
});
1998+
}, RangeError);
1999+
});
19512000
});
19522001

19532002
describe('ZonedDateTime.round()', () => {
@@ -2188,6 +2237,17 @@ describe('ZonedDateTime', () => {
21882237
TypeError
21892238
);
21902239
});
2240+
it('throws with invalid offset', () => {
2241+
throws(() => {
2242+
zdt.equals({
2243+
year: 2021,
2244+
month: 11,
2245+
day: 26,
2246+
offset: '+099:00',
2247+
timeZone: 'Europe/London'
2248+
});
2249+
}, RangeError);
2250+
});
21912251
});
21922252
describe('ZonedDateTime.toString()', () => {
21932253
const zdt1 = ZonedDateTime.from('1976-11-18T15:23+01:00[Europe/Vienna]');
@@ -2895,6 +2955,17 @@ describe('ZonedDateTime', () => {
28952955
equal(ZonedDateTime.compare(clockBefore, clockAfter), 1);
28962956
equal(Temporal.PlainDateTime.compare(clockBefore.toPlainDateTime(), clockAfter.toPlainDateTime()), -1);
28972957
});
2958+
it('throws with invalid offset', () => {
2959+
throws(() => {
2960+
Temporal.ZonedDateTime.compare(zdt1, {
2961+
year: 2021,
2962+
month: 11,
2963+
day: 26,
2964+
offset: '+099:00',
2965+
timeZone: 'Europe/London'
2966+
});
2967+
}, RangeError);
2968+
});
28982969
});
28992970
});
29002971

0 commit comments

Comments
 (0)