refactor: event instances

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-03 19:10:00 -04:00
parent acbe0ad5ca
commit 37fa7b911e
17 changed files with 498 additions and 479 deletions
+89 -47
View File
@@ -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
}