-
Notifications
You must be signed in to change notification settings - Fork 13
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
Refactor: Expand recurring events until end date is reached #48
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -99,7 +99,7 @@ function getNormalizedEndDate(endDate, duration) { | |
endDate.toISOString(); | ||
} | ||
|
||
function getNormalOccurenceEventData(nextEvent, eventData, vevent) { | ||
function getNormalOccurenceEventData(nextEvent, vevent) { | ||
const dtstart = nextEvent.startDate; | ||
const dtend = nextEvent.endDate; | ||
const { summary: title, uid, location, description } = nextEvent.item; | ||
|
@@ -118,7 +118,7 @@ function getNormalOccurenceEventData(nextEvent, eventData, vevent) { | |
}; | ||
} | ||
|
||
function getModifiedOccurenceEventData(vevent, eventData) { | ||
function getModifiedOccurenceEventData(vevent) { | ||
const dtstart = vevent.getFirstPropertyValue("dtstart"); | ||
const dtend = vevent.getFirstPropertyValue("dtend"); | ||
const duration = getNormalizedDuration(dtstart, dtend); | ||
|
@@ -136,7 +136,7 @@ function getModifiedOccurenceEventData(vevent, eventData) { | |
}; | ||
} | ||
|
||
function parseEvents(xml) { | ||
function parseEvents(xml, maxExpandCount, startDate = null, endDate = null) { | ||
let parsed; | ||
const formatted = []; | ||
|
||
|
@@ -152,8 +152,6 @@ function parseEvents(xml) { | |
|
||
etag = stripDoubleQuotes(etag); | ||
|
||
let eventData = {}; | ||
|
||
if (!event.propstat[0].prop[0]["calendar-data"]) { | ||
return; | ||
} | ||
|
@@ -178,48 +176,51 @@ function parseEvents(xml) { | |
const icalEvent = new ICAL.Event(vevent); | ||
|
||
if (icalEvent.isRecurring()) { | ||
// This is a recurring event, expand the next few occurances | ||
const expand = new ICAL.RecurExpansion({ | ||
component: vevent, | ||
dtstart: vevent.getFirstPropertyValue("dtstart"), | ||
}); | ||
const startRange = startDate ? ICAL.Time.fromDateTimeString(moment(startDate).toISOString()) : null; | ||
const endRange = endDate ? ICAL.Time.fromDateTimeString(moment(endDate).toISOString()) : null; | ||
let occurrencesCount = 0; | ||
|
||
for (let next = expand.next(); next; next = expand.next()) { | ||
// If max iteration count is reached: break | ||
if (occurrencesCount >= maxExpandCount) { | ||
break; | ||
} | ||
|
||
// If date is after end date: break | ||
if (endRange && next.compare(endRange) > 0) { | ||
break; | ||
} | ||
|
||
// Since there are infinite rules, its a good idea to limit the scope | ||
// of the iteration then resume later on | ||
// Expand upto a maximum of 10 upcoming occurances | ||
for (let i = 0; i < 10; i++) { | ||
const nextOccuranceTime = expand.next(); | ||
|
||
if (!expand.complete) { | ||
// Handle this next expanded occurence | ||
const nextEvent = icalEvent.getOccurrenceDetails(nextOccuranceTime); | ||
|
||
eventData = {}; | ||
|
||
if (modifiedOccurences.length === 0) { | ||
// No events have been modified | ||
formatted.push({ | ||
ics: event.href[0], | ||
etag, | ||
data: getNormalOccurenceEventData(nextEvent, eventData, vevent), | ||
}); | ||
} else if (isModified(nextOccuranceTime, modifiedOccurences)) { | ||
// This is the event that has been modied | ||
const key = getModifiedOccuranceKey(nextOccuranceTime, modifiedOccurences) || 0; | ||
|
||
formatted.push({ | ||
ics: event.href[0], | ||
etag, | ||
data: getModifiedOccurenceEventData(vevents[key], eventData), | ||
}); | ||
} else { | ||
// Expand this event normally | ||
formatted.push({ | ||
ics: event.href[0], | ||
etag, | ||
data: getNormalOccurenceEventData(nextEvent, eventData, vevent), | ||
}); | ||
} | ||
// If date is before start date: continue | ||
if (startRange && next.compare(startRange) < 0) { | ||
continue; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like how the break/continue conditions are at the beginning of the for loop 👍 Personally, I would've reached for a while loop here: while (next = expand.next()) {
// do something with next
} but that's just a minor stylistic preference. Logic-wise I'm not 100% sure if it's a good idea to include the possibility of an almost infinite loop. The problem here is that the library user of scrapegoat has no way to limit the iteration count if bad data is passed to the parser. Personally I'd prefer the library user to specify the upper iteration bound, like Furthermore I think there's an opportunity for improvement here: we could remove all recurring events that start after the specified end date. That would save us some iterations. So my preferred logic would be:
Maybe you can also add an error if startRange is after endRange. What do you think? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for raising the almost infinite loop possibility 👍 The only case where I see an almost infinite loop occurring is when an event is finite but has an end date (or count) that is way too far in the future (for infinite events we are already capping the iterations to 10). Are we seeing this the same way? I like the idea of the library user specifying the upper iteration bound 👍 If I understand you well, I will include const config = {
auth: {
user: BAIKAL_USER,
pass: BAIKAL_PASSWORD,
},
uri: BAIKAL_URI,
maxExpandCount: 10,
};
This situation does not exist. When we query CalDav with an end date specified, the response we get only includes recurring events that start (or have an occurrence) between the startRange and endRange.
Here is where It gets blurry again: (going with the logic you suggested) CASE 1: startRange and endRange provided and maxExpandCount: Infinity
CASE 2: startRange provided, endRange not provided and maxExpandCount: Infinity
Right now we silently change the endRange to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I was referring to this case. I don't know how common that is. But I know that some tools limit the max iteration count or the end date when creating new re-occurring events. I think I would scope the config option under const config = {
auth: {
user: BAIKAL_USER,
pass: BAIKAL_PASSWORD,
},
uri: BAIKAL_URI,
events: {
maxExpandCount: 10,
}
};
Ok, I didn't know that. Thanks for clarifying. You could add a comment in the code that explains this. About the logic behind I think Regarding your concern when an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool, thanks for the clarification. This is now implemented and ready for review. |
||
|
||
occurrencesCount += 1; | ||
|
||
// Handle this next expanded occurence | ||
const nextEvent = icalEvent.getOccurrenceDetails(next); | ||
|
||
if (modifiedOccurences.length && isModified(next, modifiedOccurences)) { | ||
// This event has been modified | ||
const key = getModifiedOccuranceKey(next, modifiedOccurences); | ||
|
||
formatted.push({ | ||
ics: event.href[0], | ||
etag, | ||
data: getModifiedOccurenceEventData(vevents[key]), | ||
}); | ||
} else { | ||
// Expand this event normally | ||
formatted.push({ | ||
ics: event.href[0], | ||
etag, | ||
data: getNormalOccurenceEventData(nextEvent, vevent), | ||
}); | ||
} | ||
} | ||
} else { | ||
|
@@ -228,8 +229,7 @@ function parseEvents(xml) { | |
const dtstart = vevent.getFirstPropertyValue("dtstart"); | ||
const dtend = vevent.getFirstPropertyValue("dtend"); | ||
const duration = getNormalizedDuration(dtstart, dtend); | ||
|
||
eventData = { | ||
const eventData = { | ||
title: vevent.getFirstPropertyValue("summary"), | ||
uid: vevent.getFirstPropertyValue("uid"), | ||
location: vevent.getFirstPropertyValue("location"), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for cleaning this up :)