@@ -38,6 +38,31 @@ const WALL_INSTANT_PATTERN =
3838const LOCALIZED_WALL_CLOCK_PATTERN =
3939 / ^ ( \d { 1 , 2 } ) \/ ( \d { 1 , 2 } ) \/ ( \d { 4 } ) [ , ] + ( \d { 1 , 2 } ) : ( \d { 2 } ) (?: : ( \d { 2 } ) ) ? (?: \s * ( A M | P M ) ) ? $ / i
4040
41+ const MONTH_NAME_PATTERN =
42+ 'Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?'
43+ const MONTH_FIRST_DATE_PATTERN = new RegExp (
44+ `\\b(${ MONTH_NAME_PATTERN } )\\s+(\\d{1,2})(?:,)?\\s+(\\d{4})\\b` ,
45+ 'i'
46+ )
47+ const DAY_FIRST_DATE_PATTERN = new RegExp (
48+ `\\b(\\d{1,2})\\s+(${ MONTH_NAME_PATTERN } )(?:,)?\\s+(\\d{4})\\b` ,
49+ 'i'
50+ )
51+ const MONTH_BY_ABBREVIATION : Record < string , number > = {
52+ JAN : 1 ,
53+ FEB : 2 ,
54+ MAR : 3 ,
55+ APR : 4 ,
56+ MAY : 5 ,
57+ JUN : 6 ,
58+ JUL : 7 ,
59+ AUG : 8 ,
60+ SEP : 9 ,
61+ OCT : 10 ,
62+ NOV : 11 ,
63+ DEC : 12 ,
64+ }
65+
4166/**
4267 * Legacy shape: old CSV imports stored date-only columns as UTC-midnight
4368 * instants. Treated as calendar dates so historical rows render as pure days
@@ -183,11 +208,46 @@ function parseLocalizedWallClock(match: RegExpMatchArray): string | null {
183208 )
184209}
185210
211+ interface CalendarFields {
212+ year : number
213+ month : number
214+ day : number
215+ }
216+
217+ /** Extracts literal calendar fields from supported month-name date forms. */
218+ function extractMonthNameCalendar ( value : string ) : CalendarFields | null {
219+ const monthFirst = value . match ( MONTH_FIRST_DATE_PATTERN )
220+ if ( monthFirst ) {
221+ return {
222+ year : Number ( monthFirst [ 3 ] ) ,
223+ month : MONTH_BY_ABBREVIATION [ monthFirst [ 1 ] . slice ( 0 , 3 ) . toUpperCase ( ) ] ,
224+ day : Number ( monthFirst [ 2 ] ) ,
225+ }
226+ }
227+ const dayFirst = value . match ( DAY_FIRST_DATE_PATTERN )
228+ if ( ! dayFirst ) return null
229+ return {
230+ year : Number ( dayFirst [ 3 ] ) ,
231+ month : MONTH_BY_ABBREVIATION [ dayFirst [ 2 ] . slice ( 0 , 3 ) . toUpperCase ( ) ] ,
232+ day : Number ( dayFirst [ 1 ] ) ,
233+ }
234+ }
235+
186236/** Recovers broader naive `Date.parse` inputs without consulting the runtime timezone. */
187237function parseNaiveWallClockAsUtc ( value : string ) : string | null {
238+ const calendar = extractMonthNameCalendar ( value )
239+ if ( calendar && ! isValidCalendarDay ( calendar . year , calendar . month , calendar . day ) ) return null
188240 const ms = Date . parse ( `${ value } UTC` )
189241 if ( Number . isNaN ( ms ) ) return null
190242 const parsed = new Date ( ms )
243+ if (
244+ calendar &&
245+ ( parsed . getUTCFullYear ( ) !== calendar . year ||
246+ parsed . getUTCMonth ( ) + 1 !== calendar . month ||
247+ parsed . getUTCDate ( ) !== calendar . day )
248+ ) {
249+ return null
250+ }
191251 return `${ toUtcCalendarDate ( parsed ) } T${ pad ( parsed . getUTCHours ( ) ) } :${ pad ( parsed . getUTCMinutes ( ) ) } :${ pad ( parsed . getUTCSeconds ( ) ) } `
192252}
193253
0 commit comments