Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 33 additions & 58 deletions polyfill/lib/calendar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,16 @@ import {
MathFloor,
MathTrunc,
MathMax,
NumberIsNaN,
MathSign,
ObjectAssign,
ObjectEntries,
RegExpPrototypeExec,
RegExpPrototypeTest,
SetPrototypeAdd,
SetPrototypeValues,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
StringPrototypeNormalize,
StringPrototypePadStart,
StringPrototypeReplace,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeStartsWith,
StringPrototypeToLowerCase,
SymbolIterator,
WeakMapPrototypeGet,
Expand All @@ -61,6 +55,7 @@ import Type from 'es-abstract/2024/Type.js';

import * as ES from './ecmascript.mjs';
import { DefineIntrinsic } from './intrinsicclass.mjs';
import { CreateMonthCode, ParseMonthCode } from './monthcode.mjs';

function arrayFromSet(src) {
const valuesIterator = Call(SetPrototypeValues, src, []);
Expand Down Expand Up @@ -256,7 +251,7 @@ impl['iso8601'] = {
daysInWeek: 7,
monthsInYear: 12
};
if (requestedFields.monthCode) date.monthCode = buildMonthCode(month);
if (requestedFields.monthCode) date.monthCode = CreateMonthCode(month, false);
if (requestedFields.dayOfWeek) {
// https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Disparate_variation
const shiftedMonth = month + (month < 3 ? 10 : -2);
Expand Down Expand Up @@ -294,21 +289,6 @@ impl['iso8601'] = {
// proposal for ECMA-262. These calendars will be standardized as part of
// ECMA-402.

function monthCodeNumberPart(monthCode) {
if (!Call(StringPrototypeStartsWith, monthCode, ['M'])) {
throw new RangeErrorCtor(`Invalid month code: ${monthCode}. Month codes must start with M.`);
}
const month = +Call(StringPrototypeSlice, monthCode, [1]);
if (NumberIsNaN(month)) throw new RangeErrorCtor(`Invalid month code: ${monthCode}`);
return month;
}

function buildMonthCode(month, leap = false) {
const digitPart = Call(StringPrototypePadStart, `${month}`, [2, '0']);
const leapMarker = leap ? 'L' : '';
return `M${digitPart}${leapMarker}`;
}

/**
* Safely merge a month, monthCode pair into an integer month.
* If both are present, make sure they match.
Expand All @@ -323,16 +303,19 @@ function resolveNonLunisolarMonth(calendarDate, overflow = undefined, monthsPerY
// rely on this function to constrain/reject out-of-range `month` values.
if (overflow === 'reject') ES.RejectToRange(month, 1, monthsPerYear);
if (overflow === 'constrain') month = ES.ConstrainToRange(month, 1, monthsPerYear);
monthCode = buildMonthCode(month);
monthCode = CreateMonthCode(month, false);
} else {
const numberPart = monthCodeNumberPart(monthCode);
if (monthCode !== buildMonthCode(numberPart)) {
const { monthNumber, isLeapMonth } = ParseMonthCode(monthCode);
if (isLeapMonth) {
throw new RangeErrorCtor(`Invalid monthCode: ${monthCode}. Leap months do not exist in this calendar`);
}
if (monthCode !== CreateMonthCode(monthNumber, false)) {
throw new RangeErrorCtor(`Invalid month code: ${monthCode}`);
}
if (month !== undefined && month !== numberPart) {
if (month !== undefined && month !== monthNumber) {
throw new RangeErrorCtor(`monthCode ${monthCode} and month ${month} must match if both are present`);
}
month = numberPart;
month = monthNumber;
if (month < 1 || month > monthsPerYear) throw new RangeErrorCtor(`Invalid monthCode: ${monthCode}`);
}
return { ...calendarDate, month, monthCode };
Expand Down Expand Up @@ -607,9 +590,8 @@ const nonIsoHelperBase = {
`monthCode must be a string, not ${ES.Call(StringPrototypeToLowerCase, Type(monthCode), [])}`
);
}
if (!ES.Call(RegExpPrototypeTest, /^M([01]?\d)(L?)$/, [monthCode])) {
throw new RangeErrorCtor(`Invalid monthCode: ${monthCode}`);
}
const { monthNumber } = ParseMonthCode(monthCode);
if (monthNumber < 1 || monthNumber > 13) throw new RangeErrorCtor(`Invalid monthCode: ${monthCode}`);
}
if (this.hasEra) {
if ((calendarDate['era'] === undefined) !== (calendarDate['eraYear'] === undefined)) {
Expand Down Expand Up @@ -1086,9 +1068,9 @@ const helperHebrew = ObjectAssign({}, nonIsoHelperBase, {
},
getMonthCode(year, month) {
if (this.inLeapYear({ year })) {
return month === 6 ? buildMonthCode(5, true) : buildMonthCode(month < 6 ? month : month - 1);
return month === 6 ? CreateMonthCode(5, true) : CreateMonthCode(month < 6 ? month : month - 1, false);
} else {
return buildMonthCode(month);
return CreateMonthCode(month, false);
}
},
adjustCalendarDate(calendarDate, cache, overflow = 'constrain', fromLegacyDate = false) {
Expand All @@ -1114,8 +1096,9 @@ const helperHebrew = ObjectAssign({}, nonIsoHelperBase, {
// that all fields are present.
this.validateCalendarDate(calendarDate);
if (month === undefined) {
if (ES.Call(StringPrototypeEndsWith, monthCode, ['L'])) {
if (monthCode !== 'M05L') {
const { monthNumber, isLeapMonth } = ParseMonthCode(monthCode);
if (isLeapMonth) {
if (monthNumber !== 5) {
throw new RangeErrorCtor(`Hebrew leap month must have monthCode M05L, not ${monthCode}`);
}
month = 6;
Expand All @@ -1129,7 +1112,7 @@ const helperHebrew = ObjectAssign({}, nonIsoHelperBase, {
}
}
} else {
month = monthCodeNumberPart(monthCode);
month = monthNumber;
// if leap month is before this one, the month index is one more than the month code
if (this.inLeapYear({ year }) && month >= 6) month++;
const largestMonth = this.monthsInYear({ year });
Expand Down Expand Up @@ -1207,8 +1190,7 @@ const helperPersian = ObjectAssign({}, nonIsoHelperBase, {
return month <= 6 ? 31 : 30;
},
maxLengthOfMonthCodeInAnyYear(monthCode) {
const month = +ES.Call(StringPrototypeSlice, monthCode, [1]);
return month <= 6 ? 31 : 30;
return ParseMonthCode(monthCode).monthNumber <= 6 ? 31 : 30;
},
estimateIsoDate(calendarDate) {
const { year } = this.adjustCalendarDate(calendarDate);
Expand Down Expand Up @@ -1237,8 +1219,7 @@ const helperIndian = ObjectAssign({}, nonIsoHelperBase, {
return this.getMonthInfo(calendarDate).length;
},
maxLengthOfMonthCodeInAnyYear(monthCode) {
const month = +ES.Call(StringPrototypeSlice, monthCode, [1]);
let monthInfo = this.months[month];
let monthInfo = this.months[ParseMonthCode(monthCode).monthNumber];
monthInfo = monthInfo.leap ?? monthInfo;
return monthInfo.length;
},
Expand Down Expand Up @@ -1462,7 +1443,7 @@ function makeHelperGregorianFixedEpoch(id) {
return this.minimumMonthLength(calendarDate);
},
maxLengthOfMonthCodeInAnyYear(monthCode) {
const month = +ES.Call(StringPrototypeSlice, monthCode, [1]);
const month = ParseMonthCode(monthCode).monthNumber;
return [undefined, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
},
estimateIsoDate(calendarDate) {
Expand Down Expand Up @@ -1502,7 +1483,7 @@ const makeHelperGregorian = (id, originalEras) => {
return this.minimumMonthLength(calendarDate);
},
maxLengthOfMonthCodeInAnyYear(monthCode) {
const month = +ES.Call(StringPrototypeSlice, monthCode, [1]);
const month = ParseMonthCode(monthCode).monthNumber;
return [undefined, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
},
/** Fill in missing parts of the (year, era, eraYear) tuple */
Expand Down Expand Up @@ -1581,7 +1562,7 @@ const makeHelperGregorian = (id, originalEras) => {
adjustCalendarDate(calendarDate, cache, overflow /*, fromLegacyDate = false */) {
// Because this is not a lunisolar calendar, it's safe to convert monthCode to a number
const { month, monthCode } = calendarDate;
if (month === undefined) calendarDate = { ...calendarDate, month: monthCodeNumberPart(monthCode) };
if (month === undefined) calendarDate = { ...calendarDate, month: ParseMonthCode(monthCode).monthNumber };
this.validateCalendarDate(calendarDate);
calendarDate = this.completeEraYear(calendarDate);
calendarDate = Call(nonIsoHelperBase.adjustCalendarDate, this, [calendarDate, cache, overflow]);
Expand Down Expand Up @@ -1611,7 +1592,7 @@ const makeHelperSameMonthDayAsGregorian = (id, originalEras) => {
// Month and day are same as ISO, so bypass Intl.DateTimeFormat and
// calculate the year, era, and eraYear here.
const { year: isoYear, month, day } = isoDate;
const monthCode = buildMonthCode(month);
const monthCode = CreateMonthCode(month, false);
const year = isoYear - this.anchorEra.isoEpoch.year + 1;
return this.completeEraYear({ year, month, monthCode, day });
}
Expand Down Expand Up @@ -1893,7 +1874,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
// "bis" suffix used only by the Chinese/Dangi calendar to indicate a leap
// month. Below we'll normalize the output.
if (monthExtra && monthExtra !== 'bis') throw new RangeErrorCtor(`Unexpected leap month suffix: ${monthExtra}`);
const monthCode = buildMonthCode(month, monthExtra !== undefined);
const monthCode = CreateMonthCode(month, monthExtra !== undefined);
const monthString = `${month}${monthExtra || ''}`;
const months = this.getMonthList(year, cache);
const monthInfo = months[monthString];
Expand All @@ -1906,23 +1887,17 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
this.validateCalendarDate(calendarDate);
if (month === undefined) {
const months = this.getMonthList(year, cache);
let numberPart = ES.Call(StringPrototypeReplace, monthCode, [/^M|L$/g, (ch) => (ch === 'L' ? 'bis' : '')]);
if (numberPart[0] === '0') numberPart = ES.Call(StringPrototypeSlice, numberPart, [1]);
const { monthNumber, isLeapMonth } = ParseMonthCode(monthCode);
const numberPart = `${monthNumber}${isLeapMonth ? 'bis' : ''}`;
let monthInfo = months[numberPart];
month = monthInfo && monthInfo.monthIndex;
// If this leap month isn't present in this year, constrain to the same
// day of the previous month.
if (
month === undefined &&
ES.Call(StringPrototypeEndsWith, monthCode, ['L']) &&
monthCode != 'M13L' &&
overflow === 'constrain'
) {
const withoutML = ES.Call(StringPrototypeReplace, monthCode, [/^M0?|L$/g, '']);
monthInfo = months[withoutML];
if (month === undefined && isLeapMonth && monthNumber !== 13 && overflow === 'constrain') {
monthInfo = months[monthNumber];
if (monthInfo) {
month = monthInfo.monthIndex;
monthCode = buildMonthCode(withoutML);
monthCode = CreateMonthCode(monthNumber, false);
}
}
if (month === undefined) {
Expand All @@ -1945,15 +1920,15 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, {
if (matchingMonthEntry === undefined) {
throw new RangeErrorCtor(`Invalid month ${month} in Chinese year ${year}`);
}
monthCode = buildMonthCode(
monthCode = CreateMonthCode(
ES.Call(StringPrototypeReplace, matchingMonthEntry[0], ['bis', '']),
ES.Call(StringPrototypeIndexOf, matchingMonthEntry[0], ['bis']) !== -1
);
} else {
// Both month and monthCode are present. Make sure they don't conflict.
const months = this.getMonthList(year, cache);
let numberPart = ES.Call(StringPrototypeReplace, monthCode, [/^M|L$/g, (ch) => (ch === 'L' ? 'bis' : '')]);
if (numberPart[0] === '0') numberPart = ES.Call(StringPrototypeSlice, numberPart, [1]);
const { monthNumber, isLeapMonth } = ParseMonthCode(monthCode);
const numberPart = `${monthNumber}${isLeapMonth ? 'bis' : ''}`;
const monthInfo = months[numberPart];
if (!monthInfo) throw new RangeErrorCtor(`Unmatched monthCode ${monthCode} in Chinese year ${year}`);
if (month !== monthInfo.monthIndex) {
Expand Down
22 changes: 5 additions & 17 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ import {
SetPrototypeHas,
StringFromCharCode,
StringPrototypeCharCodeAt,
StringPrototypeIndexOf,
StringPrototypeMatch,
StringPrototypePadStart,
StringPrototypeReplace,
Expand Down Expand Up @@ -96,6 +95,7 @@ import {
GetUnsignedRoundingMode,
TruncatingDivModByPowerOf10
} from './math.mjs';
import { CreateMonthCode, ParseMonthCode } from './monthcode.mjs';
import { TimeDuration } from './timeduration.mjs';
import {
CreateSlots,
Expand Down Expand Up @@ -235,21 +235,9 @@ export function RequireString(value) {
return value;
}

function ToSyntacticallyValidMonthCode(value) {
value = ToPrimitive(value, StringCtor);
RequireString(value);
if (
value.length < 3 ||
value.length > 4 ||
value[0] !== 'M' ||
Call(StringPrototypeIndexOf, '0123456789', [value[1]]) === -1 ||
Call(StringPrototypeIndexOf, '0123456789', [value[2]]) === -1 ||
(value[1] + value[2] === '00' && value[3] !== 'L') ||
(value[3] !== 'L' && value[3] !== undefined)
) {
throw new RangeError(`bad month code ${value}; must match M01-M99 or M00L-M99L`);
}
return value;
function ToMonthCode(value) {
const { monthNumber, isLeapMonth } = ParseMonthCode(value);
return CreateMonthCode(monthNumber, isLeapMonth);
}

function ToOffsetString(value) {
Expand Down Expand Up @@ -281,7 +269,7 @@ const BUILTIN_CASTS = new MapCtor([
['eraYear', ToIntegerWithTruncation],
['year', ToIntegerWithTruncation],
['month', ToPositiveIntegerWithTruncation],
['monthCode', ToSyntacticallyValidMonthCode],
['monthCode', ToMonthCode],
['day', ToPositiveIntegerWithTruncation],
['hour', ToIntegerWithTruncation],
['minute', ToIntegerWithTruncation],
Expand Down
28 changes: 28 additions & 0 deletions polyfill/lib/monthcode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
String as StringCtor,
RangeError as RangeErrorCtor,
TypeError as TypeErrorCtor,
StringPrototypePadStart,
RegExpPrototypeExec
} from './primordials.mjs';

import Call from 'es-abstract/2024/Call.js';
import ToPrimitive from 'es-abstract/2024/ToPrimitive.js';

import { monthCode as MONTH_CODE_REGEX } from './regex.mjs';

export function ParseMonthCode(argument) {
const value = ToPrimitive(argument, StringCtor);
if (typeof value !== 'string') throw new TypeErrorCtor('month code must be a string');
const match = Call(RegExpPrototypeExec, MONTH_CODE_REGEX, [value]);
if (!match) throw new RangeErrorCtor(`bad month code ${value}; must match M01-M99 or M00L-M99L`);
return {
monthNumber: +(match[1] ?? match[3] ?? match[5]),
isLeapMonth: (match[2] ?? match[4] ?? match[6]) === 'L'
};
}

export function CreateMonthCode(monthNumber, isLeapMonth) {
const numberPart = Call(StringPrototypePadStart, `${monthNumber}`, [2, '0']);
return isLeapMonth ? `M${numberPart}L` : `M${numberPart}`;
}
2 changes: 2 additions & 0 deletions polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ const fraction = /(\d+)(?:[.,](\d{1,9}))?/;
const durationDate = /(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?/;
const durationTime = new RegExpCtor(`(?:${fraction.source}H)?(?:${fraction.source}M)?(?:${fraction.source}S)?`);
export const duration = new RegExpCtor(`^([+-])?P${durationDate.source}(?:T(?!$)${durationTime.source})?$`, 'i');

export const monthCode = /^M(?:(00)(L)|(0[1-9])(L)?|([1-9][0-9])(L)?)$/;
29 changes: 0 additions & 29 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -1774,35 +1774,6 @@ <h1>
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-tomonthcode" type="abstract operation">
<h1>
ToMonthCode (
_argument_: an ECMAScript language value,
): either a normal completion containing a String or a throw completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It converts _argument_ to a String, or throws a *TypeError* if that is not possible.
It also requires that the String is a syntactically valid month code, or throws a *RangeError* if it is not.
The month code is not guaranteed to be correct in the context of any particular calendar; for example, some calendars do not have leap months.
</dd>
</dl>
<emu-alg>
1. Let _monthCode_ be ? ToPrimitive(_argument_, ~string~).
1. If _monthCode_ is not a String, throw a *TypeError* exception.
1. If the length of _monthCode_ is not 3 or 4, throw a *RangeError* exception.
1. If the first code unit of _monthCode_ is not 0x004D (LATIN CAPITAL LETTER M), throw a *RangeError* exception.
1. If the second code unit of _monthCode_ is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), throw a *RangeError* exception.
1. If the third code unit of _monthCode_ is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE), throw a *RangeError* exception.
1. If the length of _monthCode_ is 4 and the fourth code unit of _monthCode_ is not 0x004C (LATIN CAPITAL LETTER L), throw a *RangeError* exception.
1. Let _monthCodeDigits_ be the substring of monthCode from 1 to 3.
1. Let _monthCodeInteger_ be ℝ(StringToNumber(_monthCodeDigits_)).
1. If _monthCodeInteger_ is 0 and the length of _monthCode_ is not 4, throw a *RangeError* exception.
1. Return _monthCode_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-tooffsetstring" type="abstract operation">
<h1>
ToOffsetString (
Expand Down
Loading