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

Normative: Allow annotations after YYYY-MM and MM-DD #2398

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const ObjectDefineProperty = Object.defineProperty;
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const ObjectIs = Object.is;
const ObjectEntries = Object.entries;
const StringPrototypeSlice = String.prototype.slice;

import bigInt from 'big-integer';
import Call from 'es-abstract/2020/Call.js';
Expand Down Expand Up @@ -388,18 +387,12 @@ export const ES = ObjectAssign({}, ES2020, {
return { hour, minute, second, millisecond, microsecond, nanosecond, calendar };
}
// Reject strings that are ambiguous with PlainMonthDay or PlainYearMonth.
// The calendar suffix is `[u-ca=${calendar}]`, i.e. calendar plus 7 characters,
// and must be stripped so presence of a calendar doesn't result in interpretation
// of otherwise ambiguous input as a time.
const isoStringWithoutCalendar = calendar
? ES.Call(StringPrototypeSlice, isoString, [0, -(calendar.length + 7)])
: isoString;
try {
const { month, day } = ES.ParseTemporalMonthDayString(isoStringWithoutCalendar);
const { month, day } = ES.ParseTemporalMonthDayString(isoString);
ES.RejectISODate(1972, month, day);
} catch {
try {
const { year, month } = ES.ParseTemporalYearMonthString(isoStringWithoutCalendar);
const { year, month } = ES.ParseTemporalYearMonthString(isoString);
ES.RejectISODate(year, month, 1);
} catch {
return { hour, minute, second, millisecond, microsecond, nanosecond, calendar };
Expand All @@ -417,6 +410,9 @@ export const ES = ObjectAssign({}, ES2020, {
year = ES.ToInteger(yearString);
month = ES.ToInteger(match[2]);
calendar = match[3];
if (calendar !== undefined && calendar !== 'iso8601') {
throw new RangeError('YYYY-MM format is only valid with iso8601 calendar');
}
} else {
let z;
({ year, month, calendar, day: referenceISODay, z } = ES.ParseISODateTime(isoString));
Expand All @@ -430,6 +426,10 @@ export const ES = ObjectAssign({}, ES2020, {
if (match) {
month = ES.ToInteger(match[1]);
day = ES.ToInteger(match[2]);
calendar = match[10];
if (calendar !== undefined && calendar != 'iso8601') {
throw new RangeError('MM-DD format is only valid with iso8601 calendar');
}
} else {
let z;
({ month, day, calendar, year: referenceISOYear, z } = ES.ParseISODateTime(isoString));
Expand Down Expand Up @@ -1628,7 +1628,11 @@ export const ES = ObjectAssign({}, ES2020, {
try {
({ calendar } = ES.ParseISODateTime(identifier));
} catch {
throw new RangeError(`Invalid calendar: ${identifier}`);
try {
({ calendar } = ES.ParseTemporalYearMonthString(identifier));
} catch {
({ calendar } = ES.ParseTemporalMonthDayString(identifier));
}
}
if (!calendar) calendar = 'iso8601';
return new TemporalCalendar(calendar);
Expand Down
13 changes: 10 additions & 3 deletions polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,21 @@ export const zoneddatetime = new RegExp(

export const time = new RegExp(`^T?${timesplit.source}(?:${zonesplit.source})?(?:${calendar.source})?$`, 'i');

// The short forms of YearMonth and MonthDay are only for the ISO calendar.
// The short forms of YearMonth and MonthDay are only for the ISO calendar, but
// a calendar annotation is still allowed and will throw if not ISO.
// Non-ISO calendar YearMonth and MonthDay have to parse as a Temporal.PlainDate,
// with the reference fields.
// YYYYMM forbidden by ISO 8601 because ambiguous with YYMMDD, but allowed by
// RFC 3339 and we don't allow 2-digit years, so we allow it.
// Not ambiguous with HHMMSS because that requires a 'T' prefix
export const yearmonth = new RegExp(`^(${yearpart.source})-?(${monthpart.source})$`);
export const monthday = new RegExp(`^(?:--)?(${monthpart.source})-?(${daypart.source})$`);
// In YearMonth, a time zone annotation is allowed but no UTC offset, because
// YYYY-MM-UU is ambiguous with YYYY-MM-DD
export const yearmonth = new RegExp(
`^(${yearpart.source})-?(${monthpart.source})(?:\\[${timeZoneID.source}\\])?(?:${calendar.source})?$`
);
export const monthday = new RegExp(
`^(?:--)?(${monthpart.source})-?(${daypart.source})(?:${zonesplit.source})?(?:${calendar.source})?$`
);

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

Expand Down
21 changes: 17 additions & 4 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,10 @@ const timeSpec = seq(
choice([':', timeMinute, [':', timeSecond, [timeFraction]]], seq(timeMinute, [timeSecond, [timeFraction]]))
);
const timeSpecWithOptionalTimeZoneNotAmbiguous = withSyntaxConstraints(seq(timeSpec, [timeZone]), (result) => {
if (/^(?:(?!02-?30)(?:0[1-9]|1[012])-?(?:0[1-9]|[12][0-9]|30)|(?:0[13578]|10|12)-?31)$/.test(result)) {
if (/^(?:(?!02-?30)(?:0[1-9]|1[012])-?(?:0[1-9]|[12][0-9]|30)|(?:0[13578]|10|12)-?31)/.test(result)) {
throw new SyntaxError('valid PlainMonthDay');
}
if (/^(?![−-]000000)(?:[0-9]{4}|[+−-][0-9]{6})-?(?:0[1-9]|1[012])$/.test(result)) {
if (/^(?![−-]000000)(?:[0-9]{4}|[+−-][0-9]{6})-?(?:0[1-9]|1[012])/.test(result)) {
throw new SyntaxError('valid PlainYearMonth');
}
});
Expand All @@ -306,6 +306,19 @@ const calendarTime = choice(
seq(timeDesignator, timeSpec, [timeZone], [calendar]),
seq(timeSpecWithOptionalTimeZoneNotAmbiguous, [calendar])
);
const annotatedYearMonth = withSyntaxConstraints(
seq(dateSpecYearMonth, [timeZoneBracketedAnnotation], [calendar]),
(result, data) => {
if (data.calendar !== undefined && data.calendar !== 'iso8601') {
throw new SyntaxError('retry if YYYY-MM with non-ISO calendar');
}
}
);
const annotatedMonthDay = withSyntaxConstraints(seq(dateSpecMonthDay, [timeZone], [calendar]), (result, data) => {
if (data.calendar !== undefined && data.calendar !== 'iso8601') {
throw new SyntaxError('retry if MM-DD with non-ISO calendar');
}
});

const durationFractionalPart = withCode(between(1, 9, digit()), (data, result) => {
const fraction = result.padEnd(9, '0');
Expand Down Expand Up @@ -368,10 +381,10 @@ const goals = {
Date: calendarDateTime,
DateTime: calendarDateTime,
Duration: duration,
MonthDay: choice(dateSpecMonthDay, calendarDateTime),
MonthDay: choice(annotatedMonthDay, calendarDateTime),
Time: choice(calendarTime, calendarDateTimeTimeRequired),
TimeZone: choice(timeZoneIdentifier, seq(date, [timeSpecSeparator], timeZone, [calendar])),
YearMonth: choice(dateSpecYearMonth, calendarDateTime),
YearMonth: choice(annotatedYearMonth, calendarDateTime),
ZonedDateTime: zonedDateTime
};

Expand Down
29 changes: 23 additions & 6 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -1096,8 +1096,14 @@ <h1>ISO 8601 grammar</h1>
TimeHour `:` TimeMinute `:` TimeSecond TimeFraction?
TimeHour TimeMinute TimeSecond TimeFraction?

AmbiguousMonthDay :
ValidMonthDay TimeZone?

AmbiguousYearMonth :
DateSpecYearMonth TimeZoneBracketedAnnotation?

TimeSpecWithOptionalTimeZoneNotAmbiguous :
TimeSpec TimeZone? but not one of ValidMonthDay or DateSpecYearMonth
TimeSpec TimeZone? but not one of AmbiguousMonthDay or AmbiguousYearMonth

TimeSpecSeparator :
DateTimeSeparator TimeSpec
Expand All @@ -1115,6 +1121,12 @@ <h1>ISO 8601 grammar</h1>
CalendarDateTimeTimeRequired :
Date TimeSpecSeparator TimeZone? Calendar?

AnnotatedYearMonth:
DateSpecYearMonth TimeZoneBracketedAnnotation? Calendar?

AnnotatedMonthDay:
DateSpecMonthDay TimeZone? Calendar?

DurationWholeSeconds :
DecimalDigits[~Sep]

Expand Down Expand Up @@ -1195,15 +1207,15 @@ <h1>ISO 8601 grammar</h1>
Duration

TemporalMonthDayString :
DateSpecMonthDay
AnnotatedMonthDay
CalendarDateTime

TemporalTimeString :
CalendarTime
CalendarDateTimeTimeRequired

TemporalYearMonthString :
DateSpecYearMonth
AnnotatedYearMonth
CalendarDateTime

TemporalZonedDateTimeString :
Expand All @@ -1214,8 +1226,8 @@ <h1>ISO 8601 grammar</h1>
TemporalInstantString
CalendarDateTime
CalendarTime
DateSpecYearMonth
DateSpecMonthDay
AnnotatedYearMonth
AnnotatedMonthDay
</emu-grammar>
<ul>
<li>
Expand All @@ -1237,8 +1249,13 @@ <h1>
<emu-note>The value of ! ToIntegerOrInfinity(*undefined*) is 0.</emu-note>
<emu-alg>
1. Let _parseResult_ be ~empty~.
1. For each nonterminal _goal_ of &laquo; |TemporalDateTimeString|, |TemporalInstantString|, |TemporalMonthDayString|, |TemporalTimeString|, |TemporalYearMonthString|, |TemporalZonedDateTimeString| &raquo;, do
1. For each nonterminal _goal_ of &laquo; |TemporalDateTimeString|, |TemporalInstantString|, |TemporalTimeString|, |TemporalZonedDateTimeString| &raquo;, do
1. If _parseResult_ is not a Parse Node, set _parseResult_ to ParseText(StringToCodePoints(_isoString_), _goal_).
1. For each nonterminal _goal_ of &laquo; |TemporalMonthDayString|, |TemporalYearMonthString| &raquo;, do
1. If _parseResult_ is not a Parse Node, then
1. Set _parseResult_ to ParseText(StringToCodePoints(_isoString_), _goal_).
1. If _parseResult_ contains a |CalendarName| Parse Node _c_, and CodePointsToString(_c_) is not *"iso8601"*, then
1. Throw a *RangeError* exception.
1. If _parseResult_ is not a Parse Node, throw a *RangeError* exception.
1. Let each of _year_, _month_, _day_, _hour_, _minute_, _second_, _fSeconds_, and _calendar_ be the source text matched by the respective |DateYear|, |DateMonth|, |DateDay|, |TimeHour|, |TimeMinute|, |TimeSecond|, |TimeFraction|, and |CalendarName| Parse Node contained within _parseResult_, or an empty sequence of code points if not present.
1. If the first code point of _year_ is U+2212 (MINUS SIGN), replace the first code point with U+002D (HYPHEN-MINUS).
Expand Down