Skip to content
This repository was archived by the owner on Aug 11, 2021. It is now read-only.

Commit 997fba4

Browse files
committed
Add exdate support
1 parent c6e1374 commit 997fba4

5 files changed

Lines changed: 165 additions & 57 deletions

File tree

lib/calendar/exdate.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { toUTCDate } from '../date/timezone';
2+
3+
export const createExdateMap = (exdate = []) => {
4+
return exdate.reduce((acc, dateProperty) => {
5+
const localExclude = toUTCDate(dateProperty.value);
6+
acc[+localExclude] = true;
7+
return acc;
8+
}, {});
9+
};

lib/calendar/recurring.js

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getInternalDateTimeValue, internalValueToIcalValue } from './vcal';
44
import { getPropertyTzid, isIcalAllDay, propertyToUTCDate } from './vcalConverter';
55
import { addDays, addMilliseconds, differenceInCalendarDays, max, MILLISECONDS_IN_MINUTE } from '../date-fns-utc';
66
import { convertUTCDateTimeToZone, fromUTCDate, toUTCDate } from '../date/timezone';
7+
import { createExdateMap } from './exdate';
78

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

@@ -13,28 +14,34 @@ export const isIcalRecurring = ({ rrule }) => {
1314

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

16-
const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, isAllDay) => {
17+
const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, isAllDay, exdateMap) => {
1718
const result = [];
1819
let next;
1920

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

24-
const utcStart = propertyToUTCDate({
25-
value: {
26-
...internalDtstart.value,
27-
...fromUTCDate(localStart)
28-
},
29-
parameters: internalDtstart.parameters
30-
});
29+
const utcStart = isAllDay
30+
? localStart
31+
: propertyToUTCDate({
32+
value: {
33+
...internalDtstart.value,
34+
...fromUTCDate(localStart)
35+
},
36+
parameters: internalDtstart.parameters
37+
});
3138

3239
const utcEnd = isAllDay
33-
? addDays(utcStart, eventDuration)
40+
? localEnd
3441
: propertyToUTCDate({
3542
value: {
3643
...internalDtstart.value,
37-
...fromUTCDate(addMilliseconds(localStart, eventDuration))
44+
...fromUTCDate(localEnd)
3845
},
3946
parameters: internalDtstart.parameters
4047
});
@@ -44,7 +51,13 @@ const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDts
4451
}
4552

4653
if (isInInterval(utcStart, utcEnd, start, end)) {
47-
result.push([utcStart, utcEnd, iterator.occurrence_number]);
54+
result.push({
55+
localStart,
56+
localEnd,
57+
utcStart,
58+
utcEnd,
59+
occurrenceNumber: iterator.occurrence_number
60+
});
4861
}
4962
}
5063
return result;
@@ -72,7 +85,7 @@ const getModifiedUntilRrule = (internalRrule, startTzid) => {
7285
};
7386

7487
export const getOccurencesBetween = (component, start, end, cache = {}) => {
75-
const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule } = component;
88+
const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule, exdate } = component;
7689

7790
if (!cache.start) {
7891
const isAllDay = isIcalAllDay(component);
@@ -99,11 +112,12 @@ export const getOccurencesBetween = (component, start, end, cache = {}) => {
99112
utcStart,
100113
isAllDay,
101114
eventDuration,
102-
modifiedRrule
115+
modifiedRrule,
116+
exdateMap: createExdateMap(exdate)
103117
};
104118
}
105119

106-
const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule } = cache.start;
120+
const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule, exdateMap } = cache.start;
107121

108122
// If it starts after the current end, ignore it
109123
if (utcStart > end) {
@@ -123,7 +137,8 @@ export const getOccurencesBetween = (component, start, end, cache = {}) => {
123137
iterator,
124138
eventDuration,
125139
internalDtstart,
126-
isAllDay
140+
isAllDay,
141+
exdateMap
127142
);
128143

129144
cache.iteration = {
@@ -138,5 +153,5 @@ export const getOccurencesBetween = (component, start, end, cache = {}) => {
138153
}
139154
}
140155

141-
return cache.iteration.result.filter(([eventStart, eventEnd]) => isInInterval(+eventStart, +eventEnd, start, end));
156+
return cache.iteration.result.filter(({ utcStart, utcEnd }) => isInInterval(+utcStart, +utcEnd, start, end));
142157
};

