Skip to content

Commit

Permalink
Merge pull request #2902 from digitalfabrik/2901-Fix-recurring-dates-…
Browse files Browse the repository at this point in the history
…yet-again

2901: Fix recurring dates yet again
  • Loading branch information
LeandraH authored Sep 3, 2024
2 parents 7775c6f + b2b7a35 commit e5bafc8
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 15 deletions.
19 changes: 10 additions & 9 deletions shared/api/models/DateModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DateTime, Duration } from 'luxon'
import { RRule as RRuleType } from 'rrule'
import { RRule as RRuleType, rrulestr } from 'rrule'

import { formatDateICal } from '../../utils'

const MAX_RECURRENCE_YEARS = 5

Expand Down Expand Up @@ -137,14 +139,13 @@ class DateModel {

private getRecurrenceRuleInLocalTime(recurrenceRule: RRuleType): RRuleType {
const startDate = recurrenceRule.options.dtstart
const offsetStartDate = DateTime.fromJSDate(startDate).minus({ minutes: startDate.getTimezoneOffset() }).toJSDate()
return new RRuleType({
freq: recurrenceRule.options.freq,
byweekday: recurrenceRule.options.byweekday,
interval: recurrenceRule.options.interval,
until: recurrenceRule.options.until,
dtstart: offsetStartDate,
})
const offsetStartDate = formatDateICal(
DateTime.fromJSDate(startDate).minus({ minutes: startDate.getTimezoneOffset() }).toUTC(),
)
const regexForFindingDate = /\d{8}T\d{6}/
// Don't parse by the recurrenceRule options here, rrule doesn't properly parse the params for every nth day of the month
// https://github.com/jkbrzt/rrule/issues/326
return rrulestr(recurrenceRule.toString().replace(regexForFindingDate, offsetStartDate))
}

isEqual(other: DateModel): boolean {
Expand Down
4 changes: 1 addition & 3 deletions shared/api/models/EventModel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { decodeHTML } from 'entities'
import { DateTime } from 'luxon'
import { RRule } from 'rrule'
import { v5 } from 'uuid'

import { formatDateICal } from '../../utils'
Expand Down Expand Up @@ -72,8 +71,7 @@ class EventModel extends ExtendedPageModel {
}

if (recurring && date.recurrenceRule) {
const { freq, interval, until, byweekday } = date.recurrenceRule.options
const recurrence = RRule.optionsToString({ freq, interval, until, byweekday })
const recurrence = date.recurrenceRule.toString()
if (recurrence) {
body.push(recurrence)
}
Expand Down
169 changes: 169 additions & 0 deletions shared/api/models/__tests__/DateModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,5 +400,174 @@ describe('DateModel', () => {
}),
])
})

it('should correctly handle events recurring every second week of the month', () => {
jest.useFakeTimers({ now: new Date('2024-07-28T15:23:57.443+02:00') })
const recurrenceRule = rrulestr('DTSTART:20240620T083000\nRRULE:FREQ=MONTHLY;BYDAY=+2MO')
const date = new DateModel({
startDate: DateTime.fromISO('2024-06-20T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-06-20T12:00:00.000+02:00'),
allDay: false,
recurrenceRule,
})

expect(date.recurrences(3)).toEqual([
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2024-08-12T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-08-12T12:00:00.000+02:00'),
offset: 120,
}),
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2024-09-09T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-09-09T12:00:00.000+02:00'),
offset: 120,
}),
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2024-10-14T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-10-14T12:00:00.000+02:00'),
offset: 120,
}),
])
})

it('should correctly handle events recurring every last week of the month', () => {
jest.useFakeTimers({ now: new Date('2024-08-28T15:23:57.443+02:00') })
const recurrenceRule = rrulestr('DTSTART:20240827T220000\nRRULE:FREQ=MONTHLY;BYDAY=-1WE')
const date = new DateModel({
startDate: DateTime.fromISO('2024-08-28T00:00:00.000+02:00'),
endDate: DateTime.fromISO('2024-08-28T23:59:00.000+02:00'),
allDay: true,
recurrenceRule,
})

expect(date.recurrences(3)).toEqual([
new DateModel({
allDay: true,
recurrenceRule,
startDate: DateTime.fromISO('2024-08-28T00:00:00.000+02:00'),
endDate: DateTime.fromISO('2024-08-28T23:59:00.000+02:00'),
offset: 120,
}),
new DateModel({
allDay: true,
recurrenceRule,
startDate: DateTime.fromISO('2024-09-25T00:00:00.000+02:00'),
endDate: DateTime.fromISO('2024-09-25T23:59:00.000+02:00'),
offset: 120,
}),
new DateModel({
allDay: true,
recurrenceRule,
startDate: DateTime.fromISO('2024-10-30T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2024-10-30T23:59:00.000+01:00'),
offset: 120,
}),
])
})

