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

Tighten ISO parsing of month and day values #1836

Merged
merged 1 commit into from
Sep 20, 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
10 changes: 7 additions & 3 deletions polyfill/lib/regex.mjs
Expand Up @@ -8,7 +8,11 @@ const calComponent = /[A-Za-z0-9]{3,8}/;
export const calendarID = new RegExp(`(?:${calComponent.source}(?:-${calComponent.source})*)`);

const yearpart = /(?:[+\u2212-]\d{6}|\d{4})/;
export const datesplit = new RegExp(`(${yearpart.source})(?:-(\\d{2})-(\\d{2})|(\\d{2})(\\d{2}))`);
const monthpart = /(?:0[1-9]|1[0-2])/;
const daypart = /(?:0[1-9]|[12]\d|3[01])/;
export const datesplit = new RegExp(
`(${yearpart.source})(?:-(${monthpart.source})-(${daypart.source})|(${monthpart.source})(${daypart.source}))`
);
const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/;
export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/;
const zonesplit = new RegExp(`(?:([zZ])|(?:${offset.source})?)(?:\\[(${timeZoneID.source})\\])?`);
Expand All @@ -30,8 +34,8 @@ export const time = new RegExp(`^${timesplit.source}(?:${zonesplit.source})?(?:$
// with the reference fields.
// YYYYMM forbidden by ISO 8601, but since it is not ambiguous with anything
// else we could parse in a YearMonth context, we allow it
export const yearmonth = new RegExp(`^(${yearpart.source})-?(\\d{2})$`);
export const monthday = /^(?:--)?(\d{2})-?(\d{2})$/;
export const yearmonth = new RegExp(`^(${yearpart.source})-?(${monthpart.source})$`);
export const monthday = new RegExp(`^(?:--)?(${monthpart.source})-?(${daypart.source})$`);

const fraction = /(\d+)(?:[.,](\d{1,9}))?/;

Expand Down
206 changes: 121 additions & 85 deletions polyfill/test/regex.mjs
Expand Up @@ -33,47 +33,65 @@ describe('fromString regex', () => {
test(`${dateTimeString}:30${zoneString}`, components.slice(0, 6));
test(`${dateTimeString}:30.123456789${zoneString}`, components);
}
// Without time component
test('2020-01-01Z', [2020, 1, 1, 0, 0, 0]);
// Time separators
['T', 't', ' '].forEach((timeSep) =>
generateTest(`1976-11-18${timeSep}15:23`, 'Z', [1976, 11, 18, 15, 23, 30, 123, 456, 789])
);
// Time zone with bracketed name
['+01:00', '+01', '+0100', '+01:00:00', '+010000', '+01:00:00.000000000', '+010000.0'].forEach((zoneString) => {
generateTest('1976-11-18T15:23', `${zoneString}[Europe/Vienna]`, [1976, 11, 18, 14, 23, 30, 123, 456, 789]);
generateTest('1976-11-18T15:23', `+01:00[${zoneString}]`, [1976, 11, 18, 14, 23, 30, 123, 456, 789]);
describe('valid', () => {
// Without time component
test('2020-01-01Z', [2020, 1, 1, 0, 0, 0]);
// Time separators
['T', 't', ' '].forEach((timeSep) =>
generateTest(`1976-11-18${timeSep}15:23`, 'Z', [1976, 11, 18, 15, 23, 30, 123, 456, 789])
);
// Time zone with bracketed name
['+01:00', '+01', '+0100', '+01:00:00', '+010000', '+01:00:00.000000000', '+010000.0'].forEach((zoneString) => {
generateTest('1976-11-18T15:23', `${zoneString}[Europe/Vienna]`, [1976, 11, 18, 14, 23, 30, 123, 456, 789]);
generateTest('1976-11-18T15:23', `+01:00[${zoneString}]`, [1976, 11, 18, 14, 23, 30, 123, 456, 789]);
});
// Time zone with only offset
['-04:00', '-04', '-0400', '-04:00:00', '-040000', '-04:00:00.000000000', '-040000.0'].forEach((zoneString) =>
generateTest('1976-11-18T15:23', zoneString, [1976, 11, 18, 19, 23, 30, 123, 456, 789])
);
// Various numbers of decimal places
test('1976-11-18T15:23:30.1Z', [1976, 11, 18, 15, 23, 30, 100]);
test('1976-11-18T15:23:30.12Z', [1976, 11, 18, 15, 23, 30, 120]);
test('1976-11-18T15:23:30.123Z', [1976, 11, 18, 15, 23, 30, 123]);
test('1976-11-18T15:23:30.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('1976-11-18T15:23:30.12345Z', [1976, 11, 18, 15, 23, 30, 123, 450]);
test('1976-11-18T15:23:30.123456Z', [1976, 11, 18, 15, 23, 30, 123, 456]);
test('1976-11-18T15:23:30.1234567Z', [1976, 11, 18, 15, 23, 30, 123, 456, 700]);
test('1976-11-18T15:23:30.12345678Z', [1976, 11, 18, 15, 23, 30, 123, 456, 780]);
// Lowercase UTC designator
generateTest('1976-11-18T15:23', 'z', [1976, 11, 18, 15, 23, 30, 123, 456, 789]);
// Comma decimal separator
test('1976-11-18T15:23:30,1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Unicode minus sign
['\u221204:00', '\u221204', '\u22120400'].forEach((offset) =>
test(`1976-11-18T15:23:30.1234${offset}`, [1976, 11, 18, 19, 23, 30, 123, 400])
);
test('\u2212009999-11-18T15:23:30.1234Z', [-9999, 11, 18, 15, 23, 30, 123, 400]);
// Mixture of basic and extended format
test('1976-11-18T152330Z', [1976, 11, 18, 15, 23, 30]);
test('1976-11-18T152330.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('19761118T15:23:30Z', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330Z', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Representations with reduced precision
test('1976-11-18T15Z', [1976, 11, 18, 15]);
});
describe('not valid', () => {
// Invalid month values
['00', '13', '20', '99'].forEach((monthString) => {
const invalidIsoString = `1976-${monthString}-18T00:00:00Z`;
it(invalidIsoString, () => {
throws(() => Temporal.Instant.from(invalidIsoString), RangeError);
});
});
// Invalid day values
['00', '32', '40', '99'].forEach((dayString) => {
const invalidIsoString = `1976-11-${dayString}T00:00:00Z`;
it(invalidIsoString, () => {
throws(() => Temporal.Instant.from(invalidIsoString), RangeError);
});
});
});
// Time zone with only offset
['-04:00', '-04', '-0400', '-04:00:00', '-040000', '-04:00:00.000000000', '-040000.0'].forEach((zoneString) =>
generateTest('1976-11-18T15:23', zoneString, [1976, 11, 18, 19, 23, 30, 123, 456, 789])
);
// Various numbers of decimal places
test('1976-11-18T15:23:30.1Z', [1976, 11, 18, 15, 23, 30, 100]);
test('1976-11-18T15:23:30.12Z', [1976, 11, 18, 15, 23, 30, 120]);
test('1976-11-18T15:23:30.123Z', [1976, 11, 18, 15, 23, 30, 123]);
test('1976-11-18T15:23:30.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('1976-11-18T15:23:30.12345Z', [1976, 11, 18, 15, 23, 30, 123, 450]);
test('1976-11-18T15:23:30.123456Z', [1976, 11, 18, 15, 23, 30, 123, 456]);
test('1976-11-18T15:23:30.1234567Z', [1976, 11, 18, 15, 23, 30, 123, 456, 700]);
test('1976-11-18T15:23:30.12345678Z', [1976, 11, 18, 15, 23, 30, 123, 456, 780]);
// Lowercase UTC designator
generateTest('1976-11-18T15:23', 'z', [1976, 11, 18, 15, 23, 30, 123, 456, 789]);
// Comma decimal separator
test('1976-11-18T15:23:30,1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Unicode minus sign
['\u221204:00', '\u221204', '\u22120400'].forEach((offset) =>
test(`1976-11-18T15:23:30.1234${offset}`, [1976, 11, 18, 19, 23, 30, 123, 400])
);
test('\u2212009999-11-18T15:23:30.1234Z', [-9999, 11, 18, 15, 23, 30, 123, 400]);
// Mixture of basic and extended format
test('1976-11-18T152330Z', [1976, 11, 18, 15, 23, 30]);
test('1976-11-18T152330.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('19761118T15:23:30Z', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330Z', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330.1234Z', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Representations with reduced precision
test('1976-11-18T15Z', [1976, 11, 18, 15]);
});

describe('datetime', () => {
Expand All @@ -99,51 +117,69 @@ describe('fromString regex', () => {
test(`${dateTimeString}:30${zoneString}`, components.slice(0, 6));
test(`${dateTimeString}:30.123456789${zoneString}`, components);
}
// Time separators
['T', 't', ' '].forEach((timeSep) => generateTest(`1976-11-18${timeSep}15:23`, ''));
// Various forms of time zone
[
'+0100[Europe/Vienna]',
'+01:00[Europe/Vienna]',
'[Europe/Vienna]',
'+01:00[Custom/Vienna]',
'-0400',
'-04:00',
'-04:00:00.000000000',
'+010000.0[Europe/Vienna]',
'+01:00[+01:00]',
'+01:00[+0100]',
''
].forEach((zoneString) => generateTest('1976-11-18T15:23', zoneString));
// Various numbers of decimal places
test('1976-11-18T15:23:30.1', [1976, 11, 18, 15, 23, 30, 100]);
test('1976-11-18T15:23:30.12', [1976, 11, 18, 15, 23, 30, 120]);
test('1976-11-18T15:23:30.123', [1976, 11, 18, 15, 23, 30, 123]);
test('1976-11-18T15:23:30.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('1976-11-18T15:23:30.12345', [1976, 11, 18, 15, 23, 30, 123, 450]);
test('1976-11-18T15:23:30.123456', [1976, 11, 18, 15, 23, 30, 123, 456]);
test('1976-11-18T15:23:30.1234567', [1976, 11, 18, 15, 23, 30, 123, 456, 700]);
test('1976-11-18T15:23:30.12345678', [1976, 11, 18, 15, 23, 30, 123, 456, 780]);
// Comma decimal separator
test('1976-11-18T15:23:30,1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Unicode minus sign
['\u221204:00', '\u221204', '\u22120400'].forEach((offset) =>
test(`1976-11-18T15:23:30.1234${offset}`, [1976, 11, 18, 15, 23, 30, 123, 400])
);
test('\u2212009999-11-18T15:23:30.1234', [-9999, 11, 18, 15, 23, 30, 123, 400]);
// Mixture of basic and extended format
test('1976-11-18T152330', [1976, 11, 18, 15, 23, 30]);
test('1976-11-18T152330.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('19761118T15:23:30', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Representations with reduced precision
test('1976-11-18T15', [1976, 11, 18, 15]);
test('1976-11-18', [1976, 11, 18]);
// Representations with calendar
['', 'Z', '+01:00[Europe/Vienna]', '+01:00[Custom/Vienna]', '[Europe/Vienna]'].forEach((zoneString) =>
test(`1976-11-18T15:23:30.123456789${zoneString}[u-ca=iso8601]`, [1976, 11, 18, 15, 23, 30, 123, 456, 789])
);
describe('valid', () => {
// Time separators
['T', 't', ' '].forEach((timeSep) => generateTest(`1976-11-18${timeSep}15:23`, ''));
// Various forms of time zone
[
'+0100[Europe/Vienna]',
'+01:00[Europe/Vienna]',
'[Europe/Vienna]',
'+01:00[Custom/Vienna]',
'-0400',
'-04:00',
'-04:00:00.000000000',
'+010000.0[Europe/Vienna]',
'+01:00[+01:00]',
'+01:00[+0100]',
''
].forEach((zoneString) => generateTest('1976-11-18T15:23', zoneString));
// Various numbers of decimal places
test('1976-11-18T15:23:30.1', [1976, 11, 18, 15, 23, 30, 100]);
test('1976-11-18T15:23:30.12', [1976, 11, 18, 15, 23, 30, 120]);
test('1976-11-18T15:23:30.123', [1976, 11, 18, 15, 23, 30, 123]);
test('1976-11-18T15:23:30.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('1976-11-18T15:23:30.12345', [1976, 11, 18, 15, 23, 30, 123, 450]);
test('1976-11-18T15:23:30.123456', [1976, 11, 18, 15, 23, 30, 123, 456]);
test('1976-11-18T15:23:30.1234567', [1976, 11, 18, 15, 23, 30, 123, 456, 700]);
test('1976-11-18T15:23:30.12345678', [1976, 11, 18, 15, 23, 30, 123, 456, 780]);
// Comma decimal separator
test('1976-11-18T15:23:30,1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Unicode minus sign
['\u221204:00', '\u221204', '\u22120400'].forEach((offset) =>
test(`1976-11-18T15:23:30.1234${offset}`, [1976, 11, 18, 15, 23, 30, 123, 400])
);
test('\u2212009999-11-18T15:23:30.1234', [-9999, 11, 18, 15, 23, 30, 123, 400]);
// Mixture of basic and extended format
test('1976-11-18T152330', [1976, 11, 18, 15, 23, 30]);
test('1976-11-18T152330.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
test('19761118T15:23:30', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330', [1976, 11, 18, 15, 23, 30]);
test('19761118T152330.1234', [1976, 11, 18, 15, 23, 30, 123, 400]);
// Representations with reduced precision
test('1976-11-18T15', [1976, 11, 18, 15]);
test('1976-11-18', [1976, 11, 18]);
// Representations with calendar
['', 'Z', '+01:00[Europe/Vienna]', '+01:00[Custom/Vienna]', '[Europe/Vienna]'].forEach((zoneString) =>
test(`1976-11-18T15:23:30.123456789${zoneString}[u-ca=iso8601]`, [1976, 11, 18, 15, 23, 30, 123, 456, 789])
);
});
describe('not valid', () => {
// Invalid month values
['00', '13', '20', '99'].forEach((monthString) => {
const invalidIsoString = `1976-${monthString}-18T00:00:00`;
it(invalidIsoString, () => {
throws(() => Temporal.PlainDateTime.from(invalidIsoString), RangeError);
});
});
// Invalid day values
['00', '32', '40', '99'].forEach((dayString) => {
const invalidIsoString = `1976-11-${dayString}T00:00:00`;
it(invalidIsoString, () => {
throws(() => Temporal.PlainDateTime.from(invalidIsoString), RangeError);
});
});
});
});

describe('date', () => {
Expand Down