lib/calendar/vcal.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ const getProperty = (name, { value, parameters }) => {
151151

152152
const type = specificType || property.type;
153153

154-
if (property.isMultiValue) {
154+
if (property.isMultiValue && Array.isArray(value)) {
155155
property.setValues(value.map((val) => internalValueToIcalValue(type, val, restParameters)));
156156
} else {
157157
property.setValue(internalValueToIcalValue(type, value, restParameters));
@@ -260,7 +260,14 @@ const fromIcalProperties = (properties = []) => {
260260
acc[name] = [];
261261
}
262262

263-
acc[name].push(propertyAsObject);
263+
// Exdate can be both an array and multivalue, force it to only be an array
264+
if (name === 'exdate') {
265+
const normalizedValues = values.map((value) => ({ ...propertyAsObject, value }));
266+
267+
acc[name] = acc[name].concat(normalizedValues);
268+
} else {
269+
acc[name].push(propertyAsObject);
270+
}
264271

265272
return acc;
266273
}, {});

test/calendar/recurring.spec.js

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import { parse } from '../../lib/calendar/vcal';
22
import { getOccurencesBetween } from '../../lib/calendar/recurring';
33

4+
const stringifyResult = (result) => {
5+
return result.map(({ utcStart, utcEnd, occurrenceNumber }) => {
6+
return `${utcStart.toISOString()} - ${utcEnd.toISOString()} | ${occurrenceNumber}`;
7+
});
8+
};
9+
10+
const stringifyResultFull = (result) => {
11+
return result.map(({ localStart, localEnd, utcStart, utcEnd, occurrenceNumber }) => {
12+
return `${localStart.toISOString()} - ${localEnd.toISOString()} | ${utcStart.toISOString()} - ${utcEnd.toISOString()} | ${occurrenceNumber}`;
13+
});
14+
};
15+
16+
const stringifyResultSimple = (result) => {
17+
return result.map(({ utcStart, utcEnd }) => {
18+
return `${utcStart.toISOString()} - ${utcEnd.toISOString()}`;
19+
});
20+
};
21+
422
describe('recurring', () => {
523
const component = {
624
dtstart: {
@@ -32,12 +50,7 @@ describe('recurring', () => {
3250
it('should get initial occurrences between a range', () => {
3351
const result = getOccurencesBetween(component, Date.UTC(2018, 1, 1), Date.UTC(2019, 1, 3));
3452

35-
expect(
36-
result.map(
37-
([start, end, occurrenceNumber]) =>
38-
`${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}`
39-
)
40-
).toEqual([
53+
expect(stringifyResult(result)).toEqual([
4154
'2019-01-30T01:30:00.000Z - 2019-01-30T02:30:00.000Z | 1',
4255
'2019-01-31T01:30:00.000Z - 2019-01-31T02:30:00.000Z | 2',
4356
'2019-02-01T01:30:00.000Z - 2019-02-01T02:30:00.000Z | 3',
@@ -48,12 +61,7 @@ describe('recurring', () => {
4861
it('should get occurrences between a range', () => {
4962
const result = getOccurencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3));
5063

51-
expect(
52-
result.map(
53-
([start, end, occurrenceNumber]) =>
54-
`${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}`
55-
)
56-
).toEqual([
64+
expect(stringifyResult(result)).toEqual([
5765
'2019-03-01T01:30:00.000Z - 2019-03-01T02:30:00.000Z | 31',
5866
'2019-03-02T01:30:00.000Z - 2019-03-02T02:30:00.000Z | 32'
5967
]);
@@ -62,12 +70,10 @@ describe('recurring', () => {
6270
it('should get occurrences between a dst range', () => {
6371
const result = getOccurencesBetween(component, Date.UTC(2019, 9, 26), Date.UTC(2019, 9, 29));
6472

65-
expect(
66-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
67-
).toEqual([
68-
'2019-10-26T00:30:00.000Z - 2019-10-26T01:30:00.000Z',
69-
'2019-10-27T01:30:00.000Z - 2019-10-27T02:30:00.000Z',
70-
'2019-10-28T01:30:00.000Z - 2019-10-28T02:30:00.000Z'
73+
expect(stringifyResultFull(result)).toEqual([
74+
'2019-10-26T02:30:00.000Z - 2019-10-26T03:30:00.000Z | 2019-10-26T00:30:00.000Z - 2019-10-26T01:30:00.000Z | 270',
75+
'2019-10-27T02:30:00.000Z - 2019-10-27T03:30:00.000Z | 2019-10-27T01:30:00.000Z - 2019-10-27T02:30:00.000Z | 271',
76+
'2019-10-28T02:30:00.000Z - 2019-10-28T03:30:00.000Z | 2019-10-28T01:30:00.000Z - 2019-10-28T02:30:00.000Z | 272'
7177
]);
7278
});
7379

@@ -82,12 +88,7 @@ describe('recurring', () => {
8288
const cache = {};
8389
const result1 = getOccurencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3), cache);
8490
const result2 = getOccurencesBetween(component, Date.UTC(2031, 2, 1), Date.UTC(2031, 2, 3), cache);
85-
expect(
86-
result2.map(
87-
([start, end, occurrenceNumber]) =>
88-
`${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}`
89-
)
90-
).toEqual([
91+
expect(stringifyResult(result2)).toEqual([
9192
'2031-03-01T01:30:00.000Z - 2031-03-01T02:30:00.000Z | 4414',
9293
'2031-03-02T01:30:00.000Z - 2031-03-02T02:30:00.000Z | 4415'
9394
]);
@@ -102,9 +103,7 @@ RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;COUNT=3
102103
END:VEVENT`);
103104
const cache = {};
104105
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache);
105-
expect(
106-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
107-
).toEqual([
106+
expect(stringifyResultSimple(result)).toEqual([
108107
'2020-01-29T00:00:00.000Z - 2020-01-29T00:00:00.000Z',
109108
'2020-01-30T00:00:00.000Z - 2020-01-30T00:00:00.000Z',
110109
'2020-01-31T00:00:00.000Z - 2020-01-31T00:00:00.000Z'
@@ -120,9 +119,7 @@ RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;UNTIL=20200131
120119
END:VEVENT`);
121120
const cache = {};
122121
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache);
123-
expect(
124-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
125-
).toEqual([
122+
expect(stringifyResultSimple(result)).toEqual([
126123
'2020-01-29T00:00:00.000Z - 2020-01-29T00:00:00.000Z',
127124
'2020-01-30T00:00:00.000Z - 2020-01-30T00:00:00.000Z',
128125
'2020-01-31T00:00:00.000Z - 2020-01-31T00:00:00.000Z'
@@ -138,9 +135,7 @@ RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;UNTIL=20200131T235959Z
138135
END:VEVENT`);
139136
const cache = {};
140137
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache);
141-
expect(
142-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
143-
).toEqual([
138+
expect(stringifyResultSimple(result)).toEqual([
144139
'2020-01-29T13:00:00.000Z - 2020-01-29T13:30:00.000Z',
145140
'2020-01-30T13:00:00.000Z - 2020-01-30T13:30:00.000Z',
146141
'2020-01-31T13:00:00.000Z - 2020-01-31T13:30:00.000Z'
@@ -156,9 +151,7 @@ RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;UNTIL=20200201T105959Z
156151
END:VEVENT`);
157152
const cache = {};
158153
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache);
159-
expect(
160-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
161-
).toEqual([
154+
expect(stringifyResultSimple(result)).toEqual([
162155
'2020-01-29T11:00:00.000Z - 2020-01-29T12:30:00.000Z',
163156
'2020-01-30T11:00:00.000Z - 2020-01-30T12:30:00.000Z',
164157
'2020-01-31T11:00:00.000Z - 2020-01-31T12:30:00.000Z'
@@ -175,13 +168,71 @@ END:VEVENT
175168
`);
176169
const cache = {};
177170
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 2, 1), cache);
178-
expect(
179-
result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`)
180-
).toEqual([
171+
expect(stringifyResultSimple(result)).toEqual([
181172
'2020-01-26T00:00:00.000Z - 2020-01-26T00:00:00.000Z',
182173
'2020-02-01T00:00:00.000Z - 2020-02-01T00:00:00.000Z',
183174
'2020-02-02T00:00:00.000Z - 2020-02-02T00:00:00.000Z',
184175
'2020-02-08T00:00:00.000Z - 2020-02-08T00:00:00.000Z'
185176
]);
186177
});
178+
179+
fit('should fill occurrences for an event with an exdate', () => {
180+
const component = parse(`
181+
BEGIN:VEVENT
182+
RRULE:FREQ=DAILY;COUNT=6
183+
DTSTART;TZID=Europe/Zurich:20200309T043000
184+
DTEND;TZID=Europe/Zurich:20200309T063000
185+
EXDATE;TZID=Europe/Zurich:20200311T043000
186+
EXDATE;TZID=Europe/Zurich:20200313T043000
187+
END:VEVENT
188+
`);
189+
const cache = {};
190+
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache);
191+
expect(stringifyResultSimple(result)).toEqual([
192+
'2020-03-09T03:30:00.000Z - 2020-03-09T05:30:00.000Z',
193+
'2020-03-10T03:30:00.000Z - 2020-03-10T05:30:00.000Z',
194+
'2020-03-12T03:30:00.000Z - 2020-03-12T05:30:00.000Z',
195+
'2020-03-14T03:30:00.000Z - 2020-03-14T05:30:00.000Z'
196+
]);
197+
});
198+
199+
fit('should fill occurrences for an all day event with an exdate', () => {
200+
const component = parse(`
201+
BEGIN:VEVENT
202+
RRULE:FREQ=DAILY;COUNT=6
203+
DTSTART;VALUE=DATE:20200201
204+
DTEND;VALUE=DATE:20200202
205+
EXDATE;VALUE=DATE:20200201
206+
EXDATE;VALUE=DATE:20200202
207+
EXDATE;VALUE=DATE:20200203
208+
END:VEVENT
209+
`);
210+
const cache = {};
211+
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache);
212+
expect(stringifyResultSimple(result)).toEqual([
213+
'2020-02-04T00:00:00.000Z - 2020-02-04T00:00:00.000Z',
214+
'2020-02-05T00:00:00.000Z - 2020-02-05T00:00:00.000Z',
215+
'2020-02-06T00:00:00.000Z - 2020-02-06T00:00:00.000Z'
216+
]);
217+
});
218+
219+
fit('should fill occurrences for a UTC date with an exdate', () => {
220+
const component = parse(`
221+
BEGIN:VEVENT
222+
RRULE:FREQ=DAILY;COUNT=6
223+
DTSTART:20200201T030000Z
224+
DTEND:20200201T040000Z
225+
EXDATE:20200202T030000Z
226+
EXDATE:20200203T030000Z
227+
EXDATE:20200204T030000Z
228+
END:VEVENT
229+
`);
230+
const cache = {};
231+
const result = getOccurencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache);
232+
expect(stringifyResultSimple(result)).toEqual([
233+
'2020-02-01T03:00:00.000Z - 2020-02-01T04:00:00.000Z',
234+
'2020-02-05T03:00:00.000Z - 2020-02-05T04:00:00.000Z',
235+
'2020-02-06T03:00:00.000Z - 2020-02-06T04:00:00.000Z'
236+
]);
237+
});
187238
});

test/calendar/vcal.spec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,32 @@ describe('calendar', () => {
683683
expect(trimAll(result)).toEqual(trimAll(allDayVevent));
684684
});
685685

686+
it('should normalize exdate', () => {
687+
const veventWithExdate = `BEGIN:VEVENT
688+
RRULE:FREQ=DAILY;COUNT=6
689+
DTSTART;TZID=Europe/Zurich:20200309T043000
690+
DTEND;TZID=Europe/Zurich:20200309T063000
691+
EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
692+
EXDATE;TZID=Europe/Zurich:20200311T043000
693+
EXDATE;TZID=Europe/Zurich:20200313T043000
694+
EXDATE;VALUE=DATE:20200311
695+
END:VEVENT
696+
`;
697+
const normalizedVevent = `BEGIN:VEVENT
698+
RRULE:FREQ=DAILY;COUNT=6
699+
DTSTART;TZID=Europe/Zurich:20200309T043000
700+
DTEND;TZID=Europe/Zurich:20200309T063000
701+
EXDATE:19960402T010000Z
702+
EXDATE:19960403T010000Z
703+
EXDATE:19960404T010000Z
704+
EXDATE;TZID=Europe/Zurich:20200311T043000
705+
EXDATE;TZID=Europe/Zurich:20200313T043000
706+
EXDATE;VALUE=DATE:20200311
707+
END:VEVENT`;
708+
const result = serialize(parse(veventWithExdate));
709+
expect(trimAll(result)).toEqual(trimAll(normalizedVevent));
710+
});
711+
686712
it('should parse trigger string', () => {
687713
expect(fromTriggerString('-PT30M')).toEqual({
688714
weeks: 0,

0 commit comments

Comments
 (0)