@happyvertical/smrt-events 0.51.39 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -7,7 +7,8 @@ Infinite-nesting event hierarchy with series, types, participants, and role/plac
7
7
  - **Event** (STI, extends `SmrtHierarchical`): self-referencing parent-child via `parentId` (UUID). Links to `seriesId`, `typeId`, `placeId`. Status: scheduled/in_progress/completed/cancelled/postponed. Hierarchy traversal (`getParent()`, `getChildren()`, `getAncestors()`, `getDescendants()`, `getHierarchy()`, `moveTo()`) is provided by `SmrtHierarchical`; `getRootEvent()` and `isRoot()` remain on `Event`.
8
8
  - **EventAsset**: dedicated owned-asset join in `event_assets` with `relationship` and `sortOrder`.
9
9
  - **EventType**: classification with JSON schema for custom fields per type.
10
- - **EventSeries**: recurrence patterns (daily/weekly/monthly/yearly).
10
+ - **Event.allDay / Event.timeZone**: whole-day flag (`endDate` is the exclusive midnight after the last day) and IANA zone (empty = inherit). Manifest fields; consumers migrate with `db:migrate`.
11
+ - **EventSeries**: recurrence patterns (daily/weekly/monthly/yearly). `expandRecurrence()` (`src/recurrence.ts`) / `EventSeries.getOccurrences()` expand them into instants, keeping the start's wall-clock time in the zone. Do all date math on calendar dates and convert through `Intl` at the end — never `Date#setDate`/`getDay` on host-local time (tests pin `TZ=UTC`, so host leaks only show in production).
11
12
  - **EventParticipant**: junction with `role` (home/away/speaker/panelist/etc.), `placement` (numeric — team ordering and rankings), `groupId` (team grouping within event). `conflictColumns: ['event_id', 'profile_id', 'role']`.
12
13
 
13
14
  ## Gotchas
package/README.md CHANGED
@@ -55,6 +55,38 @@ await series.create({
55
55
  });
56
56
  ```
57
57
 
58
+ ### All-day events, time zones, and recurrence expansion
59
+
60
+ `Event.allDay` marks whole-day events (`startDate` is local midnight of the
61
+ first day, `endDate` the exclusive midnight after the last day) and
62
+ `Event.timeZone` is the IANA zone the event is scheduled in (empty inherits
63
+ from the series, place, or site).
64
+
65
+ `expandRecurrence(pattern, { start, rangeStart, rangeEnd, timeZone })` turns
66
+ a `RecurrencePattern` (object or stored JSON) into occurrence start instants.
67
+ It supports `frequency` daily/weekly/monthly/yearly, `interval`, `byDay`
68
+ (weekly day lists; monthly ordinals like `2TU` or `-1FR`), `byMonthDay`
69
+ (negative counts from the month end), `byMonth`, `bySetPos`, inclusive
70
+ `until`, and `count` (counted from `start`). The start's wall-clock time is
71
+ kept in `timeZone` (default `pattern.timeZone`, then UTC), so a 19:00 meeting
72
+ stays at 19:00 across daylight-saving changes. `EventSeries.getOccurrences()`
73
+ wraps it with the series' `startDate`/`endDate`.
74
+
75
+ ```typescript
76
+ import { expandRecurrence } from '@happyvertical/smrt-events';
77
+
78
+ // Council meets the second Tuesday at 19:00 Edmonton time
79
+ const october = expandRecurrence(
80
+ { frequency: 'monthly', byDay: ['2TU'] },
81
+ {
82
+ start: new Date('2026-01-14T02:00:00Z'), // Jan 13 19:00 MST
83
+ rangeStart: new Date('2026-10-01T00:00:00Z'),
84
+ rangeEnd: new Date('2026-11-01T00:00:00Z'),
85
+ timeZone: 'America/Edmonton',
86
+ },
87
+ );
88
+ ```
89
+
58
90
  ### Owned assets
59
91
 
60
92
  ```typescript
@@ -80,7 +112,7 @@ const galleryAssets = await events.getAssets(game.id!, 'gallery');
80
112
 
81
113
  | Export | Description |
82
114
  |--------|------------|
83
- | `Event` | Hierarchical event with status lifecycle, STI enabled. Links to series, type, place via string IDs |
115
+ | `Event` | Hierarchical event with status lifecycle, STI enabled. Links to series, type, place via string IDs; `allDay` and IANA `timeZone` fields |
84
116
  | `EventSeries` | Recurring event group with recurrence patterns (daily/weekly/monthly/yearly) |
85
117
  | `EventType` | Classification with JSON schema for custom fields per type |
86
118
  | `EventParticipant` | Junction linking profiles to events with role, placement, and groupId |
@@ -120,7 +152,10 @@ const galleryAssets = await events.getAssets(game.id!, 'gallery');
120
152
  | `getEventStatusFromDates` | Auto-detect status from start/end dates |
121
153
  | `sortEventsByDate` | Sort events chronologically |
122
154
  | `validateEventStatus` | Validate status transition is allowed |
123
- | `calculateNextOccurrence` | Next date for a recurrence pattern |
155
+ | `calculateNextOccurrence` | Next date for a recurrence pattern (host-local arithmetic; prefer `expandRecurrence`) |
156
+ | `expandRecurrence` | Time-zone-aware occurrence instants for a pattern within a window |
157
+ | `toZonedWallTime` / `fromZonedWallTime` | Convert between an instant and wall-clock fields in an IANA zone |
158
+ | `isValidTimeZone` | Check an IANA zone name against the runtime's `Intl` data |
124
159
  | `parseRecurrencePattern` | Parse recurrence from string or object |
125
160
 
126
161
  ### UI Metadata
@@ -0,0 +1,320 @@
1
+ //#region src/recurrence.ts
2
+ var WEEKDAY_CODES = [
3
+ "SU",
4
+ "MO",
5
+ "TU",
6
+ "WE",
7
+ "TH",
8
+ "FR",
9
+ "SA"
10
+ ];
11
+ var DAY_MS = 864e5;
12
+ var MAX_PERIODS = 5e4;
13
+ var formatterCache = /* @__PURE__ */ new Map();
14
+ function formatterFor(timeZone) {
15
+ let formatter = formatterCache.get(timeZone);
16
+ if (!formatter) {
17
+ formatter = new Intl.DateTimeFormat("en-US", {
18
+ timeZone,
19
+ hourCycle: "h23",
20
+ year: "numeric",
21
+ month: "numeric",
22
+ day: "numeric",
23
+ hour: "numeric",
24
+ minute: "numeric",
25
+ second: "numeric"
26
+ });
27
+ formatterCache.set(timeZone, formatter);
28
+ }
29
+ return formatter;
30
+ }
31
+ function isValidTimeZone(timeZone) {
32
+ if (!timeZone) return false;
33
+ try {
34
+ formatterFor(timeZone);
35
+ return true;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ function toZonedWallTime(instant, timeZone) {
41
+ const parts = formatterFor(timeZone).formatToParts(instant);
42
+ const get = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
43
+ return {
44
+ year: get("year"),
45
+ month: get("month"),
46
+ day: get("day"),
47
+ hour: get("hour") % 24,
48
+ minute: get("minute"),
49
+ second: get("second"),
50
+ millisecond: (instant.getTime() % 1e3 + 1e3) % 1e3
51
+ };
52
+ }
53
+ function offsetAt(epochMs, timeZone) {
54
+ const wall = toZonedWallTime(new Date(epochMs), timeZone);
55
+ return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second, wall.millisecond) - epochMs;
56
+ }
57
+ function fromZonedWallTime(wall, timeZone) {
58
+ const guess = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second, wall.millisecond);
59
+ const before = offsetAt(guess - DAY_MS, timeZone);
60
+ const matches = [.../* @__PURE__ */ new Set([
61
+ before,
62
+ offsetAt(guess, timeZone),
63
+ offsetAt(guess + DAY_MS, timeZone)
64
+ ])].map((offset) => guess - offset).filter((epoch) => offsetAt(epoch, timeZone) === guess - epoch).sort((a, b) => a - b);
65
+ if (matches.length > 0) return new Date(matches[0]);
66
+ return new Date(guess - before);
67
+ }
68
+ function civilToDayNumber(date) {
69
+ return Math.floor(Date.UTC(date.year, date.month - 1, date.day) / DAY_MS);
70
+ }
71
+ function dayNumberToCivil(dayNumber) {
72
+ const date = /* @__PURE__ */ new Date(dayNumber * DAY_MS);
73
+ return {
74
+ year: date.getUTCFullYear(),
75
+ month: date.getUTCMonth() + 1,
76
+ day: date.getUTCDate()
77
+ };
78
+ }
79
+ function weekdayOf(dayNumber) {
80
+ return ((dayNumber + 4) % 7 + 7) % 7;
81
+ }
82
+ function daysInMonth(year, month) {
83
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
84
+ }
85
+ function parseByDay(values) {
86
+ if (!values) return [];
87
+ const parsed = [];
88
+ for (const raw of values) {
89
+ const match = /^([+-]?\d{1,2})?([A-Za-z]{2})$/.exec(raw.trim());
90
+ if (!match) throw new RangeError(`Invalid recurrence byDay value: ${raw}`);
91
+ const weekday = WEEKDAY_CODES.indexOf(match[2].toUpperCase());
92
+ if (weekday < 0) throw new RangeError(`Invalid recurrence byDay value: ${raw}`);
93
+ parsed.push({
94
+ weekday,
95
+ ordinal: match[1] ? Number(match[1]) : null
96
+ });
97
+ }
98
+ return parsed;
99
+ }
100
+ function toDate(value) {
101
+ if (value === void 0 || value === null || value === "") return null;
102
+ const date = value instanceof Date ? value : new Date(value);
103
+ if (Number.isNaN(date.getTime())) throw new RangeError(`Invalid recurrence until date: ${String(value)}`);
104
+ return date;
105
+ }
106
+ function monthDays(year, month, pattern, byDay, startDay) {
107
+ const length = daysInMonth(year, month);
108
+ const firstDayNumber = civilToDayNumber({
109
+ year,
110
+ month,
111
+ day: 1
112
+ });
113
+ let days;
114
+ if (pattern.byMonthDay?.length) days = pattern.byMonthDay.map((value) => value < 0 ? length + 1 + value : value).filter((value) => value >= 1 && value <= length);
115
+ else if (byDay.length) {
116
+ days = [];
117
+ for (const { weekday, ordinal } of byDay) {
118
+ const matches = [];
119
+ for (let day = 1; day <= length; day++) if (weekdayOf(firstDayNumber + day - 1) === weekday) matches.push(day);
120
+ if (ordinal === null) days.push(...matches);
121
+ else {
122
+ const index = ordinal > 0 ? ordinal - 1 : matches.length + ordinal;
123
+ if (index >= 0 && index < matches.length) days.push(matches[index]);
124
+ }
125
+ }
126
+ } else days = startDay <= length ? [startDay] : [];
127
+ days = [...new Set(days)].sort((a, b) => a - b);
128
+ if (pattern.bySetPos?.length) {
129
+ const picked = pattern.bySetPos.map((pos) => pos > 0 ? days[pos - 1] : days[days.length + pos]).filter((day) => day !== void 0);
130
+ days = [...new Set(picked)].sort((a, b) => a - b);
131
+ }
132
+ return days;
133
+ }
134
+ function periodDates(pattern, period, first, firstDayNumber, byDay) {
135
+ const interval = Math.max(1, Math.floor(pattern.interval ?? 1));
136
+ switch (pattern.frequency) {
137
+ case "daily": return [firstDayNumber + period * interval];
138
+ case "weekly": {
139
+ const weekStart = firstDayNumber - (weekdayOf(firstDayNumber) + 6) % 7 + period * interval * 7;
140
+ return (byDay.length ? [...new Set(byDay.map((entry) => entry.weekday))] : [weekdayOf(firstDayNumber)]).map((weekday) => weekStart + (weekday + 6) % 7).sort((a, b) => a - b);
141
+ }
142
+ case "monthly": {
143
+ const monthIndex = first.month - 1 + period * interval;
144
+ const year = first.year + Math.floor(monthIndex / 12);
145
+ const month = monthIndex % 12 + 1;
146
+ const base = civilToDayNumber({
147
+ year,
148
+ month,
149
+ day: 1
150
+ }) - 1;
151
+ return monthDays(year, month, pattern, byDay, first.day).map((day) => base + day);
152
+ }
153
+ case "yearly": {
154
+ const year = first.year + period * interval;
155
+ const months = pattern.byMonth?.length ? [...new Set(pattern.byMonth)].sort((a, b) => a - b) : [first.month];
156
+ const dates = [];
157
+ for (const month of months) {
158
+ if (month < 1 || month > 12) continue;
159
+ if (pattern.byMonthDay?.length || byDay.length) {
160
+ const base = civilToDayNumber({
161
+ year,
162
+ month,
163
+ day: 1
164
+ }) - 1;
165
+ dates.push(...monthDays(year, month, pattern, byDay, first.day).map((day) => base + day));
166
+ } else if (first.day <= daysInMonth(year, month)) dates.push(civilToDayNumber({
167
+ year,
168
+ month,
169
+ day: first.day
170
+ }));
171
+ }
172
+ return dates.sort((a, b) => a - b);
173
+ }
174
+ default: throw new RangeError(`Unsupported recurrence frequency: ${String(pattern.frequency)}`);
175
+ }
176
+ }
177
+ function expandRecurrence(pattern, options) {
178
+ const parsed = typeof pattern === "string" ? pattern ? JSON.parse(pattern) : null : pattern ?? null;
179
+ if (!parsed) return [];
180
+ const timeZone = options.timeZone || parsed.timeZone || "UTC";
181
+ const limit = options.limit ?? 1e3;
182
+ const startMs = options.start.getTime();
183
+ const rangeStartMs = options.rangeStart.getTime();
184
+ const rangeEndMs = options.rangeEnd.getTime();
185
+ if (Number.isNaN(startMs) || Number.isNaN(rangeStartMs)) throw new RangeError("expandRecurrence needs valid start and range dates");
186
+ if (rangeEndMs < rangeStartMs) return [];
187
+ const until = toDate(parsed.until);
188
+ const untilMs = until ? until.getTime() : Number.POSITIVE_INFINITY;
189
+ const count = parsed.count ?? Number.POSITIVE_INFINITY;
190
+ const byDay = parseByDay(parsed.byDay);
191
+ const wall = toZonedWallTime(options.start, timeZone);
192
+ const first = {
193
+ year: wall.year,
194
+ month: wall.month,
195
+ day: wall.day
196
+ };
197
+ const firstDayNumber = civilToDayNumber(first);
198
+ const results = [];
199
+ let produced = 0;
200
+ for (let period = 0; period < MAX_PERIODS; period++) {
201
+ const dates = periodDates(parsed, period, first, firstDayNumber, byDay);
202
+ for (const dayNumber of dates) {
203
+ if (dayNumber < firstDayNumber) continue;
204
+ const occurrence = fromZonedWallTime({
205
+ ...dayNumberToCivil(dayNumber),
206
+ hour: wall.hour,
207
+ minute: wall.minute,
208
+ second: wall.second,
209
+ millisecond: wall.millisecond
210
+ }, timeZone);
211
+ const occurrenceMs = occurrence.getTime();
212
+ if (occurrenceMs < startMs) continue;
213
+ if (occurrenceMs > untilMs || produced >= count) return results;
214
+ if (occurrenceMs > rangeEndMs) return results;
215
+ produced++;
216
+ if (occurrenceMs >= rangeStartMs) {
217
+ results.push(occurrence);
218
+ if (results.length >= limit) return results;
219
+ }
220
+ }
221
+ }
222
+ return results;
223
+ }
224
+ //#endregion
225
+ //#region src/utils.ts
226
+ function validateEventStatus(currentStatus, newStatus) {
227
+ return {
228
+ scheduled: [
229
+ "in_progress",
230
+ "cancelled",
231
+ "postponed"
232
+ ],
233
+ in_progress: ["completed", "cancelled"],
234
+ completed: [],
235
+ cancelled: ["scheduled"],
236
+ postponed: ["scheduled", "cancelled"]
237
+ }[currentStatus]?.includes(newStatus) || false;
238
+ }
239
+ function formatEventDateRange(startDate, endDate) {
240
+ const start = startDate.toLocaleDateString();
241
+ if (!endDate) return start;
242
+ if (startDate.toDateString() === endDate.toDateString()) return `${start} ${startDate.toLocaleTimeString()} - ${endDate.toLocaleTimeString()}`;
243
+ return `${start} - ${endDate.toLocaleDateString()}`;
244
+ }
245
+ function generateEventSlug(name, date) {
246
+ return `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}-${date.toISOString().split("T")[0]}`;
247
+ }
248
+ function checkSchedulingConflict(event1Start, event1End, event2Start, event2End) {
249
+ return event1Start < (event2End || event2Start) && (event1End || event1Start) > event2Start;
250
+ }
251
+ function parseRecurrencePattern(pattern) {
252
+ if (!pattern) return null;
253
+ if (typeof pattern === "string") try {
254
+ return JSON.parse(pattern);
255
+ } catch {
256
+ return null;
257
+ }
258
+ return pattern;
259
+ }
260
+ function calculateNextOccurrence(pattern, fromDate) {
261
+ const { frequency, interval = 1, until, count } = pattern;
262
+ if (until && fromDate >= until) return null;
263
+ const nextDate = new Date(fromDate);
264
+ switch (frequency) {
265
+ case "daily":
266
+ nextDate.setDate(nextDate.getDate() + interval);
267
+ break;
268
+ case "weekly":
269
+ nextDate.setDate(nextDate.getDate() + interval * 7);
270
+ break;
271
+ case "monthly":
272
+ nextDate.setMonth(nextDate.getMonth() + interval);
273
+ break;
274
+ case "yearly":
275
+ nextDate.setFullYear(nextDate.getFullYear() + interval);
276
+ break;
277
+ default: return null;
278
+ }
279
+ if (until && nextDate > until) return null;
280
+ return nextDate;
281
+ }
282
+ function calculateDuration(startDate, endDate) {
283
+ return endDate.getTime() - startDate.getTime();
284
+ }
285
+ function formatDuration(durationMs) {
286
+ const seconds = Math.floor(durationMs / 1e3);
287
+ const minutes = Math.floor(seconds / 60);
288
+ const hours = Math.floor(minutes / 60);
289
+ const days = Math.floor(hours / 24);
290
+ if (days > 0) return `${days}d ${hours % 24}h`;
291
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
292
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
293
+ return `${seconds}s`;
294
+ }
295
+ function isEventNow(startDate, endDate) {
296
+ const now = /* @__PURE__ */ new Date();
297
+ if (now < startDate) return false;
298
+ if (endDate && now > endDate) return false;
299
+ return true;
300
+ }
301
+ function getEventStatusFromDates(startDate, endDate, currentStatus) {
302
+ if (currentStatus === "cancelled" || currentStatus === "postponed") return currentStatus;
303
+ const now = /* @__PURE__ */ new Date();
304
+ if (now < startDate) return "scheduled";
305
+ if (endDate && now > endDate) return "completed";
306
+ return "in_progress";
307
+ }
308
+ function sortEventsByDate(events, ascending = true) {
309
+ return events.sort((a, b) => {
310
+ if (!a.startDate && !b.startDate) return 0;
311
+ if (!a.startDate) return 1;
312
+ if (!b.startDate) return -1;
313
+ const diff = a.startDate.getTime() - b.startDate.getTime();
314
+ return ascending ? diff : -diff;
315
+ });
316
+ }
317
+ //#endregion
318
+ export { formatEventDateRange as a, isEventNow as c, validateEventStatus as d, expandRecurrence as f, toZonedWallTime as h, formatDuration as i, parseRecurrencePattern as l, isValidTimeZone as m, calculateNextOccurrence as n, generateEventSlug as o, fromZonedWallTime as p, checkSchedulingConflict as r, getEventStatusFromDates as s, calculateDuration as t, sortEventsByDate as u };
319
+
320
+ //# sourceMappingURL=utils-DV00CoDY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils-DV00CoDY.js","names":[],"sources":["../../src/recurrence.ts","../../src/utils.ts"],"sourcesContent":["/**\n * Time-zone-aware recurrence expansion for {@link RecurrencePattern}.\n *\n * Implements the RFC 5545 RRULE subset that EventSeries patterns use:\n * `FREQ` daily / weekly / monthly / yearly, `INTERVAL`, `BYDAY` (weekly day\n * lists and monthly ordinals such as `2TU` / `-1FR`), `BYMONTHDAY` (negative\n * values count from the month end), `BYMONTH` (yearly), `BYSETPOS`\n * (monthly), `UNTIL`, and `COUNT`.\n *\n * Occurrences keep the wall-clock time of the first occurrence in the given\n * IANA time zone, so a 19:00 weekly meeting stays at 19:00 local across a\n * daylight-saving change. All arithmetic runs on calendar dates; instants are\n * produced only at the end through `Intl`, so the host time zone never leaks\n * into the result.\n */\n\nimport type { RecurrencePattern } from './types';\n\n/** Options for {@link expandRecurrence}. */\nexport interface ExpandRecurrenceOptions {\n /** First occurrence (RRULE `DTSTART`); its local time is reused for every occurrence. */\n start: Date;\n /** Inclusive lower bound of the window to return occurrences for. */\n rangeStart: Date;\n /** Inclusive upper bound of the window to return occurrences for. */\n rangeEnd: Date;\n /** IANA time zone the pattern repeats in. Defaults to `pattern.timeZone`, then `UTC`. */\n timeZone?: string;\n /** Safety cap on returned occurrences. Defaults to 1000. */\n limit?: number;\n}\n\nconst WEEKDAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] as const;\nconst DAY_MS = 86_400_000;\n/** Hard ceiling on generated periods so a pattern that never matches still terminates. */\nconst MAX_PERIODS = 50_000;\n\n/** A calendar date with no time zone. */\nexport interface CivilDate {\n year: number;\n month: number; // 1-12\n day: number;\n}\n\n/** A local date and time with no time zone. */\nexport interface ZonedWallTime extends CivilDate {\n hour: number;\n minute: number;\n second: number;\n millisecond: number;\n}\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nfunction formatterFor(timeZone: string): Intl.DateTimeFormat {\n let formatter = formatterCache.get(timeZone);\n if (!formatter) {\n // Throws RangeError for an unknown zone — callers get a real error.\n formatter = new Intl.DateTimeFormat('en-US', {\n timeZone,\n hourCycle: 'h23',\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n });\n formatterCache.set(timeZone, formatter);\n }\n return formatter;\n}\n\n/**\n * Check whether a string is an IANA time zone this runtime recognizes.\n *\n * @param timeZone - Candidate zone name (e.g. `America/Edmonton`)\n * @returns True when `Intl` accepts the zone\n */\nexport function isValidTimeZone(timeZone: string): boolean {\n if (!timeZone) return false;\n try {\n formatterFor(timeZone);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Wall-clock fields of an instant in a time zone.\n *\n * @param instant - Instant to project\n * @param timeZone - IANA time zone\n * @returns Local year/month/day/hour/minute/second/millisecond\n */\nexport function toZonedWallTime(\n instant: Date,\n timeZone: string,\n): ZonedWallTime {\n const parts = formatterFor(timeZone).formatToParts(instant);\n const get = (type: string) =>\n Number(parts.find((part) => part.type === type)?.value ?? 0);\n return {\n year: get('year'),\n month: get('month'),\n day: get('day'),\n hour: get('hour') % 24,\n minute: get('minute'),\n second: get('second'),\n millisecond: ((instant.getTime() % 1000) + 1000) % 1000,\n };\n}\n\nfunction offsetAt(epochMs: number, timeZone: string): number {\n const wall = toZonedWallTime(new Date(epochMs), timeZone);\n const asUtc = Date.UTC(\n wall.year,\n wall.month - 1,\n wall.day,\n wall.hour,\n wall.minute,\n wall.second,\n wall.millisecond,\n );\n return asUtc - epochMs;\n}\n\n/**\n * Instant for a wall-clock time in a time zone.\n *\n * - A time skipped by a daylight-saving jump (spring forward) resolves\n * FORWARD by the size of the gap: 02:30 on a 02:00 -> 03:00 day is 03:30.\n * - A time that occurs twice (fall back) resolves to its FIRST occurrence.\n *\n * These are the RFC 5545 / Temporal `compatible` rules, independent of the\n * zone's sign or the host time zone.\n *\n * @param wall - Local date and time\n * @param timeZone - IANA time zone\n * @returns The matching instant\n */\nexport function fromZonedWallTime(wall: ZonedWallTime, timeZone: string): Date {\n const guess = Date.UTC(\n wall.year,\n wall.month - 1,\n wall.day,\n wall.hour,\n wall.minute,\n wall.second,\n wall.millisecond,\n );\n // Zone offsets in force a day either side bracket any single transition\n // near this wall time; together with the offsets at the naive guess they\n // are every offset the wall time can resolve under.\n const before = offsetAt(guess - DAY_MS, timeZone);\n const offsets = new Set([\n before,\n offsetAt(guess, timeZone),\n offsetAt(guess + DAY_MS, timeZone),\n ]);\n const matches = [...offsets]\n .map((offset) => guess - offset)\n .filter((epoch) => offsetAt(epoch, timeZone) === guess - epoch)\n .sort((a, b) => a - b);\n if (matches.length > 0) return new Date(matches[0]);\n // No offset reproduces the wall time: it falls in a gap. Read it with the\n // offset in force before the jump, which lands after the jump by exactly\n // the gap's size.\n return new Date(guess - before);\n}\n\nfunction civilToDayNumber(date: CivilDate): number {\n return Math.floor(Date.UTC(date.year, date.month - 1, date.day) / DAY_MS);\n}\n\nfunction dayNumberToCivil(dayNumber: number): CivilDate {\n const date = new Date(dayNumber * DAY_MS);\n return {\n year: date.getUTCFullYear(),\n month: date.getUTCMonth() + 1,\n day: date.getUTCDate(),\n };\n}\n\nfunction weekdayOf(dayNumber: number): number {\n // Day 0 (1970-01-01) was a Thursday.\n return (((dayNumber + 4) % 7) + 7) % 7;\n}\n\nfunction daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\ninterface ParsedByDay {\n weekday: number;\n ordinal: number | null;\n}\n\nfunction parseByDay(values: string[] | undefined): ParsedByDay[] {\n if (!values) return [];\n const parsed: ParsedByDay[] = [];\n for (const raw of values) {\n const match = /^([+-]?\\d{1,2})?([A-Za-z]{2})$/.exec(raw.trim());\n if (!match) {\n throw new RangeError(`Invalid recurrence byDay value: ${raw}`);\n }\n const weekday = WEEKDAY_CODES.indexOf(\n match[2].toUpperCase() as (typeof WEEKDAY_CODES)[number],\n );\n if (weekday < 0) {\n throw new RangeError(`Invalid recurrence byDay value: ${raw}`);\n }\n parsed.push({\n weekday,\n ordinal: match[1] ? Number(match[1]) : null,\n });\n }\n return parsed;\n}\n\nfunction toDate(value: Date | string | undefined): Date | null {\n if (value === undefined || value === null || value === '') return null;\n const date = value instanceof Date ? value : new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new RangeError(`Invalid recurrence until date: ${String(value)}`);\n }\n return date;\n}\n\nfunction monthDays(\n year: number,\n month: number,\n pattern: RecurrencePattern,\n byDay: ParsedByDay[],\n startDay: number,\n): number[] {\n const length = daysInMonth(year, month);\n const firstDayNumber = civilToDayNumber({ year, month, day: 1 });\n let days: number[];\n\n if (pattern.byMonthDay?.length) {\n days = pattern.byMonthDay\n .map((value) => (value < 0 ? length + 1 + value : value))\n .filter((value) => value >= 1 && value <= length);\n } else if (byDay.length) {\n days = [];\n for (const { weekday, ordinal } of byDay) {\n const matches: number[] = [];\n for (let day = 1; day <= length; day++) {\n if (weekdayOf(firstDayNumber + day - 1) === weekday) matches.push(day);\n }\n if (ordinal === null) {\n days.push(...matches);\n } else {\n const index = ordinal > 0 ? ordinal - 1 : matches.length + ordinal;\n if (index >= 0 && index < matches.length) days.push(matches[index]);\n }\n }\n } else {\n // RFC 5545: a month without the start's day (e.g. the 31st) is skipped.\n days = startDay <= length ? [startDay] : [];\n }\n\n days = [...new Set(days)].sort((a, b) => a - b);\n\n if (pattern.bySetPos?.length) {\n const picked = pattern.bySetPos\n .map((pos) => (pos > 0 ? days[pos - 1] : days[days.length + pos]))\n .filter((day): day is number => day !== undefined);\n days = [...new Set(picked)].sort((a, b) => a - b);\n }\n\n return days;\n}\n\n/**\n * Candidate local dates (as day numbers) for one period of the pattern.\n */\nfunction periodDates(\n pattern: RecurrencePattern,\n period: number,\n first: CivilDate,\n firstDayNumber: number,\n byDay: ParsedByDay[],\n): number[] {\n const interval = Math.max(1, Math.floor(pattern.interval ?? 1));\n\n switch (pattern.frequency) {\n case 'daily':\n return [firstDayNumber + period * interval];\n case 'weekly': {\n // Weeks start on Monday (RRULE WKST default).\n const weekStart =\n firstDayNumber -\n ((weekdayOf(firstDayNumber) + 6) % 7) +\n period * interval * 7;\n const weekdays = byDay.length\n ? [...new Set(byDay.map((entry) => entry.weekday))]\n : [weekdayOf(firstDayNumber)];\n return weekdays\n .map((weekday) => weekStart + ((weekday + 6) % 7))\n .sort((a, b) => a - b);\n }\n case 'monthly': {\n const monthIndex = first.month - 1 + period * interval;\n const year = first.year + Math.floor(monthIndex / 12);\n const month = (monthIndex % 12) + 1;\n const base = civilToDayNumber({ year, month, day: 1 }) - 1;\n return monthDays(year, month, pattern, byDay, first.day).map(\n (day) => base + day,\n );\n }\n case 'yearly': {\n const year = first.year + period * interval;\n const months = pattern.byMonth?.length\n ? [...new Set(pattern.byMonth)].sort((a, b) => a - b)\n : [first.month];\n const dates: number[] = [];\n for (const month of months) {\n if (month < 1 || month > 12) continue;\n if (pattern.byMonthDay?.length || byDay.length) {\n const base = civilToDayNumber({ year, month, day: 1 }) - 1;\n dates.push(\n ...monthDays(year, month, pattern, byDay, first.day).map(\n (day) => base + day,\n ),\n );\n } else if (first.day <= daysInMonth(year, month)) {\n dates.push(civilToDayNumber({ year, month, day: first.day }));\n }\n }\n return dates.sort((a, b) => a - b);\n }\n default:\n throw new RangeError(\n `Unsupported recurrence frequency: ${String(pattern.frequency)}`,\n );\n }\n}\n\n/**\n * Expand a recurrence pattern into occurrence start instants inside a window.\n *\n * `count` counts from the first occurrence (`start`), including occurrences\n * that fall before `rangeStart`; `until` is inclusive. Occurrences before\n * `start` are never produced, and a `start` that does not itself match the\n * pattern is not an occurrence (as in rrule.js, unlike a bare RFC 5545\n * `DTSTART`). Invalid zones or `byDay` codes throw\n * `RangeError`.\n *\n * @param pattern - Recurrence pattern (object or stored JSON string)\n * @param options - First occurrence, window, and time zone\n * @returns Occurrence start instants in ascending order\n *\n * @example\n * ```ts\n * // Every second Tuesday at 19:00 Edmonton time, for October 2026\n * expandRecurrence(\n * { frequency: 'monthly', byDay: ['2TU'] },\n * {\n * start: new Date('2026-01-14T02:00:00Z'),\n * rangeStart: new Date('2026-10-01T00:00:00Z'),\n * rangeEnd: new Date('2026-10-31T23:59:59Z'),\n * timeZone: 'America/Edmonton',\n * },\n * );\n * ```\n */\nexport function expandRecurrence(\n pattern: RecurrencePattern | string | null | undefined,\n options: ExpandRecurrenceOptions,\n): Date[] {\n const parsed: RecurrencePattern | null =\n typeof pattern === 'string'\n ? pattern\n ? (JSON.parse(pattern) as RecurrencePattern)\n : null\n : (pattern ?? null);\n if (!parsed) return [];\n\n const timeZone = options.timeZone || parsed.timeZone || 'UTC';\n const limit = options.limit ?? 1000;\n const startMs = options.start.getTime();\n const rangeStartMs = options.rangeStart.getTime();\n const rangeEndMs = options.rangeEnd.getTime();\n if (Number.isNaN(startMs) || Number.isNaN(rangeStartMs)) {\n throw new RangeError('expandRecurrence needs valid start and range dates');\n }\n if (rangeEndMs < rangeStartMs) return [];\n\n const until = toDate(parsed.until as Date | string | undefined);\n const untilMs = until ? until.getTime() : Number.POSITIVE_INFINITY;\n const count = parsed.count ?? Number.POSITIVE_INFINITY;\n const byDay = parseByDay(parsed.byDay);\n\n const wall = toZonedWallTime(options.start, timeZone);\n const first: CivilDate = {\n year: wall.year,\n month: wall.month,\n day: wall.day,\n };\n const firstDayNumber = civilToDayNumber(first);\n\n const results: Date[] = [];\n let produced = 0;\n\n for (let period = 0; period < MAX_PERIODS; period++) {\n const dates = periodDates(parsed, period, first, firstDayNumber, byDay);\n for (const dayNumber of dates) {\n if (dayNumber < firstDayNumber) continue;\n const civil = dayNumberToCivil(dayNumber);\n const occurrence = fromZonedWallTime(\n {\n ...civil,\n hour: wall.hour,\n minute: wall.minute,\n second: wall.second,\n millisecond: wall.millisecond,\n },\n timeZone,\n );\n const occurrenceMs = occurrence.getTime();\n if (occurrenceMs < startMs) continue;\n if (occurrenceMs > untilMs || produced >= count) return results;\n if (occurrenceMs > rangeEndMs) return results;\n produced++;\n if (occurrenceMs >= rangeStartMs) {\n results.push(occurrence);\n if (results.length >= limit) return results;\n }\n }\n }\n\n return results;\n}\n","/**\n * Utility functions for @have/events package\n */\n\nimport type { EventStatus, RecurrencePattern } from './types';\n\nexport type {\n CivilDate,\n ExpandRecurrenceOptions,\n ZonedWallTime,\n} from './recurrence';\nexport {\n expandRecurrence,\n fromZonedWallTime,\n isValidTimeZone,\n toZonedWallTime,\n} from './recurrence';\n\n/**\n * Validate event status transition\n *\n * @param currentStatus - Current event status\n * @param newStatus - Proposed new status\n * @returns True if transition is valid\n */\nexport function validateEventStatus(\n currentStatus: EventStatus,\n newStatus: EventStatus,\n): boolean {\n // Define valid transitions\n const validTransitions: Record<EventStatus, EventStatus[]> = {\n scheduled: ['in_progress', 'cancelled', 'postponed'],\n in_progress: ['completed', 'cancelled'],\n completed: [], // Cannot transition from completed\n cancelled: ['scheduled'], // Can reschedule\n postponed: ['scheduled', 'cancelled'],\n };\n\n return validTransitions[currentStatus]?.includes(newStatus) || false;\n}\n\n/**\n * Format event date range as string\n *\n * @param startDate - Event start date\n * @param endDate - Event end date (optional)\n * @returns Formatted date range string\n */\nexport function formatEventDateRange(\n startDate: Date,\n endDate?: Date | null,\n): string {\n const start = startDate.toLocaleDateString();\n\n if (!endDate) {\n return start;\n }\n\n // Same day\n if (startDate.toDateString() === endDate.toDateString()) {\n return `${start} ${startDate.toLocaleTimeString()} - ${endDate.toLocaleTimeString()}`;\n }\n\n // Multi-day\n return `${start} - ${endDate.toLocaleDateString()}`;\n}\n\n/**\n * Generate unique slug for an event\n *\n * @param name - Event name\n * @param date - Event date\n * @returns URL-friendly slug\n */\nexport function generateEventSlug(name: string, date: Date): string {\n const namePart = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n\n const datePart = date.toISOString().split('T')[0]; // YYYY-MM-DD\n\n return `${namePart}-${datePart}`;\n}\n\n/**\n * Check if events have scheduling conflict\n *\n * @param event1Start - First event start\n * @param event1End - First event end\n * @param event2Start - Second event start\n * @param event2End - Second event end\n * @returns True if events overlap\n */\nexport function checkSchedulingConflict(\n event1Start: Date,\n event1End: Date | null,\n event2Start: Date,\n event2End: Date | null,\n): boolean {\n // If either event has no end date, assume it's instantaneous\n const e1End = event1End || event1Start;\n const e2End = event2End || event2Start;\n\n // Check for overlap\n return event1Start < e2End && e1End > event2Start;\n}\n\n/**\n * Parse recurrence pattern from various formats\n *\n * @param pattern - Recurrence pattern (object or string)\n * @returns Parsed RecurrencePattern or null\n */\nexport function parseRecurrencePattern(\n pattern: RecurrencePattern | string | null,\n): RecurrencePattern | null {\n if (!pattern) return null;\n\n if (typeof pattern === 'string') {\n try {\n return JSON.parse(pattern) as RecurrencePattern;\n } catch {\n return null;\n }\n }\n\n return pattern;\n}\n\n/**\n * Calculate next occurrence for a recurring event\n *\n * @param pattern - Recurrence pattern\n * @param fromDate - Date to calculate from\n * @returns Next occurrence date or null\n */\nexport function calculateNextOccurrence(\n pattern: RecurrencePattern,\n fromDate: Date,\n): Date | null {\n const { frequency, interval = 1, until, count } = pattern;\n\n // Check if recurrence has ended\n if (until && fromDate >= until) return null;\n\n // Calculate next date based on frequency\n const nextDate = new Date(fromDate);\n\n switch (frequency) {\n case 'daily':\n nextDate.setDate(nextDate.getDate() + interval);\n break;\n case 'weekly':\n nextDate.setDate(nextDate.getDate() + interval * 7);\n break;\n case 'monthly':\n nextDate.setMonth(nextDate.getMonth() + interval);\n break;\n case 'yearly':\n nextDate.setFullYear(nextDate.getFullYear() + interval);\n break;\n default:\n return null;\n }\n\n // Check if we've exceeded until date\n if (until && nextDate > until) return null;\n\n return nextDate;\n}\n\n/**\n * Calculate duration between two dates\n *\n * @param startDate - Start date\n * @param endDate - End date\n * @returns Duration in milliseconds\n */\nexport function calculateDuration(startDate: Date, endDate: Date): number {\n return endDate.getTime() - startDate.getTime();\n}\n\n/**\n * Format duration as human-readable string\n *\n * @param durationMs - Duration in milliseconds\n * @returns Formatted duration string\n */\nexport function formatDuration(durationMs: number): string {\n const seconds = Math.floor(durationMs / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n\n if (days > 0) {\n return `${days}d ${hours % 24}h`;\n }\n if (hours > 0) {\n return `${hours}h ${minutes % 60}m`;\n }\n if (minutes > 0) {\n return `${minutes}m ${seconds % 60}s`;\n }\n return `${seconds}s`;\n}\n\n/**\n * Check if an event is happening now\n *\n * @param startDate - Event start date\n * @param endDate - Event end date (optional)\n * @returns True if event is currently in progress\n */\nexport function isEventNow(startDate: Date, endDate?: Date | null): boolean {\n const now = new Date();\n\n if (now < startDate) return false;\n if (endDate && now > endDate) return false;\n\n return true;\n}\n\n/**\n * Get event status based on dates\n *\n * @param startDate - Event start date\n * @param endDate - Event end date (optional)\n * @param currentStatus - Current status (optional)\n * @returns Suggested event status\n */\nexport function getEventStatusFromDates(\n startDate: Date,\n endDate?: Date | null,\n currentStatus?: EventStatus,\n): EventStatus {\n // Don't override cancelled or postponed status\n if (currentStatus === 'cancelled' || currentStatus === 'postponed') {\n return currentStatus;\n }\n\n const now = new Date();\n\n if (now < startDate) {\n return 'scheduled';\n }\n\n if (endDate && now > endDate) {\n return 'completed';\n }\n\n return 'in_progress';\n}\n\n/**\n * Sort events by start date\n *\n * @param events - Array of objects with startDate property\n * @param ascending - Sort ascending (default: true)\n * @returns Sorted array\n */\nexport function sortEventsByDate<T extends { startDate: Date | null }>(\n events: T[],\n ascending: boolean = true,\n): T[] {\n return events.sort((a, b) => {\n if (!a.startDate && !b.startDate) return 0;\n if (!a.startDate) return 1;\n if (!b.startDate) return -1;\n\n const diff = a.startDate.getTime() - b.startDate.getTime();\n return ascending ? diff : -diff;\n });\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -17,7 +17,8 @@ export { EventAsset } from './models/EventAsset';
17
17
  export { EventParticipant } from './models/EventParticipant';
18
18
  export { EventSeries } from './models/EventSeries';
19
19
  export { EventType } from './models/EventType';
20
+ export type { CivilDate, ExpandRecurrenceOptions, ZonedWallTime, } from './recurrence';
20
21
  export type { EventOptions, EventParticipantOptions, EventSearchFilters, EventSeriesOptions, EventSeriesSearchFilters, EventStatus, EventTypeOptions, ParticipantRole, ParticipantSearchFilters, RecurrenceFrequency, RecurrencePattern, } from './types';
21
22
  export { EVENTS_MODULE_META, EVENTS_UI_SLOTS } from './ui';
22
- export { calculateDuration, calculateNextOccurrence, checkSchedulingConflict, formatDuration, formatEventDateRange, generateEventSlug, getEventStatusFromDates, isEventNow, parseRecurrencePattern, sortEventsByDate, validateEventStatus, } from './utils';
23
+ export { calculateDuration, calculateNextOccurrence, checkSchedulingConflict, expandRecurrence, formatDuration, formatEventDateRange, fromZonedWallTime, generateEventSlug, getEventStatusFromDates, isEventNow, isValidTimeZone, parseRecurrencePattern, sortEventsByDate, toZonedWallTime, validateEventStatus, } from './utils';
23
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG/C,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,SAAS,CAAC"}