Skip to content
This repository was archived by the owner on Aug 11, 2021. It is now read-only.
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
9 changes: 9 additions & 0 deletions lib/calendar/exdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { toUTCDate } from '../date/timezone';

export const createExdateMap = (exdate = []) => {
return exdate.reduce((acc, dateProperty) => {
const localExclude = toUTCDate(dateProperty.value);
acc[+localExclude] = true;
return acc;
}, {});
};
150 changes: 101 additions & 49 deletions lib/calendar/recurring.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getInternalDateTimeValue, internalValueToIcalValue } from './vcal';
import { getPropertyTzid, isIcalAllDay, propertyToUTCDate } from './vcalConverter';
import { addDays, addMilliseconds, differenceInCalendarDays, max, MILLISECONDS_IN_MINUTE } from '../date-fns-utc';
import { convertUTCDateTimeToZone, fromUTCDate, toUTCDate } from '../date/timezone';
import { createExdateMap } from './exdate';

const YEAR_IN_MS = Date.UTC(1971, 0, 1);

Expand All @@ -13,38 +14,57 @@ export const isIcalRecurring = ({ rrule }) => {

const isInInterval = (a1, a2, b1, b2) => a1 <= b2 && a2 >= b1;

const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, isAllDay) => {
const fillOccurrencesBetween = ({
interval: [start, end],
iterator,
eventDuration,
originalDtstart,
isAllDay,
exdateMap
}) => {
const result = [];
let next;

// eslint-disable-next-line no-cond-assign
while ((next = iterator.next())) {
const localStart = toUTCDate(getInternalDateTimeValue(next));
if (exdateMap[+localStart]) {
continue;
}
const localEnd = isAllDay ? addDays(localStart, eventDuration) : addMilliseconds(localStart, eventDuration);

const utcStart = propertyToUTCDate({
value: {
...internalDtstart.value,
...fromUTCDate(localStart)
},
parameters: internalDtstart.parameters
});
const utcStart = isAllDay
? localStart
: propertyToUTCDate({
value: {
...originalDtstart.value,
...fromUTCDate(localStart)
},
parameters: originalDtstart.parameters
});

const utcEnd = isAllDay
? addDays(utcStart, eventDuration)
? localEnd
: propertyToUTCDate({
value: {
...internalDtstart.value,
...fromUTCDate(addMilliseconds(localStart, eventDuration))
...originalDtstart.value,
...fromUTCDate(localEnd)
},
parameters: internalDtstart.parameters
parameters: originalDtstart.parameters
});

if (utcStart > end) {
break;
}

if (isInInterval(utcStart, utcEnd, start, end)) {
result.push([utcStart, utcEnd]);
result.push({
localStart,
localEnd,
utcStart,
utcEnd,
occurrenceNumber: iterator.occurrence_number
});
}
}
return result;
Expand All @@ -71,39 +91,71 @@ const getModifiedUntilRrule = (internalRrule, startTzid) => {
};
};

export const getOccurrencesBetween = (component, start, end, cache = {}) => {
const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule } = component;
const getOccurrenceSetup = (component) => {
const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule, exdate: internalExdate } = component;

const isAllDay = isIcalAllDay(component);
const dtstartType = isAllDay ? 'date' : 'date-time';

// Pretend the (local) date is in UTC time to keep the absolute times.
const dtstart = internalValueToIcalValue(dtstartType, { ...internalDtstart.value, isUTC: true });
// Since the local date is pretended in UTC time, the until has to be converted into a fake local UTC time too
const modifiedRrule = getModifiedUntilRrule(internalRrule, getPropertyTzid(internalDtstart));

const utcStart = propertyToUTCDate(internalDtstart);
const rawEnd = propertyToUTCDate(internalDtEnd);
const modifiedEnd = isAllDay
? addDays(rawEnd, -1) // All day event range is non-inclusive
: rawEnd;
const utcEnd = max(utcStart, modifiedEnd);

const eventDuration = isAllDay
? differenceInCalendarDays(utcEnd, utcStart)
: differenceInMinutes(utcEnd, utcStart) * MILLISECONDS_IN_MINUTE;

return {
dtstart,
utcStart,
isAllDay,
eventDuration,
modifiedRrule,
exdateMap: createExdateMap(internalExdate)
};
};

