Skip to content

Commit

Permalink
Allow Unicode minus sign (U+2212) in ISO 8601 strings
Browse files Browse the repository at this point in the history
Wherever a sign character is needed (in 6-digit extended years and in
time zone offsets), also accept the Unicode minus sign as well as the
normal ASCII + and -.

Closes: #814
  • Loading branch information
ptomato committed Aug 24, 2020
1 parent dd6b1f7 commit c4d30c5
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 16 deletions.
17 changes: 11 additions & 6 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ export const ES = ObjectAssign({}, ES2019, {
const regex = zoneRequired ? PARSE.absolute : PARSE.datetime;
const match = regex.exec(isoString);
if (!match) throw new RangeError(`invalid ISO 8601 string: ${isoString}`);
const year = ES.ToInteger(match[1]);
let yearString = match[1];
if (yearString[0] === '\u2212') yearString = `-${yearString.slice(1)}`;
const year = ES.ToInteger(yearString);
const month = ES.ToInteger(match[2] || match[4]);
const day = ES.ToInteger(match[3] || match[5]);
const hour = ES.ToInteger(match[6]);
Expand All @@ -107,8 +109,9 @@ export const ES = ObjectAssign({}, ES2019, {
const millisecond = ES.ToInteger(fraction.slice(0, 3));
const microsecond = ES.ToInteger(fraction.slice(3, 6));
const nanosecond = ES.ToInteger(fraction.slice(6, 9));
const offset = `${match[14]}:${match[15] || '00'}`;
let ianaName = match[16];
const offsetSign = match[14] === '-' || match[14] === '\u2212' ? '-' : '+';
const offset = `${offsetSign}${match[15]}:${match[16] || '00'}`;
let ianaName = match[17];
if (ianaName) {
try {
// Canonicalize name if it is an IANA link name or is capitalized wrong
Expand All @@ -118,7 +121,7 @@ export const ES = ObjectAssign({}, ES2019, {
}
}
const zone = match[13] ? 'UTC' : ianaName || offset;
const calendar = match[17] || null;
const calendar = match[18] || null;
return {
year,
month,
Expand Down Expand Up @@ -167,7 +170,9 @@ export const ES = ObjectAssign({}, ES2019, {
const match = PARSE.yearmonth.exec(isoString);
let year, month, calendar, refISODay;
if (match) {
year = ES.ToInteger(match[1]);
let yearString = match[1];
if (yearString[0] === '\u2212') yearString = `-${yearString.slice(1)}`;
year = ES.ToInteger(yearString);
month = ES.ToInteger(match[2]);
calendar = match[3] || null;
} else {
Expand Down Expand Up @@ -1572,7 +1577,7 @@ const OFFSET = new RegExp(`^${PARSE.offset.source}$`);
function parseOffsetString(string) {
const match = OFFSET.exec(String(string));
if (!match) return null;
const sign = match[1] === '-' ? -1 : +1;
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
const hours = +match[2];
const minutes = +(match[3] || 0);
return sign * (hours * 60 + minutes) * 60 * 1e9;
Expand Down
6 changes: 3 additions & 3 deletions polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const yearpart = /(?:[+-]\d{6}|\d{4})/;
const yearpart = /(?:[+-\u2212]\d{6}|\d{4})/;
const datesplit = new RegExp(`(${yearpart.source})(?:-(\\d{2})-(\\d{2})|(\\d{2})(\\d{2}))`);
const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/;
const zonesplit = /(?:([zZ])|(?:([+-]\d{2})(?::?(\d{2}))?(?:\[(?!c=)([^\]\s]*)?\])?))/;
export const offset = /([+-\u2212])([0-2][0-9])(?::?([0-5][0-9]))?/;
const zonesplit = new RegExp(`(?:([zZ])|(?:${offset.source}?(?:\\[(?!c=)([^\\]\\s]*)?\\])?))`);
const calendar = /\[c=([^\]\s]+)\]/;

export const absolute = new RegExp(
Expand All @@ -23,5 +24,4 @@ export const time = new RegExp(`^${timesplit.source}(?:${zonesplit.source})?(?:$
export const yearmonth = new RegExp(`^(${yearpart.source})-?(\\d{2})$`);
export const monthday = /^(?:--)?(\d{2})-?(\d{2})$/;

export const offset = /([+-])([0-2][0-9])(?::?([0-5][0-9]))?/;
export const duration = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?!$)(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:[.,](\d{1,9}))?S)?)?$/i;
2 changes: 1 addition & 1 deletion polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const OFFSET = new RegExp(`^${REGEX.offset.source}$`);
function parseOffsetString(string) {
const match = OFFSET.exec(String(string));
if (!match) return null;
const sign = match[1] === '-' ? -1 : +1;
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
const hours = +match[2];
const minutes = +(match[3] || 0);
return sign * (hours * 60 + minutes) * 60 * 1e9;
Expand Down
4 changes: 4 additions & 0 deletions polyfill/test/absolute.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ describe('Absolute', () => {
it('variant decimal separator', () => {
equal(`${Absolute.from('1976-11-18T15:23:30,12Z')}`, '1976-11-18T15:23:30.120Z');
});
it('variant minus sign', () => {
equal(`${Absolute.from('1976-11-18T15:23:30.12\u221202:00')}`, '1976-11-18T17:23:30.120Z');
equal(`${Absolute.from('\u2212009999-11-18T15:23:30.12Z')}`, '-009999-11-18T15:23:30.120Z');
});
it('mixture of basic and extended format', () => {
equal(`${Absolute.from('19761118T15:23:30.1+00:00')}`, '1976-11-18T15:23:30.100Z');
equal(`${Absolute.from('1976-11-18T152330.1+00:00')}`, '1976-11-18T15:23:30.100Z');
Expand Down
4 changes: 4 additions & 0 deletions polyfill/test/datetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ describe('DateTime', () => {
it('variant decimal separator', () => {
equal(`${DateTime.from('1976-11-18T15:23:30,12Z')}`, '1976-11-18T15:23:30.120');
});
it('variant minus sign', () => {
equal(`${DateTime.from('1976-11-18T15:23:30.12\u221202:00')}`, '1976-11-18T15:23:30.120');
equal(`${DateTime.from('\u2212009999-11-18T15:23:30.12')}`, '-009999-11-18T15:23:30.120');
});
it('mixture of basic and extended format', () => {
equal(`${DateTime.from('1976-11-18T152330.1+00:00')}`, '1976-11-18T15:23:30.100');
equal(`${DateTime.from('19761118T15:23:30.1+00:00')}`, '1976-11-18T15:23:30.100');
Expand Down
24 changes: 24 additions & 0 deletions polyfill/test/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ describe('fromString regex', () => {
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]);
Expand Down Expand Up @@ -112,6 +117,11 @@ describe('fromString regex', () => {
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]);
Expand Down Expand Up @@ -164,6 +174,8 @@ describe('fromString regex', () => {
'19761118T152330',
'19761118T152330.1234'
].forEach((str) => test(str, [1976, 11, 18]));
// Unicode minus sign
test('\u2212009999-11-18', [-9999, 11, 18]);
// Representations with reduced precision
test('1976-11-18T15', [1976, 11, 18]);
// Date-only forms
Expand Down Expand Up @@ -277,6 +289,8 @@ describe('fromString regex', () => {
// Representations with reduced precision
'1976-11-18T15'
].forEach((str) => test(str, [1976, 11]));
// Unicode minus sign
test('\u2212009999-11-18T15:23:30.1234Z', [-9999, 11]);
// Date-only forms
test('1976-11-18', [1976, 11]);
test('19761118', [1976, 11]);
Expand Down Expand Up @@ -331,6 +345,8 @@ describe('fromString regex', () => {
[
// Comma decimal separator
'1976-11-18T15:23:30,1234',
// Unicode minus sign
'\u2212009999-11-18',
// Mixture of basic and extended format
'1976-11-18T152330',
'1976-11-18T152330.1234',
Expand Down Expand Up @@ -396,6 +412,8 @@ describe('fromString regex', () => {
generateTest('1976-11-18T15:23', 'z', 'UTC');
// Comma decimal separator
test('1976-11-18T15:23:30,1234Z', 'UTC');
// Unicode minus sign
['\u221204:00', '\u221204', '\u22120400'].forEach((offset) => test(`1976-11-18T15:23${offset}`, '-04:00'));
[
// Mixture of basic and extended format
'1976-11-18T152330',
Expand Down Expand Up @@ -425,6 +443,12 @@ describe('fromString regex', () => {
test('-03:00', '-03:00');
test('+03', '+03:00');
test('-03', '-03:00');
test('\u22120000', '+00:00');
test('\u221200:00', '+00:00');
test('\u221200', '+00:00');
test('\u22120300', '-03:00');
test('\u221203:00', '-03:00');
test('\u221203', '-03:00');
// Representations with calendar
test('1976-11-18T15:23:30.123456789Z[c=iso8601]', 'UTC');
test('1976-11-18T15:23:30.123456789-04:00[c=iso8601]', '-04:00');
Expand Down
3 changes: 3 additions & 0 deletions polyfill/test/time.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ describe('Time', () => {
it('variant decimal separator', () => {
equal(`${Time.from('1976-11-18T15:23:30,12Z')}`, '15:23:30.120');
});
it('variant minus sign', () => {
equal(`${Time.from('1976-11-18T15:23:30.12\u221202:00')}`, '15:23:30.120');
});
it('basic format', () => {
equal(`${Time.from('152330')}`, '15:23:30');
equal(`${Time.from('152330.1')}`, '15:23:30.100');
Expand Down
12 changes: 12 additions & 0 deletions polyfill/test/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ describe('TimeZone', () => {
test('+0330');
test('-0650');
test('-08');
test('\u221201:00');
test('\u22120650');
test('\u221208');
test('Europe/Vienna');
test('America/New_York');
test('Africa/CAIRO'); // capitalization
Expand All @@ -63,6 +66,9 @@ describe('TimeZone', () => {
test('+0330', '+03:30');
test('-0650', '-06:50');
test('-08', '-08:00');
test('\u221201:00', '-01:00');
test('\u22120650', '-06:50');
test('\u221208', '-08:00');
test('Europe/Vienna');
test('America/New_York');
test('Africa/CAIRO', 'Africa/Cairo');
Expand All @@ -79,6 +85,9 @@ describe('TimeZone', () => {
test('+0330');
test('-0650');
test('-08');
test('\u221201:00');
test('\u22120650');
test('\u221208');
test('Europe/Vienna');
test('America/New_York');
test('Africa/CAIRO');
Expand All @@ -102,6 +111,9 @@ describe('TimeZone', () => {
test('1994-11-05T08:15:30-05:00', '-05:00');
test('1994-11-05T08:15:30-05:00[America/New_York]', 'America/New_York');
test('1994-11-05T08:15:30-05[America/New_York]', 'America/New_York');
test('1994-11-05T08:15:30\u221205:00', '-05:00');
test('1994-11-05T08:15:30\u221205:00[America/New_York]', 'America/New_York');
test('1994-11-05T08:15:30\u221205[America/New_York]', 'America/New_York');
test('1994-11-05T13:15:30Z', 'UTC');
function test(isoString, name) {
const tz = Temporal.TimeZone.from(isoString);
Expand Down
12 changes: 9 additions & 3 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function seq(...productions) {
// Grammar productions, based on the grammar in RFC 3339

// characters
const sign = character('+-');
const sign = character('+-');
const decimalSeparator = character('.,');
const daysDesignator = character('Dd');
const hoursDesignator = character('Hh');
Expand All @@ -172,7 +172,10 @@ const utcDesignator = withCode(character('Zz'), (data) => {

const dateFourDigitYear = repeat(4, digit());
const dateExtendedYear = seq(sign, repeat(6, digit()));
const dateYear = withCode(choice(dateFourDigitYear, dateExtendedYear), (data, result) => (data.year = +result));
const dateYear = withCode(
choice(dateFourDigitYear, dateExtendedYear),
(data, result) => (data.year = +result.replace('\u2212', '-'))
);
const dateMonth = withCode(zeroPaddedInclusive(1, 12, 2), (data, result) => (data.month = +result));
const dateDay = withCode(zeroPaddedInclusive(1, 31, 2), (data, result) => (data.day = +result));

Expand All @@ -189,7 +192,10 @@ const timeFractionalPart = withCode(between(1, 9, digit()), (data, result) => {
data.nanosecond = +fraction.slice(6, 9);
});
const timeFraction = seq(decimalSeparator, timeFractionalPart);
const timeZoneUTCOffsetSign = withCode(sign, (data, result) => (data.offsetSign = result));
const timeZoneUTCOffsetSign = withCode(
sign,
(data, result) => (data.offsetSign = result === '-' || result === '\u2212' ? '-' : '+')
);
const timeZoneUTCOffsetHour = withCode(zeroPaddedInclusive(0, 23, 2), (data, result) => (data.offsetHour = +result));
const timeZoneUTCOffsetMinute = withCode(
zeroPaddedInclusive(0, 59, 2),
Expand Down
4 changes: 4 additions & 0 deletions polyfill/test/yearmonth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ describe('YearMonth', () => {
equal(`${YearMonth.from('197611')}`, '1976-11');
equal(`${YearMonth.from('+00197611')}`, '1976-11');
});
it('variant minus sign', () => {
equal(`${YearMonth.from('\u2212009999-11')}`, '-009999-11');
equal(`${YearMonth.from('1976-11-18T15:23:30.1\u221202:00')}`, '1976-11');
});
it('mixture of basic and extended format', () => {
equal(`${YearMonth.from('1976-11-18T152330.1+00:00')}`, '1976-11');
equal(`${YearMonth.from('19761118T15:23:30.1+00:00')}`, '1976-11');
Expand Down
9 changes: 6 additions & 3 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,10 @@ <h1>ISO 8601 grammar</h1>
NonzeroDigit : one of
`1` `2` `3` `4` `5` `6` `7` `8` `9`

Sign : one of
`+` `-`
Sign :
`+`
`-`
U+2212

DecimalSeparator : one of
`.` `,`
Expand Down Expand Up @@ -447,6 +449,7 @@ <h1>ParseISODateTime ( _isoString_ )</h1>
1. Assert: Type(_isoString_) is String.
1. Let _year_, _month_, _day_, _hour_, _minute_, _second_, and _fraction_ be the parts of _isoString_ produced respectively by the |DateYear|, |DateMonth|, |DateDay|, |TimeHour|, |TimeMinute|, |TimeSecond|, and |TimeFractionalPart| productions, or *undefined* if not present.
1. Let _year_ be the part of _isoString_ produced by the |DateYear| production.
1. If the first code unit of _year_ is 0x2212 (MINUS SIGN), replace it with the code unit 0x002D (HYPHEN-MINUS).
1. Set _year_ to ! ToInteger(_year_).
1. If _month_ is *undefined*, then
1. Set _month_ to 1.
Expand Down Expand Up @@ -667,7 +670,7 @@ <h1>ParseTemporalTimeZoneString ( _isoString_ )</h1>
1. If _hour_ is not *undefined*, then
1. Assert: _sign_ is not *undefined*.
1. Set _hour_ to ! ToInteger(_hour_).
1. If _sign_ = `"-"`, then
1. If _sign_ is the code unit 0x002D (HYPHEN-MINUS) or the code unit 0x2212 (MINUS SIGN), then
1. Set _sign_ to −1.
1. Else,
1. Set _sign_ to 1.
Expand Down

0 comments on commit c4d30c5

Please sign in to comment.