Repository navigation
feat(core): read typed dates in every calendar - #9792
nynexman4464 wants to merge 7 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
Approval is on hold for a spec decision, and five fixes are needed either way.
This parser implements draft spec:AST-111 FR9–FR13 and DEC-4. On main, architecture:internationalization INV10 still keeps date parsing Gregorian, and INV8's Owning code does not list the new module. So an engineering owner needs to approve this addition ahead of those records, or identify the current record that governs it. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
Required fixes:
- The parser reads its own text back as a different date: several European locales write the Chinese and Korean hint with the 60-year cycle number (
2/12/41for 1 Jan 2025) or with no year, and Hungarian and Serbian Islamic month names contain a digit (Réb. 1) (calendarDateParser.ts:1284–1288,:427–437). - Committed text that cannot be read back: Spanish, French, Italian and Romanian
date_weekdaytext fails on Tuesdays becausemaris also a month name, Vietnamese weekday digits (Thứ 4) count as date numbers, and the parser never learns some era spellings in hints, such as FrenchEBandH. - The new RuleTester cases are spliced into two existing objects without closing them, so two
calendarSystem.tsboundary cases are overwritten (no-raw-intl-locale.test.mjs:155–158,:310–316). normalizeDateTextcopies the digit folding and invisible-character stripping thatutils/unicodeDigits.tsalready givesnumberParser.tsandtimeFormat.ts; importtoAsciiDigitsandINVISIBLE_CHARSinstead.- The changeset says the parser reads a date "the way a locale writes it", but the round-trip matrix covers only native locales,
enandsv-SE; extend the matrix to the shipped locales or narrow the wording.
[Automated review]
| if (!parts.some(p => partType(p) === 'relatedYear' || p.type === 'year')) { | ||
| // Some locales write only the cycle year name; the long form also has | ||
| // the Gregorian year. | ||
| resolved = options.weekday ? LONG_WITH_WEEKDAY : LONG; | ||
| parts = partsOf(locale, calendar, resolved, mid); |
There was a problem hiding this comment.
In ca-ES, cs-CZ, de-DE, fi-FI, fr-FR, it-IT, nl-NL, pl-PL, pt-PT and ru-RU, the numeric year part here is the 60-year cycle number. The hint is 2/12/41 for 1 Jan 2025, and the parser reads it back as 24 Dec 2041. In de-DE, pt-BR and pt-PT the long form also has only yearName (2. M12 jia-chen), so the text reads back in the current year. Always write the related Gregorian year.
| if (/\d/.test(name)) { | ||
| // A month number with affixes: "11月", "9:e mån", "윤6", "6bis". | ||
| const match = /^(\D*)(\d+)(\D*)$/.exec(name); | ||
| if (match != null && (match[1] || match[3])) { | ||
| templates.set(`${match[1]}#${match[3]}`, { | ||
| pre: match[1], | ||
| post: match[3], | ||
| isLeap: code.endsWith('L'), | ||
| }); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Hungarian and Serbian Islamic month abbreviations contain a digit (Réb. 1, Реб. 1 = Rabīʿ I). They become a numeric template here, so MF 1447. Réb. 1 2. (26 Aug 2025) reads as month 1 (28 Jun 2025). Treat the name as a month name unless the digit is that month's own number.
| for (const word of [ | ||
| ...months.keys(), | ||
| ...eras.keys(), | ||
| ...yearAliases.keys(), | ||
| ]) { | ||
| skip.delete(word); | ||
| } |
There was a problem hiding this comment.
Month names win over weekday words, and every lexicon includes English (line 403). So mar (Tuesday in es, fr, it, ro) becomes a second month, and every Tuesday's date_weekday text is unreadable; no-NO tir. and pt-BR ter. collide the same way. Read a weekday word as a weekday when a month is already present, or when it sits at the pattern's weekday position.
| .replace(BIDI_MARKS, '') | ||
| .replace(/\p{Nd}/gu, asciiDigit) |
There was a problem hiding this comment.
utils/unicodeDigits.ts already owns this for numberParser.ts and timeFormat.ts (INVISIBLE_CHARS, toAsciiDigits). BIDI_MARKS and asciiDigit have already drifted from it. Import the shared helpers, add U+202A–U+202E to INVISIBLE_CHARS if needed, and delete the copies.
| { | ||
| code: `new Intl.NumberFormat(locale, {useGrouping: false}).format(day);`, | ||
| filename: CALENDAR_SYSTEM_INFRA_FILE, | ||
| code: `new Intl.DateTimeFormat(locale, {calendar, timeZone: 'UTC'}).formatToParts(ms);`, |
There was a problem hiding this comment.
The object above is still open, so this code and filename overwrite the existing calendarSystem.ts NumberFormat case. Close it with }, and open a new { first. The same applies at line 314. The suite runs 111 cases, not 113.
| code: `const {NumberFormat} = Intl;`, | ||
| filename: CALENDAR_SYSTEM_INFRA_FILE, | ||
| errors: [rawIntlReference], | ||
| code: `new Intl.DateTimeFormat(undefined, {calendar: 'persian'});`, |
There was a problem hiding this comment.
Same splice: this overwrites the invalid const {NumberFormat} = Intl; case for calendarSystem.ts. Close the object with }, and open a new { before this line.
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [feat] Core gains an internal parser for dates typed in a calendar system. It reads a date the way a locale writes it in any `CalendarSystem`, in any decimal digits, with month and era names, leap months such as Adar I, and Japanese eras such as `令和元年`, and returns a Gregorian date. A date the calendar does not have is rejected and never adjusted. It also writes the committed text and the format hint. No component uses it yet, and no public API changes. |
There was a problem hiding this comment.
"reads a date the way a locale writes it in any CalendarSystem" is broader than the evidence. The round trip covers each calendar's native locale, en and sv-SE, and the parser cannot read its own text in shipped locales such as es-ES, fr-FR, it-IT, ro-RO and vi-VN. Fix the parser and widen the matrix, or narrow the wording.
There was a problem hiding this comment.
Approval is on hold for a spec decision, and six fixes are needed either way.
This parser implements draft spec:AST-111 FR9–FR13 and DEC-4, and nearestAcceptedDate implements its draft AR2. On main, architecture:internationalization INV10 still keeps date parsing Gregorian, and INV8's Owning code does not list the new module. An engineering owner needs to approve these additions ahead of those records, or identify the current record that governs them; they record that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
Required fixes:
- The parser reads its own text back as a different date, because several European locales write the Chinese and Korean hint with the 60-year cycle number (
2/12/41for 1 Jan 2025) or with no year, and Hungarian and Serbian Islamic month names contain a digit (Réb. 1) (calendarDateParser.ts:1283–1288,:427–437). - Nine Unicode property escapes are written as regex literals (
calendarDateParser.ts:129and eight more), sounicodePropertyEscapes.source-build.test.mjsfails and a Next.js source build without a browserslist stops withUnknown property: Nd; build them withnew RegExp(...)asutils/unicodeDigits.tsdoes. - Committed text cannot be read back in several shipped locales: Spanish, French, Italian and Romanian
date_weekdaytext fails on Tuesdays becausemaris also a month name, Vietnamese weekday digits (Thứ 4) count as date numbers, and hints such as FrenchEBandHuse era spellings the parser never learns. - The new RuleTester cases are spliced into two existing objects without closing them, so two
calendarSystem.tsboundary cases are overwritten (no-raw-intl-locale.test.mjs:154–159,:310–317). normalizeDateTextcopies the digit folding and invisible-character stripping thatutils/unicodeDigits.tsalready givesnumberParser.tsandtimeFormat.ts, so importtoAsciiDigitsandINVISIBLE_CHARSinstead.- The changeset says the parser reads a date "the way a locale writes it", but the round-trip matrix covers only native locales,
enandsv-SE, so extend the matrix to the shipped locales or narrow the wording.
[Automated review]
| weekday: 'short', | ||
| }; | ||
| const BIDI_MARKS = /[\u061c\u200b-\u200f\u202a-\u202e\u2066-\u2069]/g; | ||
| const DECIMAL_DIGIT = /\p{Nd}/u; |
There was a problem hiding this comment.
This and eight more \p{…} regex literals (lines 130, 131, 135, 287, 291, 298, 415, 509) fail unicodePropertyEscapes.source-build.test.mjs, and Next.js's bundled Babel stops on the file with Unknown property: Nd when an app sets no browserslist. Build each one once with new RegExp('\\p{Nd}', 'u'), as utils/unicodeDigits.ts does.
53da574 to
fe3c951
Compare
3bfcb3f to
96920a3
Compare
There was a problem hiding this comment.
Approval is on hold for a spec decision, and one fix is needed either way.
The parser and nearestAcceptedDate implement draft spec:AST-111 FR9–FR13, AR2 and DEC-4, while architecture:internationalization INV10 on main still keeps date parsing Gregorian and INV8's Owning code does not list the parser; the widened invisible-character set also lets NumberInput and DateTimeInput's time field accept text that no current clause covers. An engineering owner needs to approve these additions ahead of those records, or identify the current record that governs them, by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.
Required fix:
calendarDateParser.ts:193–227re-implements the UTC date-only formatting path thatplainDateFormatToPartsandplainDateFormatalready provide (andcalendarSystem.tsalready calls), so routepartsOfandformatInCalendarthrough them and deleteepochMs.
The read-back, source-build, lint-case, digit-folding and changeset fixes from the last round all hold at this head.
[Automated review]
| function epochMs(pd: PlainDate): number { | ||
| const date = new Date(0); | ||
| date.setUTCFullYear(pd.year, pd.month - 1, pd.day); | ||
| return date.getTime(); | ||
| } | ||
|
|
||
| function isoToPlainDate(iso: string): PlainDate { | ||
| const match = /^(-?\d+)-(\d+)-(\d+)$/.exec(iso); | ||
| if (match == null) { | ||
| throw new RangeError(`Bad sample date ${iso}`); | ||
| } | ||
| return {year: +match[1], month: +match[2], day: +match[3]}; | ||
| } | ||
|
|
||
| function partsOf( | ||
| locale: Locale, | ||
| calendar: CalendarSystem, | ||
| options: Intl.DateTimeFormatOptions, | ||
| pd: PlainDate, | ||
| ): Intl.DateTimeFormatPart[] { | ||
| return formatter(locale, calendar, options).formatToParts(epochMs(pd)); | ||
| } |
There was a problem hiding this comment.
epochMs and partsOf repeat the date-fields branch of plainDateFormatInput (plainDate.ts:494–500): a cached formatter with timeZone: 'UTC' and UTC midnight. plainDateFormatToParts(pd, {...options, calendar}, locale) already does this, and calendarSystem.ts:300–307 calls it for the same job. Call it here and plainDateFormat in formatInCalendar, then delete epochMs, so the parser always learns from the text the fields show.
| } | ||
| const typedEra = eraCodes.size === 1 ? [...eraCodes][0] : undefined; | ||
|
|
||
| // Assign the numbers to fields. |
There was a problem hiding this comment.
Non-blocking (INV29): this comment and // Resolve the year. at line 969 narrate the code. Delete them, or extract assignFields() and resolveYear().
fe3c951 to
0041c70
Compare
96920a3 to
f81d686
Compare
There was a problem hiding this comment.
Approval is on hold for a spec decision, and one fix is needed either way.
The parser and nearestAcceptedDate implement draft spec:AST-111 FR9–FR13, AR2 and DEC-4, while architecture:internationalization INV10 on main still keeps date parsing Gregorian and INV8's Owning code does not list the parser; the wider invisible-character set also changes what NumberInput and DateTimeInput's time field accept, which no current clause covers. An engineering owner needs to approve these additions ahead of those records, or identify the current record that governs them, by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.
Required fix:
- Miss:
normalizeDateText(calendarDateParser.ts:232) does not mapʻ(U+02BB), the letterIntlwrites in the English namesDhuʻl-QiʻdahandDhuʻl-Hijjah, soDhu'l-Hijjah 10, 1447 AHtyped with a straight or curly apostrophe is unreadable in every Islamic calendar; add U+02BB and U+02BF to that apostrophe class with a test.
The earlier formatting-path finding is fixed.
[Automated review]
| .normalize('NFKC') | ||
| .replace(INVISIBLE_CHARS, '') | ||
| // People type an ASCII apostrophe or quote for Hebrew geresh and gershayim. | ||
| .replace(/[\u05f3\u2018\u2019\u02bc`]/g, "'") |
There was a problem hiding this comment.
Miss: ʻ (U+02BB) is missing from this class. Intl writes it in the English Islamic month names Dhuʻl-Qiʻdah and Dhuʻl-Hijjah (and Dhuʻl-Q., Dhuʻl-H.), so Dhu'l-Hijjah 10, 1447 AH and Dhu’l-Hijjah 10, 1447 AH are unreadable in every Islamic calendar. Adding U+02BB and U+02BF here makes both read, and the parser's unit and round-trip suites still pass.
0041c70 to
de34259
Compare
f81d686 to
18150b8
Compare
There was a problem hiding this comment.
Approval is on hold for a spec decision, and one fix is needed either way.
The parser and nearestAcceptedDate implement draft spec:AST-111 FR9–FR13, AR2 and DEC-4, while architecture:internationalization INV10 on main still keeps date parsing Gregorian and INV8's Owning code does not list the parser; the wider invisible-character set also changes what NumberInput and DateTimeInput's time field accept, which no current clause covers. An engineering owner needs to approve these additions ahead of those records, or identify the current records that govern them, by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.
Required fix:
- Miss:
normalizeDateText(calendarDateParser.ts:232) does not foldʻ(U+02BB), the letterIntlwrites in the English namesDhuʻl-QiʻdahandDhuʻl-Hijjah, soDhu'l-Hijjah 10, 1447 AHtyped with a straight or curly apostrophe is unreadable in every Islamic calendar; add U+02BB and U+02BF to that class with a test.
[Automated review]
| .normalize('NFKC') | ||
| .replace(INVISIBLE_CHARS, '') | ||
| // People type an ASCII apostrophe or quote for Hebrew geresh and gershayim. | ||
| .replace(/[\u05f3\u2018\u2019\u02bc`]/g, "'") |
There was a problem hiding this comment.
Miss: ʻ (U+02BB) is missing from this class. Intl writes it in the English Islamic month names Dhuʻl-Qiʻdah and Dhuʻl-Hijjah (and Dhuʻl-Q., Dhuʻl-H.), so Dhu'l-Hijjah 10, 1447 AH and Dhu’l-Hijjah 10, 1447 AH are unreadable in every Islamic calendar. Adding U+02BB and U+02BF here makes both spellings read and keeps the parser's tests passing:
.replace(/[\u05f3\u2018\u2019\u02bb\u02bc\u02bf`]/g, "'")Please add a test that Dhu'l-Hijjah 10, 1447 AH reads in en with islamic-umalqura.
| } | ||
|
|
||
| function windowedYear(twoDigits: number, current: number): number { | ||
| // Eighty years back and nineteen ahead of the calendar's current year. |
There was a problem hiding this comment.
Non-blocking (INV29): this comment restates the two comparisons below. Delete it, or move the window into a JSDoc on windowedYear: "Resolves a two-digit year to the century that puts it at most 80 years before and 19 years after the current year."
de34259 to
babf230
Compare
18150b8 to
f02913f
Compare
|
/approve-spec f02913f |
There was a problem hiding this comment.
Changes requested. An engineering owner approved the calendar parser's direction at this head, and two fixes remain.
- Miss:
formatLunisolarinpackages/core/src/utils/calendarDateParser.ts(lines 1241–1280) copies the lunisolar branch offormatCalendarDateinutils/calendarSystem.tsinstead of extending it, so Calendar's formatter and the field already write different Chinese and Dangi text for the same day (24. M09 bing-wuagainst24. M09 2026inde-DE); giveformatCalendarDatea related-year option, call it fromformatInCalendar, and delete the copy. - The description still says that the shared
INVISIBLE_CHARSset drops U+202A–U+202E and that NumberInput and the time fields now ignore those characters, butunicodeDigits.tsis unchanged at this head and only the calendar parser strips them, so please remove those two statements.
The inline comments add two non-blocking points: the ASCII fast path in normalizeDateText skips the backtick fold, and two comments only label the next block.
[Automated review]
| * names. Intl and a polyfilled Temporal can start a month one day apart, so the | ||
| * month's name comes from its middle day and the day number from Temporal. | ||
| */ | ||
| function formatLunisolar( |
There was a problem hiding this comment.
Miss: this copies the lunisolar branch of formatCalendarDate (calendarSystem.ts:330–377). Both take the middle-of-month Intl parts, write Temporal's day through plainDateFormatDayNumber with the resolved numbering system, and take the weekday from the date. Only the related-year swap (lines 1258–1260 and 1273–1276) is new. With the same options, Calendar's formatter writes 24. M09 bing-wu and this function writes 24. M09 2026 for 2 Nov 2026 in de-DE. Give formatCalendarDate an option that writes the related Gregorian year, call it from formatInCalendar for every non-Gregorian calendar, and delete this function.
| /** Text in one comparable form: ASCII digits, lower case, no Latin accents. */ | ||
| export function normalizeDateText(text: string, locale: Locale): string { | ||
| // Most typed text is printable ASCII; skip the Unicode passes it does not need. | ||
| if (/^[\u0020-\u007e]*$/.test(text)) { |
There was a problem hiding this comment.
Non-blocking: this returns before the apostrophe fold at line 235, so the backtick there is folded only when the text also has a non-ASCII character. Dhu`l-Hijjah 10, 1447 AH is unreadable in en with islamic-umalqura, but the same text with a trailing U+200F reads as 27 May 2026. Fold the backtick here too, or drop it from the class.
| } | ||
| } | ||
|
|
||
| // Literals, weekday names and year names: words to read past. |
There was a problem hiding this comment.
Non-blocking (INV29): this comment and the one at line 494 only label the next block. Extract collectSkipWords() and collectEras(), or delete the comments.
babf230 to
5ec2dc4
Compare
f02913f to
e38dac1
Compare
There was a problem hiding this comment.
Approval is on hold for a spec decision at this head, and one fix is needed either way.
The calendar parser, its raw-Intl lint admission and nearestAcceptedDate implement spec:AST-111 FR9–FR13, AR2 and DEC-4, which is still draft on main, and architecture:internationalization INV8's Owning code does not list the parser; the earlier /approve-spec names the previous head, and an approval counts only at its exact head. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
- Miss:
formatLunisolarinpackages/core/src/utils/calendarDateParser.ts(lines 1241–1280) copies the lunisolar branch offormatCalendarDateinutils/calendarSystem.tsinstead of extending it, so the Calendar label and the field already write different Chinese and Dangi text for the same day (24. M09 bing-wuagainst24. M09 2026inde-DE); giveformatCalendarDatea related-year option, call it fromformatInCalendar, and delete the copy.
Two earlier non-blocking points still apply: the ASCII fast path in normalizeDateText skips the backtick fold, and the comments at lines 428 and 494 only label the next block.
[Automated review]
Add calendarDateParser: per-locale lexicons, a typed-date parser with distinct rejection kinds, committed text, the format hint, and calendar display names. Unit and round-trip tests cover every calendar. The raw-Intl lint rule approves the parser at its own path.
…cales Write the related Gregorian year for Chinese and Korean dates, read month names that hold a digit, read weekday words that are also month or era names, learn the era spellings of full dates, and keep a one-digit day open until it exceeds 31. Reuse unicodeDigits for digit folding and invisible characters, and build Unicode property escapes at runtime.
…eParser Build formatters through dateTimeFormatFor instead of a second cache, and fold the new parser cases into the existing tests.
…matToParts partsOf and formatInCalendar call plainDateFormatToParts and plainDateFormat, which already format a date-only PlainDate at UTC midnight in UTC, so epochMs and the local formatter go away. Lunisolar day and year digits use plainDateFormatDayNumber with the formatter's numbering system.
… in typed month names The shared invisible-character set stays as on main, so NumberInput and time text read as before. The calendar parser strips U+202A-U+202E itself, and reads a straight or curly apostrophe for the ʻ that Intl writes in Islamic month names such as Dhuʻl-Hijjah.
e38dac1 to
0d0e06d
Compare
This implements the parser behind
spec:AST-111(approved in #9605) FR9–FR13: reading dates written in a calendar, years of the calendar, rejecting dates the calendar does not have, completeness for FR12, and committed text that reads back as the same date. It also adds the helper for AR2's example date. No component uses it yet; #9643 wires it into the fields.User need
People who use a calendar other than the Gregorian calendar need to type dates the way their locale writes them in that calendar:
۱۴۰۵/۸/۱۱,1 Adar 5784,令和元年5月1日. The existingparseDateInputreads English month names and ASCII digits in Gregorian only. One parser that works for every calendar keeps the fields' behavior the same across calendars.Problem and solution fit
utils/calendarDateParser.ts, built fromIntloutput (through Core'splainDateFormatandplainDateFormatToParts) and validated byTemporal:2026is Buddhist year 2026. The committed text and the format hint add the short era where the locale's format omits it and the era keeps the field order, in every calendar with eras other than the Chinese and Dangi calendars. fix(utils): read back calendar text in Latvian, Amharic and Hindi #9840 adds the narrow-era and earlier-era cases that FR10 needs forjapaneseandroc.calendarDateFromFieldsrejects 30 Esfand in a common Persian year.nearestAcceptedDateinutils/plainDate.ts: today when a field accepts it, else the nearest date withinmin,maxand the disabled days. feat(DateInput, DateTimeInput): type dates in the active calendar #9643's format hint uses it. It is not exported from@astryxdesign/core/utils.parseDateInputis unchanged.INVISIBLE_CHARSand, for pasted text, the bidi embedding and override characters (U+202A–U+202E). The shared set inutils/unicodeDigits.tsis unchanged, so NumberInput's number parser and the time parser inutils/timeFormat.tsread text as before.ʻ(U+02BB) thatIntlwrites in the English Islamic month names, such asDhuʻl-Hijjah; U+02BF folds the same way.Intlrule approves the parser at its own path, with valid and invalid cases.Current specification
spec:AST-111FR9–FR13, AR2 and DEC-4 (approved in spec(AST-111): date components use the calendar the application chooses #9605). The component records take these requirements in feat(DateInput, DateTimeInput): type dates in the active calendar #9643, where the fields use the parser.Public delta and caller burden
nearestAcceptedDateare internal and not exported. No rendered output changes, and NumberInput and time text read as before.Design and alternatives
spec:AST-111DEC-4). DEC-4 rejects segmented fields.Intlfor the locale and calendar, so the parser reads what the same runtime writes.Temporal's day plusIntlnames, so display and parsing agree in one runtime.Evidence
enandsv-SE, with zero failures.calendarDateParser.roundtrip.test.tssamples this matrix in CI, plus locales with weekday words that name months or eras, month names with digits, and cycle years. Dates before 1900 are best effort.Dhu'l-Hijjah 10, 1447 AHtyped with a straight, curly orʻapostrophe inislamic-umalqura.Scope
Testing
pnpm exec vitest run --maxWorkers=2over the test files this pull request changes (the raw-Intlrule, the parser, its round trip andplainDate) and the other tests beside its changed sources inutils, plusdocPropLiterals.test.ts: 56 files, 1,084 of 1,085 tests pass. The one failure,hitArea.test.ts"batches every pending control into one frame: all reads, then all writes", fails the same way on themaincommit the stack sits on (d23d1fe438). NumberInput andtimeFormattests pass too.pnpm exec tsc --noEmit -p packages/core: clean.pnpm check:knowledge,pnpm check:changesets: pass.node scripts/pre-review.mjs --base main: 1,997 authored lines.Writing rubric results
No knowledge record is touched.