From d910191fbdb2d4bef4b3f18fecd31bfe7b4a3d1c Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Mon, 1 Sep 2025 23:14:10 +0500 Subject: [PATCH] Fix calendar rec events (#9755) Signed-off-by: Denis Bykhov --- plugins/calendar/src/__tests__/utils.test.ts | 36 ++++++++ plugins/calendar/src/utils.ts | 87 ++++++++++++++++---- 2 files changed, 105 insertions(+), 18 deletions(-) diff --git a/plugins/calendar/src/__tests__/utils.test.ts b/plugins/calendar/src/__tests__/utils.test.ts index 82fa51fc43..2cbece116b 100644 --- a/plugins/calendar/src/__tests__/utils.test.ts +++ b/plugins/calendar/src/__tests__/utils.test.ts @@ -161,4 +161,40 @@ describe('generateRecurringValues', () => { } expect(() => generateRecurringValues(rule as any, baseDate, from, to)).toThrow('Invalid recurring rule frequency') }) + + it('generates simple monthly recurring values (no specific rules)', () => { + const rule: RecurringRule = { + freq: 'MONTHLY', + interval: 1 + } + const startDate = new Date('2024-01-15T10:00:00Z').getTime() // 15th of month + const from = new Date('2024-01-01T00:00:00Z').getTime() + const to = new Date('2024-04-30T23:59:59Z').getTime() + const result = generateRecurringValues(rule, startDate, from, to) + + // Should generate events on 15th of each month: Jan, Feb, Mar, Apr + expect(result.length).toBe(4) + expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-01-15') + expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2024-02-15') + expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2024-03-15') + expect(new Date(result[3]).toISOString().slice(0, 10)).toBe('2024-04-15') + }) + + it('generates simple yearly recurring values (no specific rules)', () => { + const rule: RecurringRule = { + freq: 'YEARLY', + interval: 1 + } + const startDate = new Date('2024-03-10T10:00:00Z').getTime() // March 10th + const from = new Date('2024-01-01T00:00:00Z').getTime() + const to = new Date('2027-12-31T23:59:59Z').getTime() + const result = generateRecurringValues(rule, startDate, from, to) + + // Should generate events on March 10th of each year: 2024, 2025, 2026, 2027 + expect(result.length).toBe(4) + expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-03-10') + expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2025-03-10') + expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2026-03-10') + expect(new Date(result[3]).toISOString().slice(0, 10)).toBe('2027-03-10') + }) }) diff --git a/plugins/calendar/src/utils.ts b/plugins/calendar/src/utils.ts index 24027b7330..8ae5176b6f 100644 --- a/plugins/calendar/src/utils.ts +++ b/plugins/calendar/src/utils.ts @@ -154,11 +154,37 @@ function generateMonthlyValues (rule: RecurringRule, currentDate: Date, from: Ti const end = new Date(new Date(currentDate).setMonth(currentDate.getMonth() + 1, 1)) let date = currentDate const candidates: Date[] = [] - while (date < end) { - if ((byDay == null || matchesByDay(date, byDay)) && (byMonthDay == null || byMonthDay.includes(date.getDate()))) { - candidates.push(new Date(date)) + + // If no specific rules are set except byMonthDay (simple monthly recurrence) + if (byDay == null && bySetPos == null && byMonthDay != null) { + for (const day of byMonthDay) { + const originalDate = new Date(currentDate.getTime()) + const sameDate = new Date( + Date.UTC( + currentDate.getUTCFullYear(), + currentDate.getUTCMonth(), + day, + originalDate.getUTCHours(), + originalDate.getUTCMinutes(), + originalDate.getUTCSeconds(), + originalDate.getUTCMilliseconds() + ) + ) + if (sameDate.getUTCMonth() === currentDate.getUTCMonth()) { + // Valid day for this month + candidates.push(sameDate) + } + } + } else { + while (date < end) { + if ( + (byDay == null || matchesByDay(date, byDay)) && + (byMonthDay == null || byMonthDay.includes(date.getDate())) + ) { + candidates.push(new Date(date)) + } + date = new Date(date.setDate(date.getDate() + 1)) } - date = new Date(date.setDate(date.getDate() + 1)) } let filtered: Date[] = candidates @@ -197,21 +223,46 @@ function generateYearlyValues (rule: RecurringRule, currentDate: Date, from: Tim const end = new Date(new Date(currentDate).setFullYear(currentDate.getFullYear() + 1, 0, 1)) let date = currentDate const candidates: Date[] = [] - while (date < end) { - if ( - (byDay == null || matchesByDay(date, byDay)) && - (byMonthDay == null || byMonthDay.includes(date.getDate())) && - (byYearDay == null || byYearDay.includes(getYearDay(date))) && - (byWeekNo == null || byWeekNo.includes(getWeekNumber(date))) && - (byMonth == null || byMonth.includes(date.getMonth())) - ) { - const res = date.getTime() - if (res >= from && res <= to) { - candidates.push(new Date(res)) + + // If no specific rules are set, only generate an event for the same month/day each year + if ( + byDay == null && + byMonthDay == null && + byYearDay == null && + byWeekNo == null && + byMonth == null && + bySetPos == null + ) { + const originalDate = new Date(currentDate.getTime()) + const sameDate = new Date( + Date.UTC( + currentDate.getUTCFullYear(), + originalDate.getUTCMonth(), + originalDate.getUTCDate(), + originalDate.getUTCHours(), + originalDate.getUTCMinutes(), + originalDate.getUTCSeconds(), + originalDate.getUTCMilliseconds() + ) + ) + candidates.push(sameDate) + } else { + while (date < end) { + if ( + (byDay == null || matchesByDay(date, byDay)) && + (byMonthDay == null || byMonthDay.includes(date.getDate())) && + (byYearDay == null || byYearDay.includes(getYearDay(date))) && + (byWeekNo == null || byWeekNo.includes(getWeekNumber(date))) && + (byMonth == null || byMonth.includes(date.getMonth())) + ) { + const res = date.getTime() + if (res >= from && res <= to) { + candidates.push(new Date(res)) + } + i++ } - i++ + date = new Date(date.setDate(date.getDate() + 1)) } - date = new Date(date.setDate(date.getDate() + 1)) } let filtered: Date[] = candidates @@ -235,7 +286,7 @@ function generateYearlyValues (rule: RecurringRule, currentDate: Date, from: Tim } if (endDate != null && next > endDate) return values if (next >= to) return values - currentDate = new Date(next) + currentDate = new Date(currentDate.setFullYear(currentDate.getFullYear() + (interval ?? 1))) } }