mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 12:04:56 +02:00
Enhanced the recurring event functionality (#9475)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user