Skip to content

Commit 8b609ee

Browse files
authored
Merge pull request #1 from ArenSH/fixup-ics-timezones
Fix timezones in ics calendars
2 parents 9601cb5 + a5f9445 commit 8b609ee

6 files changed

Lines changed: 433 additions & 86 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "obsidian-full-calendar",
3-
"version": "0.10.7",
3+
"version": "0.10.8",
44
"description": "Obsidian integration with Full Calendar (fullcalendar.io)",
55
"main": "main.js",
66
"scripts": {

src/calendars/parsing/__snapshots__/ics.test.ts.snap

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,19 @@ VERSION:2.0
4242
CALSCALE:GREGORIAN
4343
METHOD:PUBLISH
4444
X-WR-CALNAME:Obsidian Test Calendar
45-
X-WR-TIMEZONE:America/New_York
45+
X-WR-TIMEZONE:UTC
4646
BEGIN:VTIMEZONE
47-
TZID:America/New_York
48-
X-LIC-LOCATION:America/New_York
49-
BEGIN:DAYLIGHT
50-
TZOFFSETFROM:-0500
51-
TZOFFSETTO:-0400
52-
TZNAME:EDT
53-
DTSTART:19700308T020000
54-
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
55-
END:DAYLIGHT
47+
TZID:UTC
5648
BEGIN:STANDARD
57-
TZOFFSETFROM:-0400
58-
TZOFFSETTO:-0500
59-
TZNAME:EST
60-
DTSTART:19701101T020000
61-
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
49+
DTSTART:16010101T000000
50+
TZOFFSETFROM:+0000
51+
TZOFFSETTO:+0000
6252
END:STANDARD
53+
BEGIN:DAYLIGHT
54+
DTSTART:16010101T000000
55+
TZOFFSETFROM:+0000
56+
TZOFFSETTO:+0000
57+
END:DAYLIGHT
6358
END:VTIMEZONE
6459
BEGIN:VEVENT
6560
DTSTART;VALUE=DATE:20220302
@@ -76,8 +71,8 @@ SUMMARY:All day event
7671
TRANSP:TRANSPARENT
7772
END:VEVENT
7873
BEGIN:VEVENT
79-
DTSTART;TZID=America/New_York:20220301T110000
80-
DTEND;TZID=America/New_York:20220301T123000
74+
DTSTART;TZID=UTC:20220301T110000
75+
DTEND;TZID=UTC:20220301T123000
8176
RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TH,TU
8277
DTSTAMP:20230302T233513Z
8378
UID:5tt2avr2th0h65homv3b6jeqof@google.com
@@ -156,32 +151,32 @@ END:VCALENDAR
156151
},
157152
{
158153
"allDay": false,
159-
"endTime": "12:30",
154+
"endTime": "13:30",
160155
"id": "ics::5tt2avr2th0h65homv3b6jeqof@google.com::2022-03-01::recurring",
161156
"rrule": "RRULE:FREQ=WEEKLY;BYDAY=TH,TU;WKST=SU",
162157
"skipDates": [],
163158
"startDate": "2022-03-01",
164-
"startTime": "11:00",
159+
"startTime": "12:00",
165160
"title": "Recurring event",
166161
"type": "rrule",
167162
},
168163
{
169164
"allDay": false,
170165
"date": "2022-02-28",
171166
"endDate": null,
172-
"endTime": "19:45",
167+
"endTime": "20:45",
173168
"id": "ics::40mdbe6fvc1rmd60n6r0c3go7e@google.com::2022-02-28::single",
174-
"startTime": "16:45",
169+
"startTime": "17:45",
175170
"title": "Hello, iCal!",
176171
"type": "single",
177172
},
178173
{
179174
"allDay": false,
180175
"date": "2022-02-19",
181-
"endDate": null,
182-
"endTime": "23:00",
176+
"endDate": "2022-02-20",
177+
"endTime": "00:00",
183178
"id": "ics::44hekcaaf0or7547vhqa772mqj@google.com::2022-02-19::single",
184-
"startTime": "19:00",
179+
"startTime": "20:00",
185180
"title": "Work on GCal Sync",
186181
"type": "single",
187182
},

src/calendars/parsing/ics.test.ts

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,34 @@
11
import { 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

333
describe("ics tests", () => {
434
it("parses all day event", () => {
@@ -37,25 +67,8 @@ VERSION:2.0
3767
CALSCALE:GREGORIAN
3868
METHOD:PUBLISH
3969
X-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}
5972
BEGIN:VEVENT
6073
DTSTART;VALUE=DATE:20220302
6174
DTEND;VALUE=DATE:20220303
@@ -71,8 +84,8 @@ SUMMARY:All day event
7184
TRANSP:TRANSPARENT
7285
END:VEVENT
7386
BEGIN: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
7689
RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TH,TU
7790
DTSTAMP:20230302T233513Z
7891
UID: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

Comments
 (0)