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

Convert to local time #581

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
120 changes: 46 additions & 74 deletions src/calendars/parsing/ics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ import { OFCEvent, validateEvent } from "../../types";
import { DateTime } from "luxon";
import { rrulestr } from "rrule";

function getDate(t: ical.Time): string {
return DateTime.fromSeconds(t.toUnixTime(), { zone: "UTC" }).toISODate();
function getDate(dateTime: DateTime): string {
return dateTime.toISODate();
}

function getTime(t: ical.Time): string {
if (t.isDate) {
return "00:00";
}
return DateTime.fromSeconds(t.toUnixTime(), { zone: "UTC" }).toISOTime({
function getTime(dateTime: DateTime): string {
return dateTime.toISOTime({
includeOffset: false,
includePrefix: false,
suppressMilliseconds: true,
suppressSeconds: true,
});
Expand All @@ -24,70 +20,61 @@ function extractEventUrl(iCalEvent: ical.Event): string {
return urlProp ? urlProp.getFirstValue() : "";
}

function specifiesEnd(iCalEvent: ical.Event) {
return (
Boolean(iCalEvent.component.getFirstProperty("dtend")) ||
Boolean(iCalEvent.component.getFirstProperty("duration"))
);

function specifiesEnd(iCalEvent: ical.Event): boolean {
return Boolean(iCalEvent.component.getFirstProperty("dtend")) ||
Boolean(iCalEvent.component.getFirstProperty("duration"));
}

function convertUtcToLocal(time: ical.Time): DateTime {
const isUtc = !time.zone || time.zone.tzid === 'Z'; // Use 'Z' to detect standard UTC, otherwise its floating
const jsDate = time.toJSDate();
if (isUtc) {
return DateTime.fromJSDate(jsDate, { zone: 'utc' }).setZone('local');
}
return DateTime.fromJSDate(jsDate, { zone: 'local' });
}

function icsToOFC(input: ical.Event): OFCEvent {
const isAllDay = input.startDate.isDate;

if (input.isRecurring()) {
const rrule = rrulestr(
input.component.getFirstProperty("rrule").getFirstValue().toString()
);
const allDay = input.startDate.isDate;
const exdates = input.component
.getAllProperties("exdate")
.map((exdateProp) => {
const exdate = exdateProp.getFirstValue();
// NOTE: We only store the date from an exdate and recreate the full datetime exdate later,
// so recurring events with exclusions that happen more than once per day are not supported.
return getDate(exdate);
});
const rrule = rrulestr(input.component.getFirstProperty("rrule").getFirstValue().toString());
const exdates = input.component.getAllProperties("exdate").map((exdateProp) => {
const exdate = exdateProp.getFirstValue();
return getDate(convertUtcToLocal(exdate));
});

return {
type: "rrule",
title: input.summary,
id: `ics::${input.uid}::${getDate(input.startDate)}::recurring`,
id: `ics::${input.uid}::${getDate(convertUtcToLocal(input.startDate))}::recurring`,
rrule: rrule.toString(),
skipDates: exdates,
startDate: getDate(
input.startDate.convertToZone(ical.Timezone.utcTimezone)
),
...(allDay
startDate: getDate(convertUtcToLocal(input.startDate)),
...(isAllDay
? { allDay: true }
: {
allDay: false,
startTime: getTime(
input.startDate.convertToZone(
ical.Timezone.utcTimezone
)
),
endTime: getTime(
input.endDate.convertToZone(ical.Timezone.utcTimezone)
),
startTime: getTime(convertUtcToLocal(input.startDate)),
endTime: getTime(convertUtcToLocal(input.endDate)),
}),
};
} else {
const date = getDate(input.startDate);
const endDate =
specifiesEnd(input) && input.endDate
? getDate(input.endDate)
: undefined;
const allDay = input.startDate.isDate;
const localStart = convertUtcToLocal(input.startDate);
const localEnd = specifiesEnd(input) && input.endDate ? convertUtcToLocal(input.endDate) : undefined;
return {
type: "single",
id: `ics::${input.uid}::${date}::single`,
id: `ics::${input.uid}::${getDate(localStart)}::single`,
title: input.summary,
date,
endDate: date !== endDate ? endDate || null : null,
...(allDay
date: getDate(localStart),
endDate: localEnd ? getDate(localEnd) : null,
...(isAllDay
? { allDay: true }
: {
allDay: false,
startTime: getTime(input.startDate),
endTime: getTime(input.endDate),
startTime: getTime(localStart),
endTime: localEnd ? getTime(localEnd) : null,
}),
};
}
Expand All @@ -97,42 +84,29 @@ export function getEventsFromICS(text: string): OFCEvent[] {
const jCalData = ical.parse(text);
const component = new ical.Component(jCalData);

// TODO: Timezone support
// const tzc = component.getAllSubcomponents("vtimezone");
// const tz = new ical.Timezone(tzc[0]);

const events: ical.Event[] = component
.getAllSubcomponents("vevent")
const events: ical.Event[] = component.getAllSubcomponents("vevent")
.map((vevent) => new ical.Event(vevent))
.filter((evt) => {
evt.iterator;
try {
evt.startDate.toJSDate();
evt.endDate.toJSDate();
if (evt.endDate) evt.endDate.toJSDate();
return true;
} catch (err) {
// skipping events with invalid time
return false;
return false; // skip invalid date events
}
});

// Events with RECURRENCE-ID will have duplicated UIDs.
// We need to modify the base event to exclude those recurrence exceptions.
const baseEvents = Object.fromEntries(
events
.filter((e) => e.recurrenceId === null)
.map((e) => [e.uid, icsToOFC(e)])
events.filter((e) => e.recurrenceId === null).map((e) => [e.uid, icsToOFC(e)])
);

const recurrenceExceptions = events
.filter((e) => e.recurrenceId !== null)
.map((e): [string, OFCEvent] => [e.uid, icsToOFC(e)]);
.map((e) => [e.uid, icsToOFC(e)] as [string, OFCEvent]);

for (const [uid, event] of recurrenceExceptions) {
const baseEvent = baseEvents[uid];
if (!baseEvent) {
continue;
}
if (!baseEvent) continue;

if (baseEvent.type !== "rrule" || event.type !== "single") {
console.warn(
Expand All @@ -144,9 +118,7 @@ export function getEventsFromICS(text: string): OFCEvent[] {
baseEvent.skipDates.push(event.date);
}

const allEvents = Object.values(baseEvents).concat(
recurrenceExceptions.map((e) => e[1])
);

return allEvents.map(validateEvent).flatMap((e) => (e ? [e] : []));
return [...Object.values(baseEvents), ...recurrenceExceptions.map((e) => e[1])]
.map(validateEvent)
.flatMap((e) => (e ? [e] : []));
}