1 /* eslint-disable no-param-reassign */
2 import { addDays, addMilliseconds, differenceInCalendarDays, max } from '../../date-fns-utc';
3 import { convertUTCDateTimeToZone, convertZonedDateTimeToUTC, fromUTCDate, toUTCDate } from '../../date/timezone';
5 VcalDateOrDateTimeProperty,
6 VcalDateOrDateTimeValue,
9 } from '../../interfaces/calendar/VcalModel';
10 import { createExdateMap } from '../exdate';
11 import { getInternalDateTimeValue, internalValueToIcalValue } from '../vcal';
12 import { getDtendProperty, propertyToUTCDate } from '../vcalConverter';
13 import { getPropertyTzid } from '../vcalHelper';
14 import { getIsAllDay } from '../veventHelper';
16 interface CacheInner {
17 dtstart: VcalDateOrDateTimeValue;
20 eventDuration: number;
21 modifiedRrule: VcalRruleProperty;
22 exdateMap: { [key: number]: boolean };
25 export interface RecurringResult {
30 occurrenceNumber: number;
33 export interface OccurrenceIterationCache {
37 result: RecurringResult[];
42 type RequiredVcalVeventComponent = Pick<VcalVeventComponent, 'dtstart' | 'rrule' | 'exdate'>;
44 const YEAR_IN_MS = Date.UTC(1971, 0, 1);
46 const isInInterval = (a1: number, a2: number, b1: number, b2: number) => a1 <= b2 && a2 >= b1;
48 // Special case for when attempting to use occurrences when an rrule does not exist.
49 // Fake an rrule so that the iteration goes through at least once
50 const DEFAULT_RRULE = {
57 interface FillOccurrencesBetween {
60 eventDuration: number;
61 originalDtstart: VcalDateOrDateTimeProperty;
62 originalDtend: VcalDateOrDateTimeProperty;
64 exdateMap: { [key: number]: boolean };
66 const fillOccurrencesBetween = ({
67 interval: [start, end],
74 }: FillOccurrencesBetween) => {
78 const startTzid = getPropertyTzid(originalDtstart);
79 const endTzid = getPropertyTzid(originalDtend);
81 // eslint-disable-next-line no-cond-assign
82 while ((next = iterator.next())) {
83 const localStart = toUTCDate(getInternalDateTimeValue(next));
84 if (exdateMap[+localStart]) {
93 localEnd = addDays(localStart, eventDuration);
94 utcStart = localStart;
96 } else if (!startTzid || !endTzid) {
97 const endInStartTimezone = addMilliseconds(localStart, eventDuration);
98 localEnd = endInStartTimezone;
99 utcStart = localStart;
100 utcEnd = endInStartTimezone;
102 const endInStartTimezone = addMilliseconds(localStart, eventDuration);
104 const endInUTC = convertZonedDateTimeToUTC(fromUTCDate(endInStartTimezone), startTzid);
105 localEnd = toUTCDate(convertUTCDateTimeToZone(endInUTC, endTzid));
107 utcStart = toUTCDate(convertZonedDateTimeToUTC(fromUTCDate(localStart), startTzid));
108 utcEnd = toUTCDate(endInUTC);
111 if (+utcStart > end) {
115 if (isInInterval(+utcStart, +utcEnd, start, end)) {
121 occurrenceNumber: iterator.occurrence_number as number,
129 * Convert the until property of an rrule to be in the timezone of the start date
131 const getModifiedUntilRrule = (internalRrule: VcalRruleProperty, startTzid: string | undefined): VcalRruleProperty => {
132 if (!internalRrule || !internalRrule.value || !internalRrule.value.until || !startTzid) {
133 return internalRrule;
135 const utcUntil = toUTCDate(internalRrule.value.until);
136 const localUntil = convertUTCDateTimeToZone(fromUTCDate(utcUntil), startTzid);
140 ...internalRrule.value,
149 const getOccurrenceSetup = (component: RequiredVcalVeventComponent) => {
150 const { dtstart: internalDtstart, rrule: internalRrule, exdate: internalExdate } = component;
151 const internalDtEnd = getDtendProperty(component);
153 const isAllDay = getIsAllDay(component);
154 const dtstartType = isAllDay ? 'date' : 'date-time';
156 // Pretend the (local) date is in UTC time to keep the absolute times.
157 const dtstart = internalValueToIcalValue(dtstartType, {
158 ...internalDtstart.value,
160 }) as VcalDateOrDateTimeValue;
161 // Since the local date is pretended in UTC time, the until has to be converted into a fake local UTC time too
162 const safeRrule = getModifiedUntilRrule(internalRrule || DEFAULT_RRULE, getPropertyTzid(internalDtstart));
164 const utcStart = propertyToUTCDate(internalDtstart);
165 let eventDuration: number;
168 const rawEnd = propertyToUTCDate(internalDtEnd);
169 // Non-inclusive end...
170 const modifiedEnd = addDays(rawEnd, -1);
171 const utcEnd = max(utcStart, modifiedEnd);
173 eventDuration = differenceInCalendarDays(utcEnd, utcStart);
175 const utcStart = propertyToUTCDate(internalDtstart);
176 const utcEnd = propertyToUTCDate(internalDtEnd);
178 eventDuration = Math.max(+utcEnd - +utcStart, 0);
186 modifiedRrule: safeRrule,
187 exdateMap: createExdateMap(internalExdate),
191 interface GetOccurrences {
192 component: RequiredVcalVeventComponent;
195 cache?: Partial<OccurrenceIterationCache>;
197 export const getOccurrences = ({
199 maxStart = new Date(9999, 0, 1),
202 }: GetOccurrences): Pick<RecurringResult, 'localStart' | 'localEnd' | 'occurrenceNumber'>[] => {
203 // ICAL.js ignores COUNT=0, so we have to deal with it by hand
204 if (maxCount <= 0 || component?.rrule?.value.count === 0) {
209 cache.start = getOccurrenceSetup(component);
212 const { eventDuration, isAllDay, dtstart, modifiedRrule, exdateMap } = cache.start;
216 const rrule = internalValueToIcalValue('recur', modifiedRrule.value);
217 iterator = rrule.iterator(dtstart);
226 // eslint-disable-next-line no-cond-assign
227 while ((next = iterator.next())) {
228 const localStart = toUTCDate(getInternalDateTimeValue(next));
229 if (exdateMap[+localStart]) {
232 if (result.length >= maxCount || localStart >= maxStart) {
235 const localEnd = isAllDay ? addDays(localStart, eventDuration) : addMilliseconds(localStart, eventDuration);
239 occurrenceNumber: iterator.occurrence_number,
245 export const getOccurrencesBetween = (
246 component: Pick<VcalVeventComponent, 'dtstart' | 'rrule' | 'exdate'>,
249 cache: Partial<OccurrenceIterationCache> = {}
250 ): RecurringResult[] => {
251 // ICAL.js ignores COUNT=0, so we have to deal with it by hand
252 if (component?.rrule?.value.count === 0) {
257 cache.start = getOccurrenceSetup(component);
260 const originalDtstart = component.dtstart;
261 const originalDtend = getDtendProperty(component);
263 const { eventDuration, isAllDay, utcStart, dtstart, modifiedRrule, exdateMap } = cache.start;
265 // If it starts after the current end, ignore it
266 if (+utcStart > end) {
270 if (!cache.iteration || start < cache.iteration.interval[0] || end > cache.iteration.interval[1]) {
272 const rrule = internalValueToIcalValue('recur', modifiedRrule.value);
273 const iterator = rrule.iterator(dtstart);
275 const interval = [start - YEAR_IN_MS, end + YEAR_IN_MS];
276 const result = fillOccurrencesBetween({
298 return cache.iteration.result.filter(({ utcStart, utcEnd }) => isInInterval(+utcStart, +utcEnd, start, end));