Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab17bf5
Polyfill: Update reference code to reflect normative change
ptomato Aug 4, 2025
2dd81bc
Update test262
ptomato Aug 22, 2025
f6f3cbf
Update test262
ptomato Aug 25, 2025
14bda72
Editorial: Revert inadvertent observable change
ptomato Sep 16, 2025
064e2e6
Polyfill: Speed up largestUnit: month date difference for calendars w…
gibson042 Sep 19, 2025
e9194ad
Polyfill: Speed up many-months date addition for calendars without le…
gibson042 Sep 19, 2025
6f8a236
fix for issues #3141, #3148 and #3149
arshaw Aug 27, 2025
d21f9b3
Polyfill: Speed up Chinese calendar getMonthList
gibson042 Sep 22, 2025
087cfc7
Polyfill: Speed up GetUTCEpochMilliseconds
gibson042 Sep 23, 2025
0868902
Polyfill: Make Gregorian calendar arithmetic match ISO8601
ptomato Sep 5, 2025
83660f3
Polyfill: Make more useful out-of-range error messages
ptomato Sep 10, 2025
0b83e10
Polyfill: Correctly handle limits in GetNamedTimeZoneEpochNanoseconds
ptomato Sep 11, 2025
ad75f82
Update test262
ptomato Sep 23, 2025
68c113a
Polyfill: Speed up months arithmetic in coptic, ethioaa, ethiopic, he…
ptomato Sep 26, 2025
7baae7f
Polyfill: Avoid RegExp in ParseMonthCode
ptomato Sep 19, 2025
dcafbed
Polyfill: Factor out code to generate cache keys
ptomato Sep 19, 2025
91dd18c
Polyfill: Only cache for one calendar at a time
ptomato Sep 19, 2025
f229b26
Polyfill: Use int32 cache keys
ptomato Sep 19, 2025
e3f2e1c
Polyfill: Pull eraFromYear() outside of completeEraYear()
ptomato Sep 20, 2025
de8cb7a
Polyfill: Change format of eraInfo table
ptomato Sep 20, 2025
79e3546
Polyfill: Change format of Hebrew months table
ptomato Sep 23, 2025
ceb75c6
Polyfill: Avoid computing Hebrew month code if we already have it
ptomato Sep 23, 2025
7d447cc
Polyfill: Don't swallow non-RangeErrors from formatToParts()
ptomato Sep 23, 2025
e96c3de
Polyfill: Use cached daysInMonth information in Chinese calendar
ptomato Sep 24, 2025
6284427
Polyfill: Cache monthcode-month mapping both ways in Chinese calendar
ptomato Sep 24, 2025
3516db5
Polyfill: Add assertion to catch error in Chinese calendar month cache
ptomato Sep 25, 2025
d4cd661
Make "datemath" test more exhaustive and compare with snapshots
ptomato Sep 5, 2025
aa55006
Polyfill: Fix off-by-1-or-2s error due to FP precision loss
ptomato Oct 31, 2025
66a5706
Update test262
ptomato Nov 3, 2025
c6a673f
Update test262
ptomato Nov 6, 2025
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
574 changes: 327 additions & 247 deletions lib/calendar.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export class Duration implements Temporal.Duration {

let largestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'largestUnit');
let { plainRelativeTo, zonedRelativeTo } = ES.GetTemporalRelativeToOption(roundTo);
const roundingIncrement = ES.GetTemporalRoundingIncrementOption(roundTo);
const roundingIncrement = ES.GetRoundingIncrementOption(roundTo);
const roundingMode = ES.GetRoundingModeOption(roundTo, 'halfExpand');
let smallestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'smallestUnit');
ES.ValidateTemporalUnitValue(smallestUnit, 'datetime');
Expand Down
181 changes: 118 additions & 63 deletions lib/ecmascript.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/instant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class Instant implements Temporal.Instant {
typeof roundToParam === 'string'
? (ES.CreateOnePropObject('smallestUnit', roundToParam) as Exclude<typeof roundToParam, string>)
: ES.GetOptionsObject(roundToParam);
const roundingIncrement = ES.GetTemporalRoundingIncrementOption(roundTo);
const roundingIncrement = ES.GetRoundingIncrementOption(roundTo);
const roundingMode = ES.GetRoundingModeOption(roundTo, 'halfExpand');
const smallestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'smallestUnit', ES.REQUIRED);
ES.ValidateTemporalUnitValue(smallestUnit, 'time');
Expand Down Expand Up @@ -82,9 +82,9 @@ export class Instant implements Temporal.Instant {
const digits = ES.GetTemporalFractionalSecondDigitsOption(resolvedOptions);
const roundingMode = ES.GetRoundingModeOption(resolvedOptions, 'trunc');
const smallestUnit = ES.GetTemporalUnitValuedOption(resolvedOptions, 'smallestUnit');
let timeZone = resolvedOptions.timeZone;
ES.ValidateTemporalUnitValue(smallestUnit, 'time');
if (smallestUnit === 'hour') throw new RangeError('smallestUnit must be a time unit other than "hour"');
let timeZone = resolvedOptions.timeZone;
if (timeZone !== undefined) timeZone = ES.ToTemporalTimeZoneIdentifier(timeZone);
const { precision, unit, increment } = ES.ToSecondsStringPrecisionRecord(smallestUnit, digits);
const ns = GetSlot(this, EPOCHNANOSECONDS);
Expand Down
21 changes: 14 additions & 7 deletions lib/monthcode.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { ToPrimitiveRequireString } from './primitive';
import { monthCode as MONTH_CODE_REGEX } from './regex';

const digitsForMonthNumber = Array.from({ length: 100 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));

export function ParseMonthCode(argument: unknown) {
const value = ToPrimitiveRequireString(argument);
const match = MONTH_CODE_REGEX.exec(value);
if (!match) throw new RangeError(`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'
};
const digits = value.slice(1, 3);
const monthNumber = digits.length === 2 ? +digits : -1; // -1 ensures failure
const isLeapMonth = value.length === 4;
if (
!(monthNumber >= 0) ||
digits !== digitsForMonthNumber[monthNumber] ||
value[0] !== 'M' ||
(isLeapMonth ? value[3] !== 'L' : value.length !== 3 || monthNumber === 0)
) {
throw new RangeError(`bad month code ${value}; must match M01-M99 or M00L-M99L`);
}
return { monthNumber, isLeapMonth };
}

export function CreateMonthCode(monthNumber: number, isLeapMonth: boolean) {
Expand Down
5 changes: 4 additions & 1 deletion lib/plaindate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ export class PlainDate implements Temporal.PlainDate {
withCalendar(calendarParam: Params['withCalendar'][0]): Return['withCalendar'] {
ES.CheckReceiver(this, ES.IsTemporalDate);
const calendar = ES.ToTemporalCalendarIdentifier(calendarParam);
return ES.CreateTemporalDate(GetSlot(this, ISO_DATE), calendar);
// Don't reuse the same ISODate object, as it should start with a fresh
// calendar cache
const { year, month, day } = GetSlot(this, ISO_DATE);
return ES.CreateTemporalDate({ year, month, day }, calendar);
}
add(temporalDurationLike: Params['add'][0], options: Params['add'][1] = undefined): Return['add'] {
ES.CheckReceiver(this, ES.IsTemporalDate);
Expand Down
10 changes: 8 additions & 2 deletions lib/plaindatetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ export class PlainDateTime implements Temporal.PlainDateTime {
withCalendar(calendarParam: Params['withCalendar'][0]): Return['withCalendar'] {
ES.CheckReceiver(this, ES.IsTemporalDateTime);
const calendar = ES.ToTemporalCalendarIdentifier(calendarParam);
return ES.CreateTemporalDateTime(GetSlot(this, ISO_DATE_TIME), calendar);
// Don't reuse the same ISODate object, as it should start with a fresh
// calendar cache
const {
isoDate: { year, month, day },
time
} = GetSlot(this, ISO_DATE_TIME);
return ES.CreateTemporalDateTime(ES.CombineISODateAndTimeRecord({ year, month, day }, time), calendar);
}
add(temporalDurationLike: Params['add'][0], options: Params['add'][1] = undefined): Return['add'] {
ES.CheckReceiver(this, ES.IsTemporalDateTime);
Expand Down Expand Up @@ -168,7 +174,7 @@ export class PlainDateTime implements Temporal.PlainDateTime {
typeof roundToParam === 'string'
? (ES.CreateOnePropObject('smallestUnit', roundToParam) as Exclude<typeof roundToParam, string>)
: ES.GetOptionsObject(roundToParam);
const roundingIncrement = ES.GetTemporalRoundingIncrementOption(roundTo);
const roundingIncrement = ES.GetRoundingIncrementOption(roundTo);
const roundingMode = ES.GetRoundingModeOption(roundTo, 'halfExpand');
const smallestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'smallestUnit', ES.REQUIRED);
ES.ValidateTemporalUnitValue(smallestUnit, 'time', ['day']);
Expand Down
2 changes: 1 addition & 1 deletion lib/plaintime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class PlainTime implements Temporal.PlainTime {
typeof roundToParam === 'string'
? (ES.CreateOnePropObject('smallestUnit', roundToParam) as Exclude<typeof roundToParam, string>)
: ES.GetOptionsObject(roundToParam);
const roundingIncrement = ES.GetTemporalRoundingIncrementOption(roundTo);
const roundingIncrement = ES.GetRoundingIncrementOption(roundTo);
const roundingMode = ES.GetRoundingModeOption(roundTo, 'halfExpand');
const smallestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'smallestUnit', ES.REQUIRED);
ES.ValidateTemporalUnitValue(smallestUnit, 'time');
Expand Down
2 changes: 0 additions & 2 deletions lib/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,3 @@ const fraction = /(\d+)(?:[.,](\d{1,9}))?/;
const durationDate = /(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?/;
const durationTime = new RegExp(`(?:${fraction.source}H)?(?:${fraction.source}M)?(?:${fraction.source}S)?`);
export const duration = new RegExp(`^([+-])?P${durationDate.source}(?:T(?!$)${durationTime.source})?$`, 'i');

export const monthCode = /^M(?:(00)(L)|(0[1-9])(L)?|([1-9][0-9])(L)?)$/;
4 changes: 2 additions & 2 deletions lib/zoneddatetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
typeof roundToParam === 'string'
? (ES.CreateOnePropObject('smallestUnit', roundToParam) as Exclude<typeof roundToParam, string>)
: ES.GetOptionsObject(roundToParam);
const roundingIncrement = ES.GetTemporalRoundingIncrementOption(roundTo);
const roundingIncrement = ES.GetRoundingIncrementOption(roundTo);
const roundingMode = ES.GetRoundingModeOption(roundTo, 'halfExpand');
const smallestUnit = ES.GetTemporalUnitValuedOption(roundTo, 'smallestUnit', ES.REQUIRED);
ES.ValidateTemporalUnitValue(smallestUnit, 'time', ['day']);
Expand Down Expand Up @@ -331,9 +331,9 @@ export class ZonedDateTime implements Temporal.ZonedDateTime {
const showOffset = ES.GetTemporalShowOffsetOption(resolvedOptions);
const roundingMode = ES.GetRoundingModeOption(resolvedOptions, 'trunc');
const smallestUnit = ES.GetTemporalUnitValuedOption(resolvedOptions, 'smallestUnit');
const showTimeZone = ES.GetTemporalShowTimeZoneNameOption(resolvedOptions);
ES.ValidateTemporalUnitValue(smallestUnit, 'time');
if (smallestUnit === 'hour') throw new RangeError('smallestUnit must be a time unit other than "hour"');
const showTimeZone = ES.GetTemporalShowTimeZoneNameOption(resolvedOptions);
const { precision, unit, increment } = ES.ToSecondsStringPrecisionRecord(smallestUnit, digits);
return ES.TemporalZonedDateTimeToString(this, precision, showCalendar, showTimeZone, showOffset, {
unit,
Expand Down
97 changes: 0 additions & 97 deletions test/datemath.mjs

This file was deleted.

138 changes: 138 additions & 0 deletions test/monthcode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, it } from '@jest/globals';
import { CreateMonthCode, ParseMonthCode } from '../lib/monthcode';

// Avoid these wrappers in new tests:
function deepEqual(a, b) {
expect(a).toEqual(b);
}
function equal(a, b) {
expect(a).toBe(b);
}
function throws(f, cons) {
expect(f).toThrow(cons);
}

function badMonthCode(code) {
throws(() => ParseMonthCode(code), RangeError, code);
}

describe('ParseMonthCode', () => {
it('all Gregorian month codes', () => {
['M01', 'M02', 'M03', 'M04', 'M05', 'M06', 'M07', 'M08', 'M09', 'M10', 'M11', 'M12'].forEach((code, ix) => {
deepEqual(ParseMonthCode(code), { monthNumber: ix + 1, isLeapMonth: false });
});
});
it('Intercalary month 13', () => {
deepEqual(ParseMonthCode('M13'), { monthNumber: 13, isLeapMonth: false });
});
it('all Chinese leap month codes', () => {
['M01L', 'M02L', 'M03L', 'M04L', 'M05L', 'M06L', 'M07L', 'M08L', 'M09L', 'M10L', 'M11L', 'M12L'].forEach(
(code, ix) => {
deepEqual(ParseMonthCode(code), { monthNumber: ix + 1, isLeapMonth: true });
}
);
});
it('M00L (valid but does not occur in currently supported calendars)', () => {
deepEqual(ParseMonthCode('M00L'), { monthNumber: 0, isLeapMonth: true });
});
it('various other month codes that do not occur in currently supported calendars', () => {
const tests = [
['M14', 14, false],
['M13L', 13, true],
['M99', 99, false],
['M99L', 99, true],
['M42', 42, false],
['M57L', 57, true]
];
for (const [code, monthNumber, isLeapMonth] of tests) {
deepEqual(ParseMonthCode(code), { monthNumber, isLeapMonth });
}
});
it('goes through ToPrimitive', () => {
['toString', Symbol.toPrimitive].forEach((prop) => {
const convertibleObject = {
[prop]() {
return 'M01';
}
};
deepEqual(ParseMonthCode(convertibleObject), { monthNumber: 1, isLeapMonth: false }, prop);
});
});
it('no M00', () => badMonthCode('M00'));
it('missing leading zero', () => {
badMonthCode('M1');
badMonthCode('M5L');
});
it('number too big', () => {
badMonthCode('M100');
badMonthCode('M999L');
});
it('negative number', () => {
badMonthCode('M-3');
badMonthCode('M-7L');
});
it('decimal point', () => {
badMonthCode('M2.');
badMonthCode('M.9L');
badMonthCode('M0.L');
});
it('no leading space', () => {
badMonthCode('M 5');
badMonthCode('M 9L');
});
it('not a number', () => {
badMonthCode('M__');
badMonthCode('MffL');
});
it('wrong leading character', () => {
badMonthCode('m11');
badMonthCode('N11L');
});
it('missing leading character', () => {
badMonthCode('12');
badMonthCode('03L');
});
it('wrong leap signifier', () => {
badMonthCode('M06l');
badMonthCode('M06T');
});
it('junk at end of string', () => badMonthCode('M04L+'));
it('wrong primitive type', () => {
[true, 3, Symbol('M01'), 7n].forEach((wrongType) => {
throws(() => ParseMonthCode(wrongType), TypeError, typeof wrongType);
});
});
it('wrong toString', () => {
throws(() => ParseMonthCode({}), RangeError);
});
});

describe('CreateMonthCode', () => {
it('all Gregorian month codes', () => {
['M01', 'M02', 'M03', 'M04', 'M05', 'M06', 'M07', 'M08', 'M09', 'M10', 'M11', 'M12'].forEach((code, ix) => {
equal(CreateMonthCode(ix + 1, false), code);
});
});
it('Intercalary month 13', () => equal(CreateMonthCode(13, false), 'M13'));
it('all Chinese leap month codes', () => {
['M01L', 'M02L', 'M03L', 'M04L', 'M05L', 'M06L', 'M07L', 'M08L', 'M09L', 'M10L', 'M11L', 'M12L'].forEach(
(code, ix) => {
equal(CreateMonthCode(ix + 1, true), code);
}
);
});
it('M00L (valid but does not occur in currently supported calendars)', () => equal(CreateMonthCode(0, true), 'M00L'));
it('various other month codes that do not occur in currently supported calendars', () => {
const tests = [
[14, false, 'M14'],
[13, true, 'M13L'],
[99, false, 'M99'],
[99, true, 'M99L'],
[42, false, 'M42'],
[57, true, 'M57L']
];
for (const [monthNumber, isLeapMonth, code] of tests) {
equal(CreateMonthCode(monthNumber, isLeapMonth), code);
}
});
});
2 changes: 1 addition & 1 deletion test262
Submodule test262 updated 1842 files
Loading