diff --git a/frontend/week-calendar-import.js b/frontend/week-calendar-import.js index d1bf4b0..51bbef4 100644 --- a/frontend/week-calendar-import.js +++ b/frontend/week-calendar-import.js @@ -1,5 +1,6 @@ function createWeekCalendarImport() { const maxBytes=1024*1024; + const zonedInstantFormatters=new Map(),zonePartFormatters=new Map(),supportedTimeZones=new Map(); function clock(value) { const match=/^(\d{2}):(\d{2})$/.exec(String(value||'')); if(!match)return null; @@ -19,9 +20,11 @@ function createWeekCalendarImport() { const match=/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/.exec(value||''); if(!match)return null; try { - const wanted=match.slice(1).map(Number),formatter=new Intl.DateTimeFormat('en-CA',{ + const wanted=match.slice(1).map(Number); + if(!zonedInstantFormatters.has(timeZone))zonedInstantFormatters.set(timeZone,new Intl.DateTimeFormat('en-CA',{ timeZone,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',hourCycle:'h23', - }); + })); + const formatter=zonedInstantFormatters.get(timeZone); let result=Date.UTC(wanted[0],wanted[1]-1,wanted[2],wanted[3],wanted[4],wanted[5]); for(let attempt=0;attempt<2;attempt+=1) { const shown=Object.fromEntries(formatter.formatToParts(new Date(result)).map(part=>[part.type,part.value])); @@ -33,15 +36,18 @@ function createWeekCalendarImport() { } function supportedTimeZone(timeZone) { if(!timeZone)return true; - try {new Intl.DateTimeFormat('en-US',{timeZone}).format(0);return true;} - catch(error) {return false;} + if(supportedTimeZones.has(timeZone))return supportedTimeZones.get(timeZone); + try {new Intl.DateTimeFormat('en-US',{timeZone}).format(0);supportedTimeZones.set(timeZone,true);return true;} + catch(error) {supportedTimeZones.set(timeZone,false);return false;} } function zoneParts(value,timeZone) { const date=new Date(value); if(!timeZone)return {year:date.getFullYear(),month:date.getMonth()+1,day:date.getDate(),weekday:date.getDay(), hour:date.getHours(),minute:date.getMinutes(),second:date.getSeconds()}; - const formatter=new Intl.DateTimeFormat('en-US',{timeZone,year:'numeric',month:'2-digit',day:'2-digit', - hour:'2-digit',minute:'2-digit',second:'2-digit',weekday:'short',hourCycle:'h23'}); + if(!zonePartFormatters.has(timeZone))zonePartFormatters.set(timeZone,new Intl.DateTimeFormat('en-US',{ + timeZone,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',weekday:'short',hourCycle:'h23', + })); + const formatter=zonePartFormatters.get(timeZone); const parts=Object.fromEntries(formatter.formatToParts(date).map(part=>[part.type,part.value])); return {year:Number(parts.year),month:Number(parts.month),day:Number(parts.day), weekday:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].indexOf(parts.weekday),hour:Number(parts.hour), diff --git a/tests/test_week_calendar_import.py b/tests/test_week_calendar_import.py index bff01c6..9a9d9f0 100644 --- a/tests/test_week_calendar_import.py +++ b/tests/test_week_calendar_import.py @@ -118,12 +118,11 @@ process.env.TZ='UTC'; const event=`BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20000101T090000\r\nDTEND;TZID=America/New_York:20000101T100000\r\nRRULE:FREQ=DAILY\r\nEND:VEVENT\r\n`; const source='BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\n'+event.repeat(80)+'END:VCALENDAR'; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; -const started=Date.now();const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); -console.log(JSON.stringify({elapsed:Date.now()-started,busy:days.map(day=>day.busy_minutes)})); +const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); +console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)})); """) assert result["busy"] == [60] * 7 - assert result["elapsed"] < 1000 def test_calendar_import_fast_forwards_historical_weekly_series_to_seven_day_range(): @@ -132,12 +131,32 @@ process.env.TZ='UTC'; const event=`BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20000101T090000\r\nDTEND;TZID=America/New_York:20000101T100000\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2\r\nEND:VEVENT\r\n`; const source='BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\n'+event.repeat(80)+'END:VCALENDAR'; const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; -const started=Date.now();const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); -console.log(JSON.stringify({elapsed:Date.now()-started,busy:days.map(day=>day.busy_minutes)})); +const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); +console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)})); """) assert result["busy"] == [0, 0, 0, 60, 0, 60, 0] - assert result["elapsed"] < 1000 + + +def test_calendar_import_reuses_timezone_formatters_for_large_historical_calendars(): + result = run_import(""" +process.env.TZ='UTC'; +const NativeDateTimeFormat=Intl.DateTimeFormat; +let formatterConstructions=0; +Intl.DateTimeFormat=function(...args) { + formatterConstructions+=1; + return new NativeDateTimeFormat(...args); +}; +Intl.DateTimeFormat.prototype=NativeDateTimeFormat.prototype; +const event=`BEGIN:VEVENT\r\nDTSTART;TZID=America/New_York:20000101T090000\r\nDTEND;TZID=America/New_York:20000101T100000\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2\r\nEND:VEVENT\r\n`; +const source='BEGIN:VCALENDAR\\r\\nVERSION:2.0\\r\\n'+event.repeat(80)+'END:VCALENDAR'; +const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27']; +const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'}); +console.log(JSON.stringify({formatterConstructions,busy:days.map(day=>day.busy_minutes)})); +""") + + assert result["busy"] == [0, 0, 0, 60, 0, 60, 0] + assert result["formatterConstructions"] <= 3 def test_calendar_import_expands_bounded_daily_recurrence_and_honors_exdate():