export const getIsMoreThanOccurrence = (component, n = 1, cache = {}) => {
if (!cache.start) {
cache.start = getOccurrenceSetup(component);
}
const { dtstart, modifiedRrule, exdateMap } = cache.start;

const rrule = internalValueToIcalValue('recur', modifiedRrule.value);
const iterator = rrule.iterator(dtstart);

let next;
let count = 0;
// eslint-disable-next-line no-cond-assign
while ((next = iterator.next())) {
const localStart = toUTCDate(getInternalDateTimeValue(next));
if (exdateMap[+localStart]) {
continue;
}
count++;
if (count > n) {
break;
}
}
return count > n;
};

export const getOccurrencesBetween = (component, start, end, cache = {}) => {
if (!cache.start) {
const isAllDay = isIcalAllDay(component);
const dtstartType = isAllDay ? 'date' : 'date-time';

// Pretend the (local) date is in UTC time to keep the absolute times.
const dtstart = internalValueToIcalValue(dtstartType, { ...internalDtstart.value, isUTC: true });
// Since the local date is pretended in UTC time, the until has to be converted into a fake local UTC time too
const modifiedRrule = getModifiedUntilRrule(internalRrule, getPropertyTzid(internalDtstart));

const utcStart = propertyToUTCDate(internalDtstart);
const rawEnd = propertyToUTCDate(internalDtEnd);
const modifiedEnd = isAllDay
? addDays(rawEnd, -1) // All day event range is non-inclusive
: rawEnd;
const utcEnd = max(utcStart, modifiedEnd);

const eventDuration = isAllDay
? differenceInCalendarDays(utcEnd, utcStart)
: differenceInMinutes(utcEnd, utcStart) * MILLISECONDS_IN_MINUTE;

cache.start = {
dtstart,
utcStart,
isAllDay,
eventDuration,
modifiedRrule
};
cache.start = getOccurrenceSetup(component);
}

const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule } = cache.start;
const { dtstart: originalDtstart } = component;

const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule, exdateMap } = cache.start;

// If it starts after the current end, ignore it
if (utcStart > end) {
Expand All @@ -117,14 +169,14 @@ export const getOccurrencesBetween = (component, start, end, cache = {}) => {
const interval = [start - YEAR_IN_MS, end + YEAR_IN_MS];

try {
const result = fillOccurrencesBetween(
interval[0],
interval[1],
const result = fillOccurrencesBetween({
interval,
iterator,
eventDuration,
internalDtstart,
isAllDay
);
originalDtstart,
isAllDay,
exdateMap
});

cache.iteration = {
iterator,
Expand All @@ -138,5 +190,5 @@ export const getOccurrencesBetween = (component, start, end, cache = {}) => {
}
}

return cache.iteration.result.filter(([eventStart, eventEnd]) => isInInterval(+eventStart, +eventEnd, start, end));
return cache.iteration.result.filter(({ utcStart, utcEnd }) => isInInterval(+utcStart, +utcEnd, start, end));
};
11 changes: 9 additions & 2 deletions lib/calendar/vcal.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const getProperty = (name, { value, parameters }) => {

const type = specificType || property.type;

if (property.isMultiValue) {
if (property.isMultiValue && Array.isArray(value)) {
property.setValues(value.map((val) => internalValueToIcalValue(type, val, restParameters)));
} else {
property.setValue(internalValueToIcalValue(type, value, restParameters));
Expand Down Expand Up @@ -260,7 +260,14 @@ const fromIcalProperties = (properties = []) => {
acc[name] = [];
}

acc[name].push(propertyAsObject);
// Exdate can be both an array and multivalue, force it to only be an array
if (name === 'exdate') {
const normalizedValues = values.map((value) => ({ ...propertyAsObject, value }));

acc[name] = acc[name].concat(normalizedValues);
} else {
acc[name].push(propertyAsObject);
}

return acc;
}, {});
Expand Down
2 changes: 1 addition & 1 deletion lib/calendar/veventHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fromInternalAttendee } from './attendees';

const { ENCRYPTED_AND_SIGNED, SIGNED, CLEAR } = CALENDAR_CARD_TYPE;

export const SHARED_SIGNED_FIELDS = ['uid', 'dtstamp', 'dtstart', 'dtend', 'rrule', 'transp', 'vtimezone'];
export const SHARED_SIGNED_FIELDS = ['uid', 'dtstamp', 'dtstart', 'dtend', 'rrule', 'exdate', 'transp', 'vtimezone'];
export const SHARED_ENCRYPTED_FIELDS = ['uid', 'dtstamp', 'created', 'description', 'summary', 'location'];

export const CALENDAR_SIGNED_FIELDS = ['uid', 'dtstamp'];
Expand Down
Loading