11import { getEventsFromICS } from "./ics" ;
2+ import { DateTime } from "luxon" ;
3+
4+ const LOCAL_TIME_ZONE = DateTime . local ( ) . zone ;
5+ const VTIMEZONE_GEORGIAN = `BEGIN:VTIMEZONE
6+ TZID:Georgian Standard Time
7+ BEGIN:STANDARD
8+ DTSTART:16010101T000000
9+ TZOFFSETFROM:+0400
10+ TZOFFSETTO:+0400
11+ END:STANDARD
12+ BEGIN:DAYLIGHT
13+ DTSTART:16010101T000000
14+ TZOFFSETFROM:+0400
15+ TZOFFSETTO:+0400
16+ END:DAYLIGHT
17+ END:VTIMEZONE` ;
18+
19+ const VTIMEZONE_UTC0 = `BEGIN:VTIMEZONE
20+ TZID:UTC
21+ BEGIN:STANDARD
22+ DTSTART:16010101T000000
23+ TZOFFSETFROM:+0000
24+ TZOFFSETTO:+0000
25+ END:STANDARD
26+ BEGIN:DAYLIGHT
27+ DTSTART:16010101T000000
28+ TZOFFSETFROM:+0000
29+ TZOFFSETTO:+0000
30+ END:DAYLIGHT
31+ END:VTIMEZONE` ;
232
333describe ( "ics tests" , ( ) => {
434 it ( "parses all day event" , ( ) => {
@@ -37,25 +67,8 @@ VERSION:2.0
3767CALSCALE:GREGORIAN
3868METHOD:PUBLISH
3969X-WR-CALNAME:Obsidian Test Calendar
40- X-WR-TIMEZONE:America/New_York
41- BEGIN:VTIMEZONE
42- TZID:America/New_York
43- X-LIC-LOCATION:America/New_York
44- BEGIN:DAYLIGHT
45- TZOFFSETFROM:-0500
46- TZOFFSETTO:-0400
47- TZNAME:EDT
48- DTSTART:19700308T020000
49- RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
50- END:DAYLIGHT
51- BEGIN:STANDARD
52- TZOFFSETFROM:-0400
53- TZOFFSETTO:-0500
54- TZNAME:EST
55- DTSTART:19701101T020000
56- RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
57- END:STANDARD
58- END:VTIMEZONE
70+ X-WR-TIMEZONE:UTC
71+ ${ VTIMEZONE_UTC0 }
5972BEGIN:VEVENT
6073DTSTART;VALUE=DATE:20220302
6174DTEND;VALUE=DATE:20220303
@@ -71,8 +84,8 @@ SUMMARY:All day event
7184TRANSP:TRANSPARENT
7285END:VEVENT
7386BEGIN:VEVENT
74- DTSTART;TZID=America/New_York :20220301T110000
75- DTEND;TZID=America/New_York :20220301T123000
87+ DTSTART;TZID=UTC :20220301T110000
88+ DTEND;TZID=UTC :20220301T123000
7689RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TH,TU
7790DTSTAMP:20230302T233513Z
7891UID:5tt2avr2th0h65homv3b6jeqof@google.com
@@ -137,4 +150,93 @@ END:VCALENDAR
137150 const events = getEventsFromICS ( ics ) ;
138151 expect ( events ) . toMatchSnapshot ( ics ) ;
139152 } ) ;
153+
154+ it ( "should convert timezones correctly using defined VTIMEZONE" , ( ) => {
155+ const ics = [
156+ "BEGIN:VCALENDAR" ,
157+ "METHOD:PUBLISH" ,
158+ "PRODID:Microsoft Exchange Server 2010" ,
159+ "VERSION:2.0" ,
160+ "X-WR-CALNAME:Calendar" ,
161+ VTIMEZONE_GEORGIAN ,
162+
163+ "BEGIN:VEVENT" ,
164+ "DESCRIPTION: Event With TimeZone" ,
165+ "EXDATE;TZID=Georgian Standard Time:20231225T150000" ,
166+ "SUMMARY: Event With TimeZone" ,
167+ "DTSTART;TZID=Georgian Standard Time:20231225T150000" ,
168+ "DTEND;TZID=Georgian Standard Time:20231225T160000" ,
169+ "END:VEVENT" ,
170+ "END:VCALENDAR" ,
171+ ] . join ( "\n" ) ;
172+
173+ const events = getEventsFromICS ( ics ) as any [ ] ;
174+ expect ( events . length ) . toBe ( 1 ) ;
175+
176+ expect ( events [ 0 ] . endTime ) . toBe ( timeFromUTCSeconds ( 1703505600 ) ) ;
177+ expect ( events [ 0 ] . startTime ) . toBe ( timeFromUTCSeconds ( 1703502000 ) ) ;
178+ } ) ;
179+
180+ it ( "should convert timezones correctly using alias for VTIMEZONE" , ( ) => {
181+ const ics = [
182+ "BEGIN:VCALENDAR" ,
183+ "METHOD:PUBLISH" ,
184+ "PRODID:Microsoft Exchange Server 2010" ,
185+ "VERSION:2.0" ,
186+ "X-WR-CALNAME:Calendar" ,
187+ VTIMEZONE_GEORGIAN ,
188+
189+ "BEGIN:VEVENT" ,
190+ "DESCRIPTION: Event With Alias" ,
191+ "EXDATE;TZID=Caucasus Standard Time:20231225T150000" ,
192+ "SUMMARY: Event With Alias" ,
193+ "DTSTART;TZID=Caucasus Standard Time:20231225T150000" ,
194+ "DTEND;TZID=Caucasus Standard Time:20231225T160000" ,
195+ "END:VEVENT" ,
196+ "END:VCALENDAR" ,
197+ ] . join ( "\n" ) ;
198+
199+ const events = getEventsFromICS ( ics ) as any [ ] ;
200+ expect ( events . length ) . toBe ( 1 ) ;
201+
202+ expect ( events [ 0 ] . endTime ) . toBe ( timeFromUTCSeconds ( 1703505600 ) ) ;
203+ expect ( events [ 0 ] . startTime ) . toBe ( timeFromUTCSeconds ( 1703502000 ) ) ;
204+ } ) ;
205+
206+ it ( "should fall back to UTC timezone if no VTIMEZONE found for event" , ( ) => {
207+ const ics = [
208+ "BEGIN:VCALENDAR" ,
209+ "METHOD:PUBLISH" ,
210+ "PRODID:Microsoft Exchange Server 2010" ,
211+ "VERSION:2.0" ,
212+ "X-WR-CALNAME:Calendar" ,
213+ VTIMEZONE_GEORGIAN ,
214+
215+ "BEGIN:VEVENT" ,
216+ "DESCRIPTION: Event With Alias" ,
217+ "EXDATE;TZID=Unknown Time:20231225T150000" ,
218+ "SUMMARY: Event With Alias" ,
219+ "DTSTART;TZID=Unknown Time:20231225T150000" ,
220+ "DTEND;TZID=Unknown Time:20231225T160000" ,
221+ "END:VEVENT" ,
222+ "END:VCALENDAR" ,
223+ ] . join ( "\n" ) ;
224+
225+ const events = getEventsFromICS ( ics ) as any [ ] ;
226+ expect ( events . length ) . toBe ( 1 ) ;
227+
228+ expect ( events [ 0 ] . endTime ) . toBe ( timeFromUTCSeconds ( 1703520000 ) ) ;
229+ expect ( events [ 0 ] . startTime ) . toBe ( timeFromUTCSeconds ( 1703516400 ) ) ;
230+ } ) ;
140231} ) ;
232+
233+ function timeFromUTCSeconds ( timestamp : number ) : string {
234+ return DateTime . fromSeconds ( timestamp , { zone : "UTC" } )
235+ . setZone ( LOCAL_TIME_ZONE )
236+ . toISOTime ( {
237+ includeOffset : false ,
238+ includePrefix : false ,
239+ suppressMilliseconds : true ,
240+ suppressSeconds : true ,
241+ } ) ;
242+ }
0 commit comments