Skip to content

Commit

Permalink
Throw RangeError for invalid offset strings
Browse files Browse the repository at this point in the history
  • Loading branch information
justingrant committed Dec 15, 2021
1 parent 500b4c9 commit d5ada8b
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 16 deletions.
24 changes: 16 additions & 8 deletions lib/ecmascript.ts
Expand Up @@ -458,7 +458,7 @@ function ParseTemporalTimeZoneString(stringIdent: string): Partial<{
let canonicalIdent = GetCanonicalTimeZoneIdentifier(stringIdent);
if (canonicalIdent) {
canonicalIdent = canonicalIdent.toString();
if (ParseOffsetString(canonicalIdent) !== null) return { offset: canonicalIdent };
if (TestTimeZoneOffsetString(canonicalIdent)) return { offset: canonicalIdent };
return { ianaName: canonicalIdent };
}
} catch {
Expand Down Expand Up @@ -515,7 +515,7 @@ function ParseTemporalInstant(isoString: string) {
const epochNs = GetEpochFromISOParts(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ParseOffsetString(offset);
const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset);
return JSBI.subtract(epochNs, JSBI.BigInt(offsetNs));
}

Expand Down Expand Up @@ -996,7 +996,7 @@ export function ToRelativeTemporalObject(options: {
if (timeZone) {
timeZone = ToTemporalTimeZone(timeZone);
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(ToString(offset));
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(ToString(offset));
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1663,7 +1663,7 @@ export function ToTemporalZonedDateTime(
matchMinute = true; // ISO strings may specify offset with less precision
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset);
const disambiguation = ToTemporalDisambiguation(options);
const offsetOpt = ToTemporalOffset(options, 'reject');
const epochNanoseconds = InterpretISODateTimeOffset(
Expand Down Expand Up @@ -2682,9 +2682,15 @@ export function TemporalZonedDateTimeToString(
return result;
}

export function ParseOffsetString(string: string): number {
export function TestTimeZoneOffsetString(string: string) {
return OFFSET.test(StringCtor(string));
}

export function ParseTimeZoneOffsetString(string: string): number {
const match = OFFSET.exec(StringCtor(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 @@ -2694,8 +2700,10 @@ export function ParseOffsetString(string: string): number {
}

export function GetCanonicalTimeZoneIdentifier(timeZoneIdentifier: string): string {
const offsetNs = ParseOffsetString(timeZoneIdentifier);
if (offsetNs !== null) return FormatTimeZoneOffsetString(offsetNs);
if (TestTimeZoneOffsetString(timeZoneIdentifier)) {
const offsetNs = ParseTimeZoneOffsetString(timeZoneIdentifier);
return FormatTimeZoneOffsetString(offsetNs);
}
const formatter = getIntlDateTimeFormatEnUsForTimeZone(StringCtor(timeZoneIdentifier));
return formatter.resolvedOptions().timeZone;
}
Expand Down
14 changes: 7 additions & 7 deletions lib/timezone.ts
Expand Up @@ -50,9 +50,9 @@ export class TimeZone implements Temporal.TimeZone {
const instant = ES.ToTemporalInstant(instantParam);
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);
}
getOffsetStringFor(instantParam: Params['getOffsetStringFor'][0]): Return['getOffsetStringFor'] {
Expand Down Expand Up @@ -84,8 +84,7 @@ export class TimeZone implements Temporal.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 @@ -98,6 +97,7 @@ export class TimeZone implements Temporal.TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
return [new Instant(JSBI.subtract(epochNs, JSBI.BigInt(offsetNs)))];
}

Expand All @@ -121,7 +121,7 @@ export class TimeZone implements Temporal.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 @@ -136,7 +136,7 @@ export class TimeZone implements Temporal.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 lib/zoneddatetime.ts
Expand Up @@ -230,7 +230,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
fields = ES.PrepareTemporalFields(fields, entries as any);
const { 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 test/duration.mjs
Expand Up @@ -687,6 +687,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 @@ -930,6 +939,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 @@ -1514,6 +1532,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 @@ -1853,6 +1881,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 @@ -1949,6 +1987,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 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 @@ -2895,6 +2955,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

0 comments on commit d5ada8b

Please sign in to comment.