refactor: event instances
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
+89
-47
@@ -1,9 +1,9 @@
|
||||
import type { EntityObject } from '@ChronoManager/models/entity'
|
||||
import type { EventObject } from '@ChronoManager/models/event'
|
||||
import type { EventMutationObject } from '@ChronoManager/models/event-mutation'
|
||||
import type { CalendarOccurrence } from '../types/occurrence'
|
||||
import type { CalendarInstance } from '../types/instance'
|
||||
import type { VisibleDateRange } from '../types'
|
||||
import { shiftLocalDays, startOfLocalDay } from './dateRanges'
|
||||
import { parseCalendarDate, shiftLocalDays, startOfLocalDay } from './date'
|
||||
|
||||
const MINIMUM_DURATION_MS = 1
|
||||
|
||||
@@ -13,6 +13,14 @@ function timestamp(value: string | null | undefined): number | null {
|
||||
return Number.isFinite(result) ? result : null
|
||||
}
|
||||
|
||||
function floatingDateTimestamp(value: string | null | undefined): number | null {
|
||||
return parseCalendarDate(value)?.getTime() ?? null
|
||||
}
|
||||
|
||||
function eventTimestamp(value: string | null | undefined, timeless: boolean): number | null {
|
||||
return timeless ? floatingDateTimestamp(value) : timestamp(value)
|
||||
}
|
||||
|
||||
function conclusionTimestamp(value: string | null | undefined): number | null {
|
||||
if (!value) return null
|
||||
|
||||
@@ -36,63 +44,79 @@ function overlapsRange(startMs: number, endMs: number, range?: VisibleDateRange)
|
||||
return !range || (startMs < range.endMs && endMs > range.startMs)
|
||||
}
|
||||
|
||||
function createOccurrence(
|
||||
function createInstance(
|
||||
entity: EntityObject,
|
||||
sourceStartMs: number,
|
||||
effectiveStartMs: number,
|
||||
effectiveEndMs: number,
|
||||
recurring: boolean,
|
||||
timeless: boolean,
|
||||
mutation?: EventMutationObject,
|
||||
): CalendarOccurrence {
|
||||
): CalendarInstance {
|
||||
const event = entity.properties as EventObject
|
||||
const endMs = effectiveEndMs > effectiveStartMs
|
||||
? effectiveEndMs
|
||||
: effectiveStartMs + MINIMUM_DURATION_MS
|
||||
const dayStartMs = startOfLocalDay(new Date(effectiveStartMs)).getTime()
|
||||
const lastOccupiedDayMs = startOfLocalDay(new Date(endMs - 1)).getTime()
|
||||
const occurrenceId = recurring ? new Date(sourceStartMs).toISOString() : null
|
||||
const instanceId = recurring ? new Date(sourceStartMs).toISOString() : null
|
||||
|
||||
return {
|
||||
key: `${String(entity.identifier)}:${occurrenceId ?? 'master'}`,
|
||||
key: `${String(entity.identifier)}:${instanceId ?? 'master'}`,
|
||||
entity,
|
||||
occurrenceId,
|
||||
instanceId,
|
||||
startMs: effectiveStartMs,
|
||||
endMs,
|
||||
dayStartMs,
|
||||
durationMs: endMs - effectiveStartMs,
|
||||
timeless: (mutation?.timeless ?? event.timeless === true) || lastOccupiedDayMs !== dayStartMs,
|
||||
timeless: timeless || lastOccupiedDayMs !== dayStartMs,
|
||||
multiDay: lastOccupiedDayMs !== dayStartMs,
|
||||
label: mutation?.label ?? event.label,
|
||||
color: mutation?.color ?? event.color,
|
||||
}
|
||||
}
|
||||
|
||||
function mutationMap(event: EventObject): Map<number, EventMutationObject> {
|
||||
function mutationMap(event: EventObject, timeless: boolean): Map<number, EventMutationObject> {
|
||||
const mutations = new Map<number, EventMutationObject>()
|
||||
|
||||
for (const [key, mutation] of Object.entries(event.mutations ?? {})) {
|
||||
const mutationId = timestamp(mutation.mutationId) ?? timestamp(key)
|
||||
const mutationId = eventTimestamp(mutation.mutationId, timeless)
|
||||
?? eventTimestamp(key, timeless)
|
||||
if (mutationId !== null) mutations.set(mutationId, mutation)
|
||||
}
|
||||
|
||||
return mutations
|
||||
}
|
||||
|
||||
function materializeOccurrence(
|
||||
function materializeInstance(
|
||||
entity: EntityObject,
|
||||
sourceStartMs: number,
|
||||
durationMs: number,
|
||||
durationDays: number | null,
|
||||
recurring: boolean,
|
||||
mutation: EventMutationObject | undefined,
|
||||
range?: VisibleDateRange,
|
||||
): CalendarOccurrence | null {
|
||||
): CalendarInstance | null {
|
||||
if (mutation?.mutationExclusion === true) return null
|
||||
|
||||
const effectiveStartMs = timestamp(mutation?.startsOn) ?? sourceStartMs
|
||||
const effectiveEndMs = timestamp(mutation?.endsOn) ?? effectiveStartMs + durationMs
|
||||
const event = entity.properties as EventObject
|
||||
const timeless = mutation?.timeless ?? event.timeless === true
|
||||
const effectiveStartMs = eventTimestamp(mutation?.startsOn, timeless) ?? sourceStartMs
|
||||
const defaultEndMs = timeless && durationDays !== null
|
||||
? shiftLocalDays(new Date(effectiveStartMs), durationDays).getTime()
|
||||
: effectiveStartMs + durationMs
|
||||
const effectiveEndMs = eventTimestamp(mutation?.endsOn, timeless) ?? defaultEndMs
|
||||
if (!overlapsRange(effectiveStartMs, effectiveEndMs, range)) return null
|
||||
|
||||
return createOccurrence(entity, sourceStartMs, effectiveStartMs, effectiveEndMs, recurring, mutation)
|
||||
return createInstance(
|
||||
entity,
|
||||
sourceStartMs,
|
||||
effectiveStartMs,
|
||||
effectiveEndMs,
|
||||
recurring,
|
||||
timeless,
|
||||
mutation,
|
||||
)
|
||||
}
|
||||
|
||||
function localDayNumber(date: Date): number {
|
||||
@@ -131,9 +155,10 @@ function expandDaily(
|
||||
entity: EntityObject,
|
||||
start: Date,
|
||||
durationMs: number,
|
||||
durationDays: number | null,
|
||||
range: VisibleDateRange,
|
||||
mutations: Map<number, EventMutationObject>,
|
||||
): CalendarOccurrence[] {
|
||||
): CalendarInstance[] {
|
||||
const pattern = (entity.properties as EventObject).pattern!
|
||||
const interval = Math.max(1, Math.trunc(pattern.interval || 1))
|
||||
const maximumOccurrences = pattern.iterations && pattern.iterations > 0
|
||||
@@ -147,7 +172,7 @@ function expandDaily(
|
||||
? dailyStartIndex(start, interval, durationMs, range)
|
||||
: 0
|
||||
let occurrenceCount = weekdays ? 0 : step
|
||||
const occurrences: CalendarOccurrence[] = []
|
||||
const instances: CalendarInstance[] = []
|
||||
|
||||
while (true) {
|
||||
const candidate = shiftLocalDays(start, step * interval)
|
||||
@@ -159,30 +184,32 @@ function expandDaily(
|
||||
occurrenceCount += 1
|
||||
if (maximumOccurrences !== null && occurrenceCount > maximumOccurrences) break
|
||||
|
||||
const occurrence = materializeOccurrence(
|
||||
const instance = materializeInstance(
|
||||
entity,
|
||||
candidateMs,
|
||||
durationMs,
|
||||
durationDays,
|
||||
true,
|
||||
mutations.get(candidateMs),
|
||||
range,
|
||||
)
|
||||
if (occurrence) occurrences.push(occurrence)
|
||||
if (instance) instances.push(instance)
|
||||
}
|
||||
|
||||
step += 1
|
||||
}
|
||||
|
||||
return occurrences
|
||||
return instances
|
||||
}
|
||||
|
||||
function expandWeekly(
|
||||
entity: EntityObject,
|
||||
start: Date,
|
||||
durationMs: number,
|
||||
durationDays: number | null,
|
||||
range: VisibleDateRange,
|
||||
mutations: Map<number, EventMutationObject>,
|
||||
): CalendarOccurrence[] {
|
||||
): CalendarInstance[] {
|
||||
const pattern = (entity.properties as EventObject).pattern!
|
||||
const interval = Math.max(1, Math.trunc(pattern.interval || 1))
|
||||
const maximumOccurrences = pattern.iterations && pattern.iterations > 0
|
||||
@@ -195,7 +222,7 @@ function expandWeekly(
|
||||
? weeklyStartIndex(weekStart, interval, durationMs, range)
|
||||
: 0
|
||||
let occurrenceCount = maximumOccurrences === null ? week * weekdays.length : 0
|
||||
const occurrences: CalendarOccurrence[] = []
|
||||
const instances: CalendarInstance[] = []
|
||||
|
||||
while (true) {
|
||||
const intervalWeekStart = shiftLocalDays(weekStart, week * interval * 7)
|
||||
@@ -205,79 +232,94 @@ function expandWeekly(
|
||||
const candidate = shiftLocalDays(intervalWeekStart, weekday - 1)
|
||||
const candidateMs = candidate.getTime()
|
||||
if (candidateMs < start.getTime()) continue
|
||||
if (candidateMs >= range.endMs) return occurrences
|
||||
if (concludesMs !== null && candidateMs > concludesMs) return occurrences
|
||||
if (candidateMs >= range.endMs) return instances
|
||||
if (concludesMs !== null && candidateMs > concludesMs) return instances
|
||||
|
||||
occurrenceCount += 1
|
||||
if (maximumOccurrences !== null && occurrenceCount > maximumOccurrences) return occurrences
|
||||
if (maximumOccurrences !== null && occurrenceCount > maximumOccurrences) return instances
|
||||
|
||||
const occurrence = materializeOccurrence(
|
||||
const instance = materializeInstance(
|
||||
entity,
|
||||
candidateMs,
|
||||
durationMs,
|
||||
durationDays,
|
||||
true,
|
||||
mutations.get(candidateMs),
|
||||
range,
|
||||
)
|
||||
if (occurrence) occurrences.push(occurrence)
|
||||
if (instance) instances.push(instance)
|
||||
}
|
||||
|
||||
week += 1
|
||||
}
|
||||
|
||||
return occurrences
|
||||
return instances
|
||||
}
|
||||
|
||||
function appendMovedMutations(
|
||||
occurrences: CalendarOccurrence[],
|
||||
instances: CalendarInstance[],
|
||||
entity: EntityObject,
|
||||
durationMs: number,
|
||||
durationDays: number | null,
|
||||
range: VisibleDateRange,
|
||||
mutations: Map<number, EventMutationObject>,
|
||||
) {
|
||||
const present = new Set(occurrences.map(occurrence => occurrence.key))
|
||||
const present = new Set(instances.map(instance => instance.key))
|
||||
|
||||
for (const [sourceStartMs, mutation] of mutations) {
|
||||
const occurrence = materializeOccurrence(
|
||||
const instance = materializeInstance(
|
||||
entity,
|
||||
sourceStartMs,
|
||||
durationMs,
|
||||
durationDays,
|
||||
true,
|
||||
mutation,
|
||||
range,
|
||||
)
|
||||
if (occurrence && !present.has(occurrence.key)) {
|
||||
occurrences.push(occurrence)
|
||||
present.add(occurrence.key)
|
||||
if (instance && !present.has(instance.key)) {
|
||||
instances.push(instance)
|
||||
present.add(instance.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function expandEntityOccurrences(
|
||||
export function expandEntityInstances(
|
||||
entity: EntityObject,
|
||||
range?: VisibleDateRange,
|
||||
): CalendarOccurrence[] {
|
||||
): CalendarInstance[] {
|
||||
const event = entity.properties as EventObject
|
||||
const startMs = timestamp(event.startsOn)
|
||||
const timeless = event.timeless === true
|
||||
const startMs = eventTimestamp(event.startsOn, timeless)
|
||||
if (startMs === null) return []
|
||||
|
||||
const parsedEndMs = timestamp(event.endsOn)
|
||||
const parsedEndMs = eventTimestamp(event.endsOn, timeless)
|
||||
const durationMs = parsedEndMs !== null && parsedEndMs > startMs
|
||||
? parsedEndMs - startMs
|
||||
: MINIMUM_DURATION_MS
|
||||
const durationDays = timeless && parsedEndMs !== null && parsedEndMs > startMs
|
||||
? Math.max(1, localDayNumber(new Date(parsedEndMs)) - localDayNumber(new Date(startMs)))
|
||||
: null
|
||||
const pattern = event.pattern
|
||||
|
||||
if (!pattern || !range || (pattern.precision !== 'daily' && pattern.precision !== 'weekly')) {
|
||||
const occurrence = materializeOccurrence(entity, startMs, durationMs, false, undefined, range)
|
||||
return occurrence ? [occurrence] : []
|
||||
const instance = materializeInstance(
|
||||
entity,
|
||||
startMs,
|
||||
durationMs,
|
||||
durationDays,
|
||||
false,
|
||||
undefined,
|
||||
range,
|
||||
)
|
||||
return instance ? [instance] : []
|
||||
}
|
||||
|
||||
const mutations = mutationMap(event)
|
||||
const occurrences = pattern.precision === 'daily'
|
||||
? expandDaily(entity, new Date(startMs), durationMs, range, mutations)
|
||||
: expandWeekly(entity, new Date(startMs), durationMs, range, mutations)
|
||||
const mutations = mutationMap(event, timeless)
|
||||
const instances = pattern.precision === 'daily'
|
||||
? expandDaily(entity, new Date(startMs), durationMs, durationDays, range, mutations)
|
||||
: expandWeekly(entity, new Date(startMs), durationMs, durationDays, range, mutations)
|
||||
|
||||
appendMovedMutations(occurrences, entity, durationMs, range, mutations)
|
||||
occurrences.sort((left, right) => left.startMs - right.startMs || left.key.localeCompare(right.key))
|
||||
return occurrences
|
||||
appendMovedMutations(instances, entity, durationMs, durationDays, range, mutations)
|
||||
instances.sort((left, right) => left.startMs - right.startMs || left.key.localeCompare(right.key))
|
||||
return instances
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user