diff --git a/plugins/calendar-assets/lang/cs.json b/plugins/calendar-assets/lang/cs.json
index db09ffe034..891adaddea 100644
--- a/plugins/calendar-assets/lang/cs.json
+++ b/plugins/calendar-assets/lang/cs.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Čas schůzky změněn",
"MeetingCanceledNotification": "Schůzka zrušena: {title}",
"PrimaryCalendar": "Hlavní kalendář",
- "Hidden": "Skrytý"
+ "Hidden": "Skrytý",
+ "Each": "Každý",
+ "OnThe": "V",
+ "First": "První",
+ "Second": "Druhý",
+ "Third": "Třetí",
+ "Fourth": "Čtvrtý",
+ "Fifth": "Pátý",
+ "NextToLast": "Předposlední",
+ "Last": "Poslední",
+ "Weekday": "Všední den",
+ "WeekendDay": "Víkendový den"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/de.json b/plugins/calendar-assets/lang/de.json
index 31799cf0ef..2d9894b341 100644
--- a/plugins/calendar-assets/lang/de.json
+++ b/plugins/calendar-assets/lang/de.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Besprechungszeit geändert",
"MeetingCanceledNotification": "Besprechung abgesagt: {title}",
"PrimaryCalendar": "Primärer Kalender",
- "Hidden": "Versteckt"
+ "Hidden": "Versteckt",
+ "Each": "Jeder",
+ "OnThe": "Am",
+ "First": "Erster",
+ "Second": "Zweiter",
+ "Third": "Dritter",
+ "Fourth": "Vierter",
+ "Fifth": "Fünfter",
+ "NextToLast": "Vorletzter",
+ "Last": "Letzter",
+ "Weekday": "Wochentag",
+ "WeekendDay": "Wochenendtag"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/en.json b/plugins/calendar-assets/lang/en.json
index 3160a207e6..a68208f6e0 100644
--- a/plugins/calendar-assets/lang/en.json
+++ b/plugins/calendar-assets/lang/en.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Meeting time changed",
"MeetingCanceledNotification": "Meeting canceled: {title}",
"PrimaryCalendar": "Primary calendar",
- "Hidden": "Hidden"
+ "Hidden": "Hidden",
+ "Each": "Each",
+ "OnThe": "On the",
+ "First": "First",
+ "Second": "Second",
+ "Third": "Third",
+ "Fourth": "Fourth",
+ "Fifth": "Fifth",
+ "NextToLast": "Next to last",
+ "Last": "Last",
+ "Weekday": "Weekday",
+ "WeekendDay": "Weekend day"
}
}
diff --git a/plugins/calendar-assets/lang/es.json b/plugins/calendar-assets/lang/es.json
index be022d7780..1e844973a4 100644
--- a/plugins/calendar-assets/lang/es.json
+++ b/plugins/calendar-assets/lang/es.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Se modificó la hora de la reunión",
"MeetingCanceledNotification": "Reunión cancelada: {title}",
"PrimaryCalendar": "Calendario principal",
- "Hidden": "Oculto"
+ "Hidden": "Oculto",
+ "Each": "Cada",
+ "OnThe": "En el",
+ "First": "Primero",
+ "Second": "Segundo",
+ "Third": "Tercero",
+ "Fourth": "Cuarto",
+ "Fifth": "Quinto",
+ "NextToLast": "Penúltimo",
+ "Last": "Último",
+ "Weekday": "Día laborable",
+ "WeekendDay": "Día de descanso"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/fr.json b/plugins/calendar-assets/lang/fr.json
index 146b6604bb..d60fcf5325 100644
--- a/plugins/calendar-assets/lang/fr.json
+++ b/plugins/calendar-assets/lang/fr.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Heure de la réunion modifiée",
"MeetingCanceledNotification": "Réunion annulée: {title}",
"PrimaryCalendar": "Calendrier principal",
- "Hidden": "Caché"
+ "Hidden": "Caché",
+ "Each": "Chaque",
+ "OnThe": "Le",
+ "First": "Premier",
+ "Second": "Deuxième",
+ "Third": "Troisième",
+ "Fourth": "Quatrième",
+ "Fifth": "Cinquième",
+ "NextToLast": "Avant-dernier",
+ "Last": "Dernier",
+ "Weekday": "Jour de la semaine",
+ "WeekendDay": "Jour de congé"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/it.json b/plugins/calendar-assets/lang/it.json
index b46620e2c4..455d26c5e2 100644
--- a/plugins/calendar-assets/lang/it.json
+++ b/plugins/calendar-assets/lang/it.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Orario della riunione modificato",
"MeetingCanceledNotification": "Riunione annullata: {title}",
"PrimaryCalendar": "Calendario principale",
- "Hidden": "Nascosto"
+ "Hidden": "Nascosto",
+ "Each": "Ogni",
+ "OnThe": "Il",
+ "First": "Primo",
+ "Second": "Secondo",
+ "Third": "Terzo",
+ "Fourth": "Quarto",
+ "Fifth": "Quinto",
+ "NextToLast": "Penultimo",
+ "Last": "Ultimo",
+ "Weekday": "Giorno lavorativo",
+ "WeekendDay": "Giorno libero"
}
}
diff --git a/plugins/calendar-assets/lang/ja.json b/plugins/calendar-assets/lang/ja.json
index 351987e607..87922765be 100644
--- a/plugins/calendar-assets/lang/ja.json
+++ b/plugins/calendar-assets/lang/ja.json
@@ -106,6 +106,17 @@
"CalDavAccessAccount": "Huly アカウント",
"CalDavAccessPassword": "パスワード(自動生成)",
"PrimaryCalendar": "プライマリカレンダー",
- "Hidden": "非表示"
+ "Hidden": "非表示",
+ "Each": "各",
+ "OnThe": "の",
+ "First": "最初の",
+ "Second": "2番目の",
+ "Third": "3番目の",
+ "Fourth": "4番目の",
+ "Fifth": "5番目の",
+ "NextToLast": "最後から2番目",
+ "Last": "最後",
+ "Weekday": "平日",
+ "WeekendDay": "休日"
}
}
diff --git a/plugins/calendar-assets/lang/pt.json b/plugins/calendar-assets/lang/pt.json
index 4147ff1c51..a839833e12 100644
--- a/plugins/calendar-assets/lang/pt.json
+++ b/plugins/calendar-assets/lang/pt.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Horário da reunião alterado",
"MeetingCanceledNotification": "Reunião cancelada: {title}",
"PrimaryCalendar": "Calendário principal",
- "Hidden": "Oculto"
+ "Hidden": "Oculto",
+ "Each": "Cada",
+ "OnThe": "No dia",
+ "First": "Primeiro",
+ "Second": "Segundo",
+ "Third": "Terceiro",
+ "Fourth": "Quarto",
+ "Fifth": "Quinto",
+ "NextToLast": "Penúltimo",
+ "Last": "Último",
+ "Weekday": "Dia da semana",
+ "WeekendDay": "Folga"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/ru.json b/plugins/calendar-assets/lang/ru.json
index 198c233dee..ed55140673 100644
--- a/plugins/calendar-assets/lang/ru.json
+++ b/plugins/calendar-assets/lang/ru.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "Время встречи изменено",
"MeetingCanceledNotification": "Встреча отменена: {title}",
"PrimaryCalendar": "Основной календарь",
- "Hidden": "Скрыт"
+ "Hidden": "Скрыт",
+ "Each": "Каждый",
+ "OnThe": "В",
+ "First": "Первый",
+ "Second": "Второй",
+ "Third": "Третий",
+ "Fourth": "Четвертый",
+ "Fifth": "Пятый",
+ "NextToLast": "Предпоследний",
+ "Last": "Последний",
+ "Weekday": "Будний день",
+ "WeekendDay": "Выходной день"
}
}
diff --git a/plugins/calendar-assets/lang/zh.json b/plugins/calendar-assets/lang/zh.json
index c1c3265037..d17e252afb 100644
--- a/plugins/calendar-assets/lang/zh.json
+++ b/plugins/calendar-assets/lang/zh.json
@@ -110,6 +110,17 @@
"MeetingRescheduledNotification": "会议时间已更改",
"MeetingCanceledNotification": "会议已取消: {title}",
"PrimaryCalendar": "主日历",
- "Hidden": "隐藏"
+ "Hidden": "隐藏",
+ "Each": "每个",
+ "OnThe": "在",
+ "First": "第一个",
+ "Second": "第二个",
+ "Third": "第三个",
+ "Fourth": "第四个",
+ "Fifth": "第五个",
+ "NextToLast": "倒数第二个",
+ "Last": "最后一个",
+ "Weekday": "工作日",
+ "WeekendDay": "周末日"
}
}
\ No newline at end of file
diff --git a/plugins/calendar-resources/src/components/ByDateSelector.svelte b/plugins/calendar-resources/src/components/ByDateSelector.svelte
new file mode 100644
index 0000000000..3f09e5d82f
--- /dev/null
+++ b/plugins/calendar-resources/src/components/ByDateSelector.svelte
@@ -0,0 +1,97 @@
+
+
+
+
+ {#each [...Array(5).keys()] as weekIndex}
+ {#each [...Array(7).keys()] as dayOfWeek}
+ {@const value = weekIndex * 7 + dayOfWeek + 1}
+ {#if value <= 31}
+
+
+
+ {/if}
+ {/each}
+ {/each}
+
+
+
diff --git a/plugins/calendar-resources/src/components/EditEvent.svelte b/plugins/calendar-resources/src/components/EditEvent.svelte
index a3931fb3c4..d5da070439 100644
--- a/plugins/calendar-resources/src/components/EditEvent.svelte
+++ b/plugins/calendar-resources/src/components/EditEvent.svelte
@@ -193,7 +193,7 @@
on:repeat={setRecurrance}
{readOnly}
on:allday={allDayChangeHandler}
- noRepeat
+ noRepeat={object._class !== calendar.class.ReccuringInstance}
/>
diff --git a/plugins/calendar-resources/src/components/MonthSelector.svelte b/plugins/calendar-resources/src/components/MonthSelector.svelte
new file mode 100644
index 0000000000..597b1ee716
--- /dev/null
+++ b/plugins/calendar-resources/src/components/MonthSelector.svelte
@@ -0,0 +1,88 @@
+
+
+
+
+ {#each [...Array(12).keys()] as month}
+ {@const value = month + 1}
+
+
+
+ {/each}
+
+
+
diff --git a/plugins/calendar-resources/src/components/ReccurancePopup.svelte b/plugins/calendar-resources/src/components/ReccurancePopup.svelte
index b4f53b808a..67cdb3bcb6 100644
--- a/plugins/calendar-resources/src/components/ReccurancePopup.svelte
+++ b/plugins/calendar-resources/src/components/ReccurancePopup.svelte
@@ -14,21 +14,26 @@
-->
+
+
+ {
+ if (e.detail !== undefined) {
+ setPos.bySetPos = [e.detail]
+ }
+ }}
+ />
+
+
diff --git a/plugins/calendar-resources/src/plugin.ts b/plugins/calendar-resources/src/plugin.ts
index 4c8188113a..d3c8116738 100644
--- a/plugins/calendar-resources/src/plugin.ts
+++ b/plugins/calendar-resources/src/plugin.ts
@@ -72,6 +72,17 @@ export default mergeIds(calendarId, calendar, {
SeeAllNumberReminders: '' as IntlString,
DefaultVisibility: '' as IntlString,
GooglePrivacy: '' as IntlString,
- PrimaryCalendar: '' as IntlString
+ PrimaryCalendar: '' as IntlString,
+ Each: '' as IntlString,
+ OnThe: '' as IntlString,
+ First: '' as IntlString,
+ Second: '' as IntlString,
+ Third: '' as IntlString,
+ Fourth: '' as IntlString,
+ Fifth: '' as IntlString,
+ NextToLast: '' as IntlString,
+ Last: '' as IntlString,
+ Weekday: '' as IntlString,
+ WeekendDay: '' as IntlString
}
})
diff --git a/plugins/calendar-resources/src/types.ts b/plugins/calendar-resources/src/types.ts
index 4d9f817f34..73d03b4713 100644
--- a/plugins/calendar-resources/src/types.ts
+++ b/plugins/calendar-resources/src/types.ts
@@ -42,3 +42,8 @@ export interface CalendarCell {
hourOfDay: number
minutes: number
}
+
+export interface SetPosRules {
+ bySetPos: number[]
+ byDay: string[]
+}
diff --git a/plugins/calendar/src/__tests__/utils.test.ts b/plugins/calendar/src/__tests__/utils.test.ts
new file mode 100644
index 0000000000..82fa51fc43
--- /dev/null
+++ b/plugins/calendar/src/__tests__/utils.test.ts
@@ -0,0 +1,164 @@
+import { RecurringRule } from '..'
+import { generateRecurringValues } from '../utils'
+
+describe('generateRecurringValues', () => {
+ const baseDate = new Date('2024-01-01T00:00:00Z').getTime()
+ const from = new Date('2024-01-01T00:00:00Z').getTime()
+ const to = new Date('2024-01-10T00:00:00Z').getTime()
+
+ it('generates daily recurring values', () => {
+ const rule: RecurringRule = {
+ freq: 'DAILY',
+ interval: 1
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Should include each day from Jan 1 to Jan 10
+ expect(result.length).toBe(10)
+ expect(result[0]).toBe(baseDate)
+ expect(result[9]).toBe(new Date('2024-01-10T00:00:00Z').getTime())
+ })
+
+ it('supports bySetPos (positive) for monthly frequency', () => {
+ // 1st Monday of each month in the range
+ const rule: RecurringRule = {
+ freq: 'MONTHLY',
+ interval: 1,
+ byDay: ['MO'],
+ bySetPos: [1]
+ }
+ const from = new Date('2024-01-01T00:00:00Z').getTime()
+ const to = new Date('2024-03-31T00:00:00Z').getTime()
+ const baseDate = from
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // 1st Monday of Jan, Feb, Mar 2024
+ expect(result.length).toBe(3)
+ expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-01-01') // Jan 1, 2024 is Monday
+ expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2024-02-05') // Feb 5, 2024 is Monday
+ expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2024-03-04') // Mar 4, 2024 is Monday
+ })
+
+ it('supports bySetPos (negative) for monthly frequency', () => {
+ // Last Friday of each month in the range
+ const rule: RecurringRule = {
+ freq: 'MONTHLY',
+ interval: 1,
+ byDay: ['FR'],
+ bySetPos: [-1]
+ }
+ const from = new Date('2024-01-01T00:00:00Z').getTime()
+ const to = new Date('2024-03-31T00:00:00Z').getTime()
+ const baseDate = from
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Last Friday of Jan, Feb, Mar 2024
+ expect(result.length).toBe(3)
+ expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-01-26')
+ expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2024-02-23')
+ expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2024-03-29')
+ })
+
+ it('supports bySetPos (positive) for yearly frequency', () => {
+ // 1st Sunday of January for each year in range
+ const rule: RecurringRule = {
+ freq: 'YEARLY',
+ interval: 1,
+ byMonth: [0], // January
+ byDay: ['SU'],
+ bySetPos: [1]
+ }
+ const from = new Date('2024-01-01T00:00:00Z').getTime()
+ const to = new Date('2026-12-31T00:00:00Z').getTime()
+ const baseDate = from
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // 1st Sunday of Jan 2024, 2025, 2026
+ expect(result.length).toBe(3)
+ expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-01-07')
+ expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2025-01-05')
+ expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2026-01-04')
+ })
+
+ it('supports bySetPos (negative) for yearly frequency', () => {
+ // Last Wednesday of December for each year in range
+ const rule: RecurringRule = {
+ freq: 'YEARLY',
+ interval: 1,
+ byMonth: [11], // December
+ byDay: ['WE'],
+ bySetPos: [-1]
+ }
+ const from = new Date('2024-01-01T00:00:00Z').getTime()
+ const to = new Date('2026-12-31T00:00:00Z').getTime()
+ const baseDate = from
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Last Wednesday of Dec 2024, 2025, 2026
+ expect(result.length).toBe(3)
+ expect(new Date(result[0]).toISOString().slice(0, 10)).toBe('2024-12-25')
+ expect(new Date(result[1]).toISOString().slice(0, 10)).toBe('2025-12-31')
+ expect(new Date(result[2]).toISOString().slice(0, 10)).toBe('2026-12-30')
+ })
+
+ it('generates weekly recurring values', () => {
+ const rule: RecurringRule = {
+ freq: 'WEEKLY',
+ interval: 1,
+ byDay: ['TU'] // Only Tuesdays
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Jan 2 and Jan 9 are Tuesdays in 2024
+ expect(result.length).toBe(2)
+ expect(new Date(result[0]).getUTCDay()).toBe(2)
+ expect(new Date(result[1]).getUTCDay()).toBe(2)
+ })
+
+ it('generates monthly recurring values', () => {
+ const rule: RecurringRule = {
+ freq: 'MONTHLY',
+ interval: 1,
+ byMonthDay: [1]
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Only Jan 1 falls in the range
+ expect(result.length).toBe(1)
+ expect(result[0]).toBe(baseDate)
+ })
+
+ it('generates yearly recurring values', () => {
+ const rule: RecurringRule = {
+ freq: 'YEARLY',
+ interval: 1,
+ byMonth: [0], // January
+ byMonthDay: [1]
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ // Only Jan 1, 2024
+ expect(result.length).toBe(1)
+ expect(result[0]).toBe(baseDate)
+ })
+
+ it('respects count limit', () => {
+ const rule: RecurringRule = {
+ freq: 'DAILY',
+ interval: 1,
+ count: 3
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ expect(result.length).toBe(3)
+ })
+
+ it('respects endDate limit', () => {
+ const rule: RecurringRule = {
+ freq: 'DAILY',
+ interval: 1,
+ endDate: new Date('2024-01-03T00:00:00Z').getTime()
+ }
+ const result = generateRecurringValues(rule, baseDate, from, to)
+ expect(result.length).toBe(3)
+ expect(result[2]).toBe(new Date('2024-01-03T00:00:00Z').getTime())
+ })
+
+ it('throws on invalid frequency', () => {
+ const rule = {
+ freq: 'INVALID'
+ }
+ expect(() => generateRecurringValues(rule as any, baseDate, from, to)).toThrow('Invalid recurring rule frequency')
+ })
+})
diff --git a/plugins/calendar/src/utils.ts b/plugins/calendar/src/utils.ts
index 9a67d54357..24027b7330 100644
--- a/plugins/calendar/src/utils.ts
+++ b/plugins/calendar/src/utils.ts
@@ -24,40 +24,29 @@ function getInstance (event: ReccuringEvent, date: Timestamp): ReccuringInstance
}
}
-function generateRecurringValues (
+export function generateRecurringValues (
rule: RecurringRule,
startDate: Timestamp,
from: Timestamp,
to: Timestamp
): Timestamp[] {
- const values: Timestamp[] = []
const currentDate = new Date(startDate)
switch (rule.freq) {
case 'DAILY':
- generateDailyValues(rule, currentDate, values, from, to)
- break
+ return generateDailyValues(rule, currentDate, from, to)
case 'WEEKLY':
- generateWeeklyValues(rule, currentDate, values, from, to)
- break
+ return generateWeeklyValues(rule, currentDate, from, to)
case 'MONTHLY':
- generateMonthlyValues(rule, currentDate, values, from, to)
- break
+ return generateMonthlyValues(rule, currentDate, from, to)
case 'YEARLY':
- generateYearlyValues(rule, currentDate, values, from, to)
- break
+ return generateYearlyValues(rule, currentDate, from, to)
default:
throw new Error('Invalid recurring rule frequency')
}
- return values
}
-function generateDailyValues (
- rule: RecurringRule,
- currentDate: Date,
- values: Timestamp[],
- from: Timestamp,
- to: Timestamp
-): void {
+function generateDailyValues (rule: RecurringRule, currentDate: Date, from: Timestamp, to: Timestamp): Timestamp[] {
+ const values: Timestamp[] = []
const { count, endDate, interval } = rule
const { bySetPos } = rule
let i = 0
@@ -76,15 +65,11 @@ function generateDailyValues (
currentDate.setDate(currentDate.getDate() + (interval ?? 1))
if (count !== undefined && i === count) break
}
+ return values
}
-function generateWeeklyValues (
- rule: RecurringRule,
- currentDate: Date,
- values: Timestamp[],
- from: Timestamp,
- to: Timestamp
-): void {
+function generateWeeklyValues (rule: RecurringRule, currentDate: Date, from: Timestamp, to: Timestamp): Timestamp[] {
+ const values: Timestamp[] = []
const { count, endDate, interval } = rule
let { byDay, bySetPos } = rule
let i = 0
@@ -98,8 +83,8 @@ function generateWeeklyValues (
const end = new Date(new Date(currentDate).setDate(currentDate.getDate() + 7))
let date = currentDate
while (date < end) {
- if (endDate != null && date.getTime() > endDate) return
- if (date.getTime() > to) return
+ if (endDate != null && date.getTime() > endDate) return values
+ if (date.getTime() > to) return values
if ((byDay == null || matchesByDay(date, byDay)) && (bySetPos == null || bySetPos.includes(getSetPos(date)))) {
const res = date.getTime()
if (res >= from && res <= to) {
@@ -108,7 +93,7 @@ function generateWeeklyValues (
i++
}
date = new Date(date.setDate(date.getDate() + 1))
- if (count !== undefined && i === count) return
+ if (count !== undefined && i === count) return values
}
currentDate = new Date(next)
@@ -154,13 +139,8 @@ function getNegativePosition (date: Date, weekday: string, pos: number): number
throw new Error(`Unable to calculate negative position ${pos}`)
}
-function generateMonthlyValues (
- rule: RecurringRule,
- currentDate: Date,
- values: Timestamp[],
- from: Timestamp,
- to: Timestamp
-): void {
+function generateMonthlyValues (rule: RecurringRule, currentDate: Date, from: Timestamp, to: Timestamp): Timestamp[] {
+ const values: Timestamp[] = []
const { count, endDate, interval } = rule
let { byDay, byMonthDay, bySetPos } = rule
let i = 0
@@ -170,82 +150,91 @@ function generateMonthlyValues (
}
while (true) {
- const next = new Date(currentDate).setMonth(currentDate.getMonth() + (interval ?? 1))
- const end = new Date(new Date(currentDate).setMonth(currentDate.getMonth() + 1))
+ const next = new Date(currentDate).setMonth(currentDate.getMonth() + (interval ?? 1), 1)
+ const end = new Date(new Date(currentDate).setMonth(currentDate.getMonth() + 1, 1))
let date = currentDate
+ const candidates: Date[] = []
while (date < end) {
- if (endDate != null && date.getTime() > endDate) return
- if (date.getTime() >= to) return
- if (
- (byDay == null || matchesByDay(date, byDay)) &&
- (byMonthDay == null || byMonthDay.includes(new Date(currentDate).getDate())) &&
- (bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
- ) {
- const res = currentDate.getTime()
- if (res >= from && res <= to) {
- values.push(res)
- }
- i++
+ 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))
-
- if (count !== undefined && i === count) return
}
+
+ let filtered: Date[] = candidates
+ if (bySetPos != null) {
+ filtered = bySetPos
+ .map((pos) => {
+ if (pos === 0) return null // invalid
+ const index = pos > 0 ? pos - 1 : candidates.length + pos
+ return candidates[index] ?? null
+ })
+ .filter((d): d is Date => d != null)
+ }
+
+ for (const d of filtered) {
+ const res = d.getTime()
+ if (res >= from && res <= to) {
+ values.push(res)
+ i++
+ if (count !== undefined && i === count) return values
+ }
+ }
+ if (endDate != null && next > endDate) return values
+ if (next >= to) return values
currentDate = new Date(next)
}
}
-function generateYearlyValues (
- rule: RecurringRule,
- currentDate: Date,
- values: Timestamp[],
- from: Timestamp,
- to: Timestamp
-): void {
+function generateYearlyValues (rule: RecurringRule, currentDate: Date, from: Timestamp, to: Timestamp): Timestamp[] {
+ const values: Timestamp[] = []
const { count, endDate, interval } = rule
const { byDay, byMonthDay, byYearDay, byWeekNo, byMonth, bySetPos } = rule
let i = 0
while (true) {
- const next = new Date(currentDate).setFullYear(currentDate.getFullYear() + (interval ?? 1))
- const end = new Date(new Date(currentDate).setFullYear(currentDate.getFullYear() + 1))
+ const next = new Date(currentDate).setFullYear(currentDate.getFullYear() + (interval ?? 1), 0, 1)
+ const end = new Date(new Date(currentDate).setFullYear(currentDate.getFullYear() + 1, 0, 1))
let date = currentDate
+ const candidates: Date[] = []
while (date < end) {
- if (endDate != null && date.getTime() > endDate) return
- if (date.getTime() > to) return
if (
- byDay == null &&
- byMonthDay == null &&
- byYearDay == null &&
- byWeekNo == null &&
- byMonth == null &&
- bySetPos == null
+ (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()))
) {
- date = new Date(next)
- const res = currentDate.getTime()
+ const res = date.getTime()
if (res >= from && res <= to) {
- values.push(res)
+ candidates.push(new Date(res))
}
i++
- } else {
- if (
- (byDay == null || matchesByDay(date, byDay)) &&
- (byMonthDay == null || byMonthDay.includes(currentDate.getDate())) &&
- (byYearDay == null || byYearDay.includes(getYearDay(currentDate))) &&
- (byWeekNo == null || byWeekNo.includes(getWeekNumber(currentDate))) &&
- (byMonth == null || byMonth.includes(currentDate.getMonth())) &&
- (bySetPos == null || bySetPos.includes(getSetPos(currentDate)))
- ) {
- const res = currentDate.getTime()
- if (res >= from && res <= to) {
- values.push(res)
- }
- i++
- }
- date = new Date(date.setDate(date.getDate() + 1))
}
- if (count !== undefined && i === count) return
+ date = new Date(date.setDate(date.getDate() + 1))
}
+
+ let filtered: Date[] = candidates
+ if (bySetPos != null) {
+ filtered = bySetPos
+ .map((pos) => {
+ if (pos === 0) return null // invalid
+ const index = pos > 0 ? pos - 1 : candidates.length + pos
+ return candidates[index] ?? null
+ })
+ .filter((d): d is Date => d != null)
+ }
+
+ for (const d of filtered) {
+ const res = d.getTime()
+ if (res >= from && res <= to) {
+ values.push(res)
+ i++
+ if (count !== undefined && i === count) return values
+ }
+ }
+ if (endDate != null && next > endDate) return values
+ if (next >= to) return values
currentDate = new Date(next)
}
}