it('should correctly handle events recurring every third week of every second month', () => {
jest.useFakeTimers({ now: new Date('2024-08-28T15:23:57.443+02:00') })
const recurrenceRule = rrulestr('DTSTART:20240620T083000\nRRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=+3TH')
const date = new DateModel({
startDate: DateTime.fromISO('2024-06-20T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-06-20T12:00:00.000+02:00'),
allDay: false,
recurrenceRule,
})

expect(date.recurrences(3)).toEqual([
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2024-10-17T10:30:00.000+02:00'),
endDate: DateTime.fromISO('2024-10-17T12:00:00.000+02:00'),
offset: 120,
}),
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2024-12-19T10:30:00.000+01:00'),
endDate: DateTime.fromISO('2024-12-19T12:00:00.000+01:00'),
offset: 120,
}),
new DateModel({
allDay: false,
recurrenceRule,
startDate: DateTime.fromISO('2025-02-20T10:30:00.000+01:00'),
endDate: DateTime.fromISO('2025-02-20T12:00:00.000+01:00'),
offset: 120,
}),
])
})

it('should correctly handle events repeating annually', () => {
jest.useFakeTimers({ now: new Date('2024-08-28T15:23:57.443+02:00') })
const recurrenceRule = rrulestr('DTSTART:20241205T230000\nRRULE:FREQ=YEARLY')
const date = new DateModel({
startDate: DateTime.fromISO('2024-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2024-12-06T23:59:00.000+01:00'),
allDay: true,
recurrenceRule,
})

expect(date.recurrences(3)).toEqual([
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2024-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2024-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2025-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2025-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2026-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2026-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
])
})

it('should correctly handle events repeating every 2 years', () => {
jest.useFakeTimers({ now: new Date('2024-08-28T15:23:57.443+02:00') })
const recurrenceRule = rrulestr('DTSTART:20241205T230000\nRRULE:FREQ=YEARLY;INTERVAL=2')
const date = new DateModel({
startDate: DateTime.fromISO('2024-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2024-12-06T23:59:00.000+01:00'),
allDay: true,
recurrenceRule,
})

expect(date.recurrences(3)).toEqual([
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2024-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2024-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2026-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2026-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
new DateModel({
allDay: true,
startDate: DateTime.fromISO('2028-12-06T00:00:00.000+01:00'),
endDate: DateTime.fromISO('2028-12-06T23:59:00.000+01:00'),
recurrenceRule,
}),
])
})
})
})
6 changes: 3 additions & 3 deletions shared/api/models/__tests__/EventModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('EventModel', () => {
startDate: DateTime.fromISO('2020-03-20T10:50:00+02:00'),
endDate: DateTime.fromISO('2020-03-20T17:50:00+02:00'),
allDay: false,
recurrenceRule: rrulestr('FREQ=WEEKLY;INTERVAL=3;UNTIL=20200703T235959Z;BYDAY=FR'),
recurrenceRule: rrulestr('FREQ=WEEKLY;INTERVAL=3;UNTIL=20200703T235959Z;BYDAY=-1FR'),
}),
location: new LocationModel({
id: 1,
Expand Down Expand Up @@ -69,9 +69,9 @@ describe('EventModel', () => {
expect(endDate).toBe(`DTEND;TZID=${timezone}:20200320T165000`)
})

it('should have a recurrence rule in iCal', () => {
it('should have the correct recurrence rule in iCal', () => {
const recurrenceField = getICalField(event, 'RRULE', true)
expect(recurrenceField).toBe('RRULE:FREQ=WEEKLY;INTERVAL=3;UNTIL=20200703T235959Z;BYDAY=FR')
expect(recurrenceField).toBe('RRULE:FREQ=WEEKLY;INTERVAL=3;UNTIL=20200703T235959Z;BYDAY=-1FR')
})

it('should correctly strip carriage returns and escape new lines in ical description', () => {
Expand Down

0 comments on commit e5bafc8

Please sign in to comment.