Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix timezones in ICS calendars #540

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-full-calendar",
"version": "0.10.7",
"version": "0.10.8",
"description": "Obsidian integration with Full Calendar (fullcalendar.io)",
"main": "main.js",
"scripts": {
Expand Down
43 changes: 19 additions & 24 deletions src/calendars/parsing/__snapshots__/ics.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,19 @@ VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Obsidian Test Calendar
X-WR-TIMEZONE:America/New_York
X-WR-TIMEZONE:UTC
BEGIN:VTIMEZONE
TZID:America/New_York
X-LIC-LOCATION:America/New_York
BEGIN:DAYLIGHT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
TZID:UTC
BEGIN:STANDARD
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
DTSTART:16010101T000000
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T000000
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20220302
Expand All @@ -76,8 +71,8 @@ SUMMARY:All day event
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20220301T110000
DTEND;TZID=America/New_York:20220301T123000
DTSTART;TZID=UTC:20220301T110000
DTEND;TZID=UTC:20220301T123000
RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TH,TU
DTSTAMP:20230302T233513Z
UID:[email protected]
Expand Down Expand Up @@ -156,32 +151,32 @@ END:VCALENDAR
},
{
"allDay": false,
"endTime": "12:30",
"endTime": "13:30",
"id": "ics::[email protected]::2022-03-01::recurring",
"rrule": "RRULE:FREQ=WEEKLY;BYDAY=TH,TU;WKST=SU",
"skipDates": [],
"startDate": "2022-03-01",
"startTime": "11:00",
"startTime": "12:00",
"title": "Recurring event",
"type": "rrule",
},
{
"allDay": false,
"date": "2022-02-28",
"endDate": null,
"endTime": "19:45",
"endTime": "20:45",
"id": "ics::[email protected]::2022-02-28::single",
"startTime": "16:45",
"startTime": "17:45",
"title": "Hello, iCal!",
"type": "single",
},
{
"allDay": false,
"date": "2022-02-19",
"endDate": null,
"endTime": "23:00",
"endDate": "2022-02-20",
"endTime": "00:00",
"id": "ics::[email protected]::2022-02-19::single",
"startTime": "19:00",
"startTime": "20:00",
"title": "Work on GCal Sync",
"type": "single",
},
Expand Down
144 changes: 123 additions & 21 deletions src/calendars/parsing/ics.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
import { getEventsFromICS } from "./ics";
import { DateTime } from "luxon";

const LOCAL_TIME_ZONE = DateTime.local().zone;
const VTIMEZONE_GEORGIAN = `BEGIN:VTIMEZONE
TZID:Georgian Standard Time
BEGIN:STANDARD
DTSTART:16010101T000000
TZOFFSETFROM:+0400
TZOFFSETTO:+0400
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T000000
TZOFFSETFROM:+0400
TZOFFSETTO:+0400
END:DAYLIGHT
END:VTIMEZONE`;

const VTIMEZONE_UTC0 = `BEGIN:VTIMEZONE
TZID:UTC
BEGIN:STANDARD
DTSTART:16010101T000000
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:16010101T000000
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
END:DAYLIGHT
END:VTIMEZONE`;

describe("ics tests", () => {
it("parses all day event", () => {
Expand Down Expand Up @@ -37,25 +67,8 @@ VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Obsidian Test Calendar
X-WR-TIMEZONE:America/New_York
BEGIN:VTIMEZONE
TZID:America/New_York
X-LIC-LOCATION:America/New_York
BEGIN:DAYLIGHT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
X-WR-TIMEZONE:UTC
${VTIMEZONE_UTC0}
BEGIN:VEVENT
DTSTART;VALUE=DATE:20220302
DTEND;VALUE=DATE:20220303
Expand All @@ -71,8 +84,8 @@ SUMMARY:All day event
TRANSP:TRANSPARENT
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20220301T110000
DTEND;TZID=America/New_York:20220301T123000
DTSTART;TZID=UTC:20220301T110000
DTEND;TZID=UTC:20220301T123000
RRULE:FREQ=WEEKLY;WKST=SU;BYDAY=TH,TU
DTSTAMP:20230302T233513Z
UID:[email protected]
Expand Down Expand Up @@ -137,4 +150,93 @@ END:VCALENDAR
const events = getEventsFromICS(ics);
expect(events).toMatchSnapshot(ics);
});

it("should convert timezones correctly using defined VTIMEZONE", () => {
const ics = [
"BEGIN:VCALENDAR",
"METHOD:PUBLISH",
"PRODID:Microsoft Exchange Server 2010",
"VERSION:2.0",
"X-WR-CALNAME:Calendar",
VTIMEZONE_GEORGIAN,

"BEGIN:VEVENT",
"DESCRIPTION: Event With TimeZone",
"EXDATE;TZID=Georgian Standard Time:20231225T150000",
"SUMMARY: Event With TimeZone",
"DTSTART;TZID=Georgian Standard Time:20231225T150000",
"DTEND;TZID=Georgian Standard Time:20231225T160000",
"END:VEVENT",
"END:VCALENDAR",
].join("\n");

const events = getEventsFromICS(ics) as any[];
expect(events.length).toBe(1);

expect(events[0].endTime).toBe(timeFromUTCSeconds(1703505600));
expect(events[0].startTime).toBe(timeFromUTCSeconds(1703502000));
});

it("should convert timezones correctly using alias for VTIMEZONE", () => {
const ics = [
"BEGIN:VCALENDAR",
"METHOD:PUBLISH",
"PRODID:Microsoft Exchange Server 2010",
"VERSION:2.0",
"X-WR-CALNAME:Calendar",
VTIMEZONE_GEORGIAN,

"BEGIN:VEVENT",
"DESCRIPTION: Event With Alias",
"EXDATE;TZID=Caucasus Standard Time:20231225T150000",
"SUMMARY: Event With Alias",
"DTSTART;TZID=Caucasus Standard Time:20231225T150000",
"DTEND;TZID=Caucasus Standard Time:20231225T160000",
"END:VEVENT",
"END:VCALENDAR",
].join("\n");

const events = getEventsFromICS(ics) as any[];
expect(events.length).toBe(1);

expect(events[0].endTime).toBe(timeFromUTCSeconds(1703505600));
expect(events[0].startTime).toBe(timeFromUTCSeconds(1703502000));
});

it("should fall back to UTC timezone if no VTIMEZONE found for event", () => {
const ics = [
"BEGIN:VCALENDAR",
"METHOD:PUBLISH",
"PRODID:Microsoft Exchange Server 2010",
"VERSION:2.0",
"X-WR-CALNAME:Calendar",
VTIMEZONE_GEORGIAN,

"BEGIN:VEVENT",
"DESCRIPTION: Event With Alias",
"EXDATE;TZID=Unknown Time:20231225T150000",
"SUMMARY: Event With Alias",
"DTSTART;TZID=Unknown Time:20231225T150000",
"DTEND;TZID=Unknown Time:20231225T160000",
"END:VEVENT",
"END:VCALENDAR",
].join("\n");

const events = getEventsFromICS(ics) as any[];
expect(events.length).toBe(1);

expect(events[0].endTime).toBe(timeFromUTCSeconds(1703520000));
expect(events[0].startTime).toBe(timeFromUTCSeconds(1703516400));
});
});

function timeFromUTCSeconds(timestamp: number): string {
return DateTime.fromSeconds(timestamp, { zone: "UTC" })
.setZone(LOCAL_TIME_ZONE)
.toISOTime({
includeOffset: false,
includePrefix: false,
suppressMilliseconds: true,
suppressSeconds: true,
});
}
Loading