mirror of
https://github.com/hcengineering/platform.git
synced 2025-03-21 15:02:24 +00:00
383 lines
11 KiB
TypeScript
383 lines
11 KiB
TypeScript
import { Timestamp, generateId } from '@hcengineering/core'
|
|
import calendar, { Event, ReccuringEvent, ReccuringInstance, RecurringRule } from '.'
|
|
|
|
function getInstance (event: ReccuringEvent, date: Timestamp): ReccuringInstance {
|
|
const diff = event.dueDate - event.date
|
|
return {
|
|
...event,
|
|
recurringEventId: event.eventId,
|
|
date,
|
|
dueDate: date + diff,
|
|
originalStartTime: date,
|
|
_class: calendar.class.ReccuringInstance,
|
|
eventId: generateEventId(),
|
|
_id: generateId(),
|
|
virtual: true
|
|
}
|
|
}
|
|
|
|
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
|
|
case 'WEEKLY':
|
|
generateWeeklyValues(rule, currentDate, values, from, to)
|
|
break
|
|
case 'MONTHLY':
|
|
generateMonthlyValues(rule, currentDate, values, from, to)
|
|
break
|
|
case 'YEARLY':
|
|
generateYearlyValues(rule, currentDate, values, from, to)
|
|
break
|
|
default:
|
|
throw new Error('Invalid recurring rule frequency')
|
|
}
|
|
return values
|
|
}
|
|
|
|
function generateDailyValues (
|
|
rule: RecurringRule,
|
|
currentDate: Date,
|
|
values: Timestamp[],
|
|
from: Timestamp,
|
|
to: Timestamp
|
|
): void {
|
|
const { count, endDate, interval } = rule
|
|
const { bySetPos } = rule
|
|
let i = 0
|
|
|
|
while (true) {
|
|
if (bySetPos == null || bySetPos.includes(getSetPos(currentDate))) {
|
|
const res = currentDate.getTime()
|
|
if (currentDate.getTime() > to) break
|
|
if (endDate != null && currentDate.getTime() > endDate) break
|
|
if (res >= from && res <= to) {
|
|
values.push(res)
|
|
}
|
|
i++
|
|
}
|
|
|
|
currentDate.setDate(currentDate.getDate() + (interval ?? 1))
|
|
if (count !== undefined && i === count) break
|
|
}
|
|
}
|
|
|
|
function generateWeeklyValues (
|
|
rule: RecurringRule,
|
|
currentDate: Date,
|
|
values: Timestamp[],
|
|
from: Timestamp,
|
|
to: Timestamp
|
|
): void {
|
|
const { count, endDate, interval } = rule
|
|
let { byDay, bySetPos } = rule
|
|
let i = 0
|
|
|
|
if (byDay === undefined) {
|
|
byDay = [getWeekday(currentDate)]
|
|
}
|
|
|
|
while (true) {
|
|
const next = new Date(currentDate).setDate(currentDate.getDate() + (interval ?? 1) * 7)
|
|
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 ((byDay == null || matchesByDay(date, byDay)) && (bySetPos == null || bySetPos.includes(getSetPos(date)))) {
|
|
const res = date.getTime()
|
|
if (res >= from && res <= to) {
|
|
values.push(res)
|
|
}
|
|
i++
|
|
}
|
|
date = new Date(date.setDate(date.getDate() + 1))
|
|
if (count !== undefined && i === count) return
|
|
}
|
|
|
|
currentDate = new Date(next)
|
|
}
|
|
}
|
|
|
|
function matchesByDay (date: Date, byDay: string[]): boolean {
|
|
const weekday = getWeekday(date)
|
|
const dayOfMonth = Math.floor((date.getDate() - 1) / 7) + 1
|
|
|
|
for (const byDayItem of byDay) {
|
|
if (byDayItem === weekday) {
|
|
return true
|
|
}
|
|
|
|
const pos = parseInt(byDayItem)
|
|
if (isNaN(pos)) continue
|
|
|
|
if (pos > 0 && dayOfMonth === pos) {
|
|
return true
|
|
}
|
|
|
|
if (pos < 0 && dayOfMonth === getNegativePosition(date, weekday, pos)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function getNegativePosition (date: Date, weekday: string, pos: number): number | undefined {
|
|
const lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
|
|
const occurrences = []
|
|
for (let day = lastDayOfMonth; day >= 1; day--) {
|
|
const tempDate = new Date(date.getFullYear(), date.getMonth(), day)
|
|
if (getWeekday(tempDate) === weekday) {
|
|
occurrences.push(day)
|
|
}
|
|
if (occurrences.length === Math.abs(pos)) {
|
|
return occurrences.pop()
|
|
}
|
|
}
|
|
throw new Error(`Unable to calculate negative position ${pos}`)
|
|
}
|
|
|
|
function generateMonthlyValues (
|
|
rule: RecurringRule,
|
|
currentDate: Date,
|
|
values: Timestamp[],
|
|
from: Timestamp,
|
|
to: Timestamp
|
|
): void {
|
|
const { count, endDate, interval } = rule
|
|
let { byDay, byMonthDay, bySetPos } = rule
|
|
let i = 0
|
|
|
|
if (byDay == null && byMonthDay == null) {
|
|
byMonthDay = [currentDate.getDate()]
|
|
}
|
|
|
|
while (true) {
|
|
const next = new Date(currentDate).setMonth(currentDate.getMonth() + (interval ?? 1))
|
|
const end = new Date(new Date(currentDate).setMonth(currentDate.getMonth() + 1))
|
|
let date = currentDate
|
|
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++
|
|
}
|
|
date = new Date(date.setDate(date.getDate() + 1))
|
|
|
|
if (count !== undefined && i === count) return
|
|
}
|
|
currentDate = new Date(next)
|
|
}
|
|
}
|
|
|
|
function generateYearlyValues (
|
|
rule: RecurringRule,
|
|
currentDate: Date,
|
|
values: Timestamp[],
|
|
from: Timestamp,
|
|
to: Timestamp
|
|
): void {
|
|
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))
|
|
let date = currentDate
|
|
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
|
|
) {
|
|
date = new Date(next)
|
|
const res = currentDate.getTime()
|
|
if (res >= from && res <= to) {
|
|
values.push(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
|
|
}
|
|
currentDate = new Date(next)
|
|
}
|
|
}
|
|
|
|
function getSetPos (date: Date): number {
|
|
const month = date.getMonth()
|
|
const year = date.getFullYear()
|
|
const firstOfMonth = new Date(year, month, 1)
|
|
const daysOffset = firstOfMonth.getDay()
|
|
const day = date.getDate()
|
|
|
|
return Math.ceil((day + daysOffset) / 7)
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getWeekday (date: Date): string {
|
|
const weekdays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
|
|
const weekday = weekdays[date.getDay()]
|
|
|
|
return weekday
|
|
}
|
|
|
|
function getWeekNumber (date: Date): number {
|
|
const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
|
|
const daysOffset = firstDayOfYear.getDay()
|
|
const diff = (date.getTime() - firstDayOfYear.getTime()) / 86400000
|
|
|
|
return Math.floor((diff + daysOffset + 1) / 7)
|
|
}
|
|
|
|
function getYearDay (date: Date): number {
|
|
const startOfYear = new Date(date.getFullYear(), 0, 0)
|
|
const diff = date.getTime() - startOfYear.getTime()
|
|
|
|
return Math.floor(diff / 86400000)
|
|
}
|
|
|
|
function getReccuringEventInstances (
|
|
event: ReccuringEvent,
|
|
instances: ReccuringInstance[],
|
|
from: Timestamp,
|
|
to: Timestamp
|
|
): ReccuringInstance[] {
|
|
let res: ReccuringInstance[] = []
|
|
for (const rule of event.rules ?? []) {
|
|
const values = generateRecurringValues(rule, event.date, from, to)
|
|
for (const val of values) {
|
|
const instance = getInstance(event, val)
|
|
res.push(instance)
|
|
}
|
|
}
|
|
for (const date of event.rdate ?? []) {
|
|
if (date < from || date > to) continue
|
|
const instance = getInstance(event, date)
|
|
const exists = res.find((p) => p.date === instance.date)
|
|
if (exists === undefined) res.push(instance)
|
|
}
|
|
|
|
const excludes = new Set(event.exdate ?? [])
|
|
res = res.filter((p) => !excludes.has(p.date))
|
|
res = res.filter((i) => {
|
|
const override = instances.find((p) => p.originalStartTime === i.originalStartTime)
|
|
return override === undefined
|
|
})
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function getAllEvents (events: Event[], from: Timestamp, to: Timestamp): Event[] {
|
|
const base: Event[] = []
|
|
const recur: ReccuringEvent[] = []
|
|
const instances: ReccuringInstance[] = []
|
|
const recurData: ReccuringInstance[] = []
|
|
const instancesMap = new Map<string, ReccuringInstance[]>()
|
|
for (const event of events) {
|
|
if (event._class === calendar.class.ReccuringEvent) {
|
|
recur.push(event as ReccuringEvent)
|
|
} else if (event._class === calendar.class.ReccuringInstance) {
|
|
const instance = event as ReccuringInstance
|
|
instances.push(instance)
|
|
const arr = instancesMap.get(instance.recurringEventId) ?? []
|
|
arr.push(instance)
|
|
instancesMap.set(instance.recurringEventId, arr)
|
|
} else {
|
|
if (from >= event.dueDate) continue
|
|
if (event.date >= to) continue
|
|
|
|
base.push(event)
|
|
}
|
|
}
|
|
for (const rec of recur) {
|
|
const recEvents = getReccuringEventInstances(rec, instancesMap.get(rec.eventId) ?? [], from, to)
|
|
recurData.push(...recEvents)
|
|
}
|
|
const res = [
|
|
...base,
|
|
...recurData,
|
|
...instances.filter((p) => {
|
|
return from <= p.dueDate && p.date <= to && p.isCancelled !== true
|
|
})
|
|
].map((it) => ({ ...it, date: Math.max(from, it.date), dueDate: Math.min(to, it.dueDate) }))
|
|
res.sort((a, b) => a.date - b.date)
|
|
return res
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function generateEventId (): string {
|
|
const id = generateId()
|
|
return encodeToBase32Hex(id)
|
|
}
|
|
|
|
function encodeToBase32Hex (input: string): string {
|
|
const encoder = new TextEncoder()
|
|
const bytes = encoder.encode(input)
|
|
const base32HexDigits = '0123456789abcdefghijklmnopqrstuv'
|
|
let result = ''
|
|
|
|
for (let i = 0; i < bytes.length; i += 5) {
|
|
const octets = [
|
|
(bytes[i] ?? 0) >> 3,
|
|
((bytes[i] & 0x07) << 2) | (bytes[i + 1] >> 6),
|
|
(bytes[i + 1] >> 1) & 0x1f,
|
|
((bytes[i + 1] & 0x01) << 4) | (bytes[i + 2] >> 4),
|
|
((bytes[i + 2] & 0x0f) << 1) | (bytes[i + 3] >> 7),
|
|
(bytes[i + 3] >> 2) & 0x1f,
|
|
((bytes[i + 3] & 0x03) << 3) | (bytes[i + 4] >> 5),
|
|
bytes[i + 4] & 0x1f
|
|
]
|
|
|
|
for (let j = 0; j < 8; j++) {
|
|
result += base32HexDigits.charAt(octets[j])
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|