From c393888968d40d57abd601a5652c6863d61ca42e Mon Sep 17 00:00:00 2001 From: mmso Date: Tue, 28 Jan 2020 17:56:29 +0100 Subject: [PATCH 1/6] Add occurrence numbering --- lib/calendar/recurring.js | 2 +- test/calendar/recurring.spec.js | 34 +++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/calendar/recurring.js b/lib/calendar/recurring.js index 41d5bd5a..7ae87fb1 100644 --- a/lib/calendar/recurring.js +++ b/lib/calendar/recurring.js @@ -44,7 +44,7 @@ const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDts } if (isInInterval(utcStart, utcEnd, start, end)) { - result.push([utcStart, utcEnd]); + result.push([utcStart, utcEnd, iterator.occurrence_number]); } } return result; diff --git a/test/calendar/recurring.spec.js b/test/calendar/recurring.spec.js index 930d123b..cf680da5 100644 --- a/test/calendar/recurring.spec.js +++ b/test/calendar/recurring.spec.js @@ -29,14 +29,33 @@ describe('recurring', () => { expect(result).toEqual([]); }); + it('should get initial occurrences between a range', () => { + const result = getOccurencesBetween(component, Date.UTC(2018, 1, 1), Date.UTC(2019, 1, 3)); + + expect( + result.map( + ([start, end, occurrenceNumber]) => + `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` + ) + ).toEqual([ + '2019-01-30T01:30:00.000Z - 2019-01-30T02:30:00.000Z | 1', + '2019-01-31T01:30:00.000Z - 2019-01-31T02:30:00.000Z | 2', + '2019-02-01T01:30:00.000Z - 2019-02-01T02:30:00.000Z | 3', + '2019-02-02T01:30:00.000Z - 2019-02-02T02:30:00.000Z | 4' + ]); + }); + it('should get occurrences between a range', () => { const result = getOccurrencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3)); expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) + result.map( + ([start, end, occurrenceNumber]) => + `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` + ) ).toEqual([ - '2019-03-01T01:30:00.000Z - 2019-03-01T02:30:00.000Z', - '2019-03-02T01:30:00.000Z - 2019-03-02T02:30:00.000Z' + '2019-03-01T01:30:00.000Z - 2019-03-01T02:30:00.000Z | 31', + '2019-03-02T01:30:00.000Z - 2019-03-02T02:30:00.000Z | 32' ]); }); @@ -64,10 +83,13 @@ describe('recurring', () => { const result1 = getOccurrencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3), cache); const result2 = getOccurrencesBetween(component, Date.UTC(2031, 2, 1), Date.UTC(2031, 2, 3), cache); expect( - result2.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) + result2.map( + ([start, end, occurrenceNumber]) => + `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` + ) ).toEqual([ - '2031-03-01T01:30:00.000Z - 2031-03-01T02:30:00.000Z', - '2031-03-02T01:30:00.000Z - 2031-03-02T02:30:00.000Z' + '2031-03-01T01:30:00.000Z - 2031-03-01T02:30:00.000Z | 4414', + '2031-03-02T01:30:00.000Z - 2031-03-02T02:30:00.000Z | 4415' ]); }); From efb6224010d75cefe215d6b1fe8a0e279ca0ef63 Mon Sep 17 00:00:00 2001 From: mmso Date: Fri, 7 Feb 2020 15:49:28 +0100 Subject: [PATCH 2/6] Add exdate support --- lib/calendar/exdate.js | 9 ++++ lib/calendar/recurring.js | 47 +++++++++++++-------- lib/calendar/vcal.js | 11 ++++- test/calendar/recurring.spec.js | 73 +++++++++++++++------------------ test/calendar/vcal.spec.js | 26 ++++++++++++ 5 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 lib/calendar/exdate.js diff --git a/lib/calendar/exdate.js b/lib/calendar/exdate.js new file mode 100644 index 00000000..60fa78d9 --- /dev/null +++ b/lib/calendar/exdate.js @@ -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; + }, {}); +}; diff --git a/lib/calendar/recurring.js b/lib/calendar/recurring.js index 7ae87fb1..6a39ca72 100644 --- a/lib/calendar/recurring.js +++ b/lib/calendar/recurring.js @@ -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); @@ -13,28 +14,34 @@ export const isIcalRecurring = ({ rrule }) => { const isInInterval = (a1, a2, b1, b2) => a1 <= b2 && a2 >= b1; -const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, isAllDay) => { +const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, 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: { + ...internalDtstart.value, + ...fromUTCDate(localStart) + }, + parameters: internalDtstart.parameters + }); const utcEnd = isAllDay - ? addDays(utcStart, eventDuration) + ? localEnd : propertyToUTCDate({ value: { ...internalDtstart.value, - ...fromUTCDate(addMilliseconds(localStart, eventDuration)) + ...fromUTCDate(localEnd) }, parameters: internalDtstart.parameters }); @@ -44,7 +51,13 @@ const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDts } if (isInInterval(utcStart, utcEnd, start, end)) { - result.push([utcStart, utcEnd, iterator.occurrence_number]); + result.push({ + localStart, + localEnd, + utcStart, + utcEnd, + occurrenceNumber: iterator.occurrence_number + }); } } return result; @@ -72,7 +85,7 @@ const getModifiedUntilRrule = (internalRrule, startTzid) => { }; export const getOccurrencesBetween = (component, start, end, cache = {}) => { - const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule } = component; + const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule, exdate } = component; if (!cache.start) { const isAllDay = isIcalAllDay(component); @@ -99,11 +112,12 @@ export const getOccurrencesBetween = (component, start, end, cache = {}) => { utcStart, isAllDay, eventDuration, - modifiedRrule + modifiedRrule, + exdateMap: createExdateMap(exdate) }; } - const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule } = cache.start; + const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule, exdateMap } = cache.start; // If it starts after the current end, ignore it if (utcStart > end) { @@ -123,7 +137,8 @@ export const getOccurrencesBetween = (component, start, end, cache = {}) => { iterator, eventDuration, internalDtstart, - isAllDay + isAllDay, + exdateMap ); cache.iteration = { @@ -138,5 +153,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)); }; diff --git a/lib/calendar/vcal.js b/lib/calendar/vcal.js index 7040e199..fbf6d515 100644 --- a/lib/calendar/vcal.js +++ b/lib/calendar/vcal.js @@ -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)); @@ -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; }, {}); diff --git a/test/calendar/recurring.spec.js b/test/calendar/recurring.spec.js index cf680da5..032e5cc5 100644 --- a/test/calendar/recurring.spec.js +++ b/test/calendar/recurring.spec.js @@ -1,6 +1,24 @@ import { parse } from '../../lib/calendar/vcal'; import { getOccurrencesBetween } from '../../lib/calendar/recurring'; +const stringifyResult = (result) => { + return result.map(({ utcStart, utcEnd, occurrenceNumber }) => { + return `${utcStart.toISOString()} - ${utcEnd.toISOString()} | ${occurrenceNumber}`; + }); +}; + +const stringifyResultFull = (result) => { + return result.map(({ localStart, localEnd, utcStart, utcEnd, occurrenceNumber }) => { + return `${localStart.toISOString()} - ${localEnd.toISOString()} | ${utcStart.toISOString()} - ${utcEnd.toISOString()} | ${occurrenceNumber}`; + }); +}; + +const stringifyResultSimple = (result) => { + return result.map(({ utcStart, utcEnd }) => { + return `${utcStart.toISOString()} - ${utcEnd.toISOString()}`; + }); +}; + describe('recurring', () => { const component = { dtstart: { @@ -30,14 +48,9 @@ describe('recurring', () => { }); it('should get initial occurrences between a range', () => { - const result = getOccurencesBetween(component, Date.UTC(2018, 1, 1), Date.UTC(2019, 1, 3)); - - expect( - result.map( - ([start, end, occurrenceNumber]) => - `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` - ) - ).toEqual([ + const result = getOccurrencesBetween(component, Date.UTC(2018, 1, 1), Date.UTC(2019, 1, 3)); + + expect(stringifyResult(result)).toEqual([ '2019-01-30T01:30:00.000Z - 2019-01-30T02:30:00.000Z | 1', '2019-01-31T01:30:00.000Z - 2019-01-31T02:30:00.000Z | 2', '2019-02-01T01:30:00.000Z - 2019-02-01T02:30:00.000Z | 3', @@ -48,12 +61,7 @@ describe('recurring', () => { it('should get occurrences between a range', () => { const result = getOccurrencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3)); - expect( - result.map( - ([start, end, occurrenceNumber]) => - `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` - ) - ).toEqual([ + expect(stringifyResult(result)).toEqual([ '2019-03-01T01:30:00.000Z - 2019-03-01T02:30:00.000Z | 31', '2019-03-02T01:30:00.000Z - 2019-03-02T02:30:00.000Z | 32' ]); @@ -62,12 +70,10 @@ describe('recurring', () => { it('should get occurrences between a dst range', () => { const result = getOccurrencesBetween(component, Date.UTC(2019, 9, 26), Date.UTC(2019, 9, 29)); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ - '2019-10-26T00:30:00.000Z - 2019-10-26T01:30:00.000Z', - '2019-10-27T01:30:00.000Z - 2019-10-27T02:30:00.000Z', - '2019-10-28T01:30:00.000Z - 2019-10-28T02:30:00.000Z' + expect(stringifyResultFull(result)).toEqual([ + '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', + '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', + '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' ]); }); @@ -82,12 +88,7 @@ describe('recurring', () => { const cache = {}; const result1 = getOccurrencesBetween(component, Date.UTC(2019, 2, 1), Date.UTC(2019, 2, 3), cache); const result2 = getOccurrencesBetween(component, Date.UTC(2031, 2, 1), Date.UTC(2031, 2, 3), cache); - expect( - result2.map( - ([start, end, occurrenceNumber]) => - `${new Date(start).toISOString()} - ${new Date(end).toISOString()} | ${occurrenceNumber}` - ) - ).toEqual([ + expect(stringifyResult(result2)).toEqual([ '2031-03-01T01:30:00.000Z - 2031-03-01T02:30:00.000Z | 4414', '2031-03-02T01:30:00.000Z - 2031-03-02T02:30:00.000Z | 4415' ]); @@ -102,9 +103,7 @@ RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA;COUNT=3 END:VEVENT`); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ + expect(stringifyResultSimple(result)).toEqual([ '2020-01-29T00:00:00.000Z - 2020-01-29T00:00:00.000Z', '2020-01-30T00:00:00.000Z - 2020-01-30T00:00:00.000Z', '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 END:VEVENT`); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ + expect(stringifyResultSimple(result)).toEqual([ '2020-01-29T00:00:00.000Z - 2020-01-29T00:00:00.000Z', '2020-01-30T00:00:00.000Z - 2020-01-30T00:00:00.000Z', '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 END:VEVENT`); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ + expect(stringifyResultSimple(result)).toEqual([ '2020-01-29T13:00:00.000Z - 2020-01-29T13:30:00.000Z', '2020-01-30T13:00:00.000Z - 2020-01-30T13:30:00.000Z', '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 END:VEVENT`); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2020, 2, 1), cache); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ + expect(stringifyResultSimple(result)).toEqual([ '2020-01-29T11:00:00.000Z - 2020-01-29T12:30:00.000Z', '2020-01-30T11:00:00.000Z - 2020-01-30T12:30:00.000Z', '2020-01-31T11:00:00.000Z - 2020-01-31T12:30:00.000Z' @@ -175,9 +168,7 @@ END:VEVENT `); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 2, 1), cache); - expect( - result.map(([start, end]) => `${new Date(start).toISOString()} - ${new Date(end).toISOString()}`) - ).toEqual([ + expect(stringifyResultSimple(result)).toEqual([ '2020-01-26T00:00:00.000Z - 2020-01-26T00:00:00.000Z', '2020-02-01T00:00:00.000Z - 2020-02-01T00:00:00.000Z', '2020-02-02T00:00:00.000Z - 2020-02-02T00:00:00.000Z', diff --git a/test/calendar/vcal.spec.js b/test/calendar/vcal.spec.js index 81b7a6f2..320dfb27 100644 --- a/test/calendar/vcal.spec.js +++ b/test/calendar/vcal.spec.js @@ -683,6 +683,32 @@ describe('calendar', () => { expect(trimAll(result)).toEqual(trimAll(allDayVevent)); }); + it('should normalize exdate', () => { + const veventWithExdate = `BEGIN:VEVENT +RRULE:FREQ=DAILY;COUNT=6 +DTSTART;TZID=Europe/Zurich:20200309T043000 +DTEND;TZID=Europe/Zurich:20200309T063000 +EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z +EXDATE;TZID=Europe/Zurich:20200311T043000 +EXDATE;TZID=Europe/Zurich:20200313T043000 +EXDATE;VALUE=DATE:20200311 +END:VEVENT +`; + const normalizedVevent = `BEGIN:VEVENT +RRULE:FREQ=DAILY;COUNT=6 +DTSTART;TZID=Europe/Zurich:20200309T043000 +DTEND;TZID=Europe/Zurich:20200309T063000 +EXDATE:19960402T010000Z +EXDATE:19960403T010000Z +EXDATE:19960404T010000Z +EXDATE;TZID=Europe/Zurich:20200311T043000 +EXDATE;TZID=Europe/Zurich:20200313T043000 +EXDATE;VALUE=DATE:20200311 +END:VEVENT`; + const result = serialize(parse(veventWithExdate)); + expect(trimAll(result)).toEqual(trimAll(normalizedVevent)); + }); + it('should parse trigger string', () => { expect(fromTriggerString('-PT30M')).toEqual({ weeks: 0, From a9fd55da981f8bfecc5195ff4c9b6349bc079afa Mon Sep 17 00:00:00 2001 From: mmso Date: Fri, 7 Feb 2020 19:07:10 +0100 Subject: [PATCH 3/6] Add exdate fields --- lib/calendar/veventHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/calendar/veventHelper.js b/lib/calendar/veventHelper.js index 0e7c624a..77e4e572 100644 --- a/lib/calendar/veventHelper.js +++ b/lib/calendar/veventHelper.js @@ -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']; From 1d40c12de11b026ff663fb6b7ae30f7e7e91a1b1 Mon Sep 17 00:00:00 2001 From: mmso Date: Sat, 8 Feb 2020 14:19:54 +0100 Subject: [PATCH 4/6] Add is more than one occurrence --- lib/calendar/recurring.js | 115 +++++++++++++++++++++----------- test/calendar/recurring.spec.js | 60 +++++++++++++++++ 2 files changed, 136 insertions(+), 39 deletions(-) diff --git a/lib/calendar/recurring.js b/lib/calendar/recurring.js index 6a39ca72..6bb1903b 100644 --- a/lib/calendar/recurring.js +++ b/lib/calendar/recurring.js @@ -14,7 +14,14 @@ export const isIcalRecurring = ({ rrule }) => { const isInInterval = (a1, a2, b1, b2) => a1 <= b2 && a2 >= b1; -const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDtstart, isAllDay, exdateMap) => { +const fillOccurrencesBetween = ({ + interval: [start, end], + iterator, + eventDuration, + originalDtstart, + isAllDay, + exdateMap +}) => { const result = []; let next; @@ -30,20 +37,20 @@ const fillOccurrencesBetween = (start, end, iterator, eventDuration, internalDts ? localStart : propertyToUTCDate({ value: { - ...internalDtstart.value, + ...originalDtstart.value, ...fromUTCDate(localStart) }, - parameters: internalDtstart.parameters + parameters: originalDtstart.parameters }); const utcEnd = isAllDay ? localEnd : propertyToUTCDate({ value: { - ...internalDtstart.value, + ...originalDtstart.value, ...fromUTCDate(localEnd) }, - parameters: internalDtstart.parameters + parameters: originalDtstart.parameters }); if (utcStart > end) { @@ -84,39 +91,70 @@ const getModifiedUntilRrule = (internalRrule, startTzid) => { }; }; -export const getOccurrencesBetween = (component, start, end, cache = {}) => { - const { dtstart: internalDtstart, dtend: internalDtEnd, rrule: internalRrule, exdate } = 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, - exdateMap: createExdateMap(exdate) - }; + cache.start = getOccurrenceSetup(component); } + const { dtstart: originalDtstart } = component; + const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule, exdateMap } = cache.start; // If it starts after the current end, ignore it @@ -131,15 +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, + originalDtstart, isAllDay, exdateMap - ); + }); cache.iteration = { iterator, diff --git a/test/calendar/recurring.spec.js b/test/calendar/recurring.spec.js index 032e5cc5..6939ed9e 100644 --- a/test/calendar/recurring.spec.js +++ b/test/calendar/recurring.spec.js @@ -175,4 +175,64 @@ END:VEVENT '2020-02-08T00:00:00.000Z - 2020-02-08T00:00:00.000Z' ]); }); + + fit('should fill occurrences for an event with an exdate', () => { + const component = parse(` +BEGIN:VEVENT +RRULE:FREQ=DAILY;COUNT=6 +DTSTART;TZID=Europe/Zurich:20200309T043000 +DTEND;TZID=Europe/Zurich:20200309T063000 +EXDATE;TZID=Europe/Zurich:20200311T043000 +EXDATE;TZID=Europe/Zurich:20200313T043000 +END:VEVENT +`); + const cache = {}; + const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache); + expect(stringifyResultSimple(result)).toEqual([ + '2020-03-09T03:30:00.000Z - 2020-03-09T05:30:00.000Z', + '2020-03-10T03:30:00.000Z - 2020-03-10T05:30:00.000Z', + '2020-03-12T03:30:00.000Z - 2020-03-12T05:30:00.000Z', + '2020-03-14T03:30:00.000Z - 2020-03-14T05:30:00.000Z' + ]); + }); + + fit('should fill occurrences for an all day event with an exdate', () => { + const component = parse(` +BEGIN:VEVENT +RRULE:FREQ=DAILY;COUNT=6 +DTSTART;VALUE=DATE:20200201 +DTEND;VALUE=DATE:20200202 +EXDATE;VALUE=DATE:20200201 +EXDATE;VALUE=DATE:20200202 +EXDATE;VALUE=DATE:20200203 +END:VEVENT +`); + const cache = {}; + const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache); + expect(stringifyResultSimple(result)).toEqual([ + '2020-02-04T00:00:00.000Z - 2020-02-04T00:00:00.000Z', + '2020-02-05T00:00:00.000Z - 2020-02-05T00:00:00.000Z', + '2020-02-06T00:00:00.000Z - 2020-02-06T00:00:00.000Z' + ]); + }); + + fit('should fill occurrences for a UTC date with an exdate', () => { + const component = parse(` +BEGIN:VEVENT +RRULE:FREQ=DAILY;COUNT=6 +DTSTART:20200201T030000Z +DTEND:20200201T040000Z +EXDATE:20200202T030000Z +EXDATE:20200203T030000Z +EXDATE:20200204T030000Z +END:VEVENT +`); + const cache = {}; + const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache); + expect(stringifyResultSimple(result)).toEqual([ + '2020-02-01T03:00:00.000Z - 2020-02-01T04:00:00.000Z', + '2020-02-05T03:00:00.000Z - 2020-02-05T04:00:00.000Z', + '2020-02-06T03:00:00.000Z - 2020-02-06T04:00:00.000Z' + ]); + }); }); From d7a4c5ce1d5d25cdecd4001095c0bed8ce99f1a5 Mon Sep 17 00:00:00 2001 From: mmso Date: Fri, 14 Feb 2020 15:54:47 +0100 Subject: [PATCH 5/6] Remove fit --- test/calendar/recurring.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/calendar/recurring.spec.js b/test/calendar/recurring.spec.js index 6939ed9e..63d0d457 100644 --- a/test/calendar/recurring.spec.js +++ b/test/calendar/recurring.spec.js @@ -176,7 +176,7 @@ END:VEVENT ]); }); - fit('should fill occurrences for an event with an exdate', () => { + it('should fill occurrences for an event with an exdate', () => { const component = parse(` BEGIN:VEVENT RRULE:FREQ=DAILY;COUNT=6 @@ -196,7 +196,7 @@ END:VEVENT ]); }); - fit('should fill occurrences for an all day event with an exdate', () => { + it('should fill occurrences for an all day event with an exdate', () => { const component = parse(` BEGIN:VEVENT RRULE:FREQ=DAILY;COUNT=6 @@ -216,7 +216,7 @@ END:VEVENT ]); }); - fit('should fill occurrences for a UTC date with an exdate', () => { + it('should fill occurrences for a UTC date with an exdate', () => { const component = parse(` BEGIN:VEVENT RRULE:FREQ=DAILY;COUNT=6 From 3926f9d369d1716d07f460ca064f17faa9df017a Mon Sep 17 00:00:00 2001 From: mmso Date: Fri, 21 Feb 2020 12:39:49 +0100 Subject: [PATCH 6/6] Add occurrence numbering --- test/calendar/recurring.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/calendar/recurring.spec.js b/test/calendar/recurring.spec.js index 63d0d457..55fc76c8 100644 --- a/test/calendar/recurring.spec.js +++ b/test/calendar/recurring.spec.js @@ -188,11 +188,11 @@ END:VEVENT `); const cache = {}; const result = getOccurrencesBetween(component, Date.UTC(2020, 0, 1), Date.UTC(2021, 4, 1), cache); - expect(stringifyResultSimple(result)).toEqual([ - '2020-03-09T03:30:00.000Z - 2020-03-09T05:30:00.000Z', - '2020-03-10T03:30:00.000Z - 2020-03-10T05:30:00.000Z', - '2020-03-12T03:30:00.000Z - 2020-03-12T05:30:00.000Z', - '2020-03-14T03:30:00.000Z - 2020-03-14T05:30:00.000Z' + expect(stringifyResult(result)).toEqual([ + '2020-03-09T03:30:00.000Z - 2020-03-09T05:30:00.000Z | 1', + '2020-03-10T03:30:00.000Z - 2020-03-10T05:30:00.000Z | 2', + '2020-03-12T03:30:00.000Z - 2020-03-12T05:30:00.000Z | 4', + '2020-03-14T03:30:00.000Z - 2020-03-14T05:30:00.000Z | 6' ]); });