calendaryjs 0.2.3 → 0.3.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/dist/index.js CHANGED
@@ -84,6 +84,14 @@ function getYearsInRange(from, to) {
84
84
  function createDate(year, month, day) {
85
85
  return new Date(year, month - 1, day);
86
86
  }
87
+ function getDaysInMonth(year, month) {
88
+ return new Date(year, month, 0).getDate();
89
+ }
90
+ function resolveDayOfMonth(day, year, month) {
91
+ if (day >= 0) return day;
92
+ const resolved = getDaysInMonth(year, month) + day + 1;
93
+ return resolved >= 1 ? resolved : null;
94
+ }
87
95
  function addDays(date, days) {
88
96
  const result = new Date(date);
89
97
  result.setDate(result.getDate() + days);
@@ -125,7 +133,7 @@ var constEventHandler = {
125
133
  validate(event) {
126
134
  if (typeof event !== "object" || event === null) return false;
127
135
  const e = event;
128
- return e.type === "const" && typeof e.month === "number" && e.month >= 1 && e.month <= 12 && typeof e.day === "number" && e.day >= 1 && e.day <= 31 && typeof e.title === "string" && typeof e.id === "string";
136
+ return e.type === "const" && typeof e.month === "number" && e.month >= 1 && e.month <= 12 && typeof e.day === "number" && e.day >= -31 && e.day <= 31 && e.day !== 0 && typeof e.title === "string" && typeof e.id === "string";
129
137
  },
130
138
  generate(event, year) {
131
139
  if (event.startYear !== void 0 && year < event.startYear) {
@@ -137,7 +145,9 @@ var constEventHandler = {
137
145
  if (event.excludeYears?.includes(year)) {
138
146
  return [];
139
147
  }
140
- return [createDate(year, event.month, event.day)];
148
+ const day = resolveDayOfMonth(event.day, year, event.month);
149
+ if (day === null) return [];
150
+ return [createDate(year, event.month, day)];
141
151
  }
142
152
  };
143
153
 
@@ -157,14 +167,11 @@ var fixedEventHandler = {
157
167
  };
158
168
 
159
169
  // src/generators/monthly.ts
160
- function getDaysInMonth(year, month) {
161
- return new Date(year, month, 0).getDate();
162
- }
163
170
  var monthlyEventHandler = {
164
171
  validate(event) {
165
172
  if (typeof event !== "object" || event === null) return false;
166
173
  const e = event;
167
- return e.type === "monthly" && typeof e.day === "number" && e.day >= 1 && e.day <= 31 && typeof e.title === "string" && typeof e.id === "string";
174
+ return e.type === "monthly" && typeof e.day === "number" && e.day >= -31 && e.day <= 31 && e.day !== 0 && typeof e.title === "string" && typeof e.id === "string";
168
175
  },
169
176
  generate(event, year) {
170
177
  const dates = [];
@@ -182,10 +189,11 @@ var monthlyEventHandler = {
182
189
  continue;
183
190
  }
184
191
  const daysInMonth = getDaysInMonth(year, month);
185
- if (event.day > daysInMonth) {
192
+ const day = resolveDayOfMonth(event.day, year, month);
193
+ if (day === null || day > daysInMonth) {
186
194
  continue;
187
195
  }
188
- const date = createDate(year, month, event.day);
196
+ const date = createDate(year, month, day);
189
197
  const dateStr = formatDate(date);
190
198
  if (event.excludeDates?.includes(dateStr)) {
191
199
  continue;
@@ -263,6 +271,49 @@ var weeklyEventHandler = {
263
271
  }
264
272
  };
265
273
 
274
+ // src/generators/daily.ts
275
+ var PHASE_EPOCH = createDate(1970, 1, 1);
276
+ var dailyEventHandler = {
277
+ validate(event) {
278
+ if (typeof event !== "object" || event === null) return false;
279
+ const e = event;
280
+ if (e.type !== "daily") return false;
281
+ if (typeof e.title !== "string" || typeof e.id !== "string") return false;
282
+ if (e.interval !== void 0 && (typeof e.interval !== "number" || e.interval < 1))
283
+ return false;
284
+ return true;
285
+ },
286
+ generate(event, year) {
287
+ const interval = event.interval ?? 1;
288
+ const yearEnd = createDate(year, 12, 31);
289
+ const startLimit = event.startDate ? parseDate(event.startDate) : null;
290
+ const endLimit = event.endDate ? parseDate(event.endDate) : null;
291
+ const anchor = startLimit ?? PHASE_EPOCH;
292
+ const dates = [];
293
+ let current = createDate(year, 1, 1);
294
+ while (current <= yearEnd) {
295
+ if (startLimit && current < startLimit) {
296
+ current = addDays(current, 1);
297
+ continue;
298
+ }
299
+ if (endLimit && current > endLimit) break;
300
+ if (interval > 1) {
301
+ const offset = daysBetween(anchor, current);
302
+ if (offset % interval !== 0) {
303
+ current = addDays(current, 1);
304
+ continue;
305
+ }
306
+ }
307
+ const dateStr = formatDate(current);
308
+ if (!event.excludeDates?.includes(dateStr)) {
309
+ dates.push(new Date(current));
310
+ }
311
+ current = addDays(current, 1);
312
+ }
313
+ return dates;
314
+ }
315
+ };
316
+
266
317
  // src/generators/nth-weekday.ts
267
318
  function nthWeekdayInMonth(year, month, dayOfWeek, nth) {
268
319
  if (nth === -1) {
@@ -305,7 +356,7 @@ var nthWeekdayEventHandler = {
305
356
  const hasAnchored = e.after !== void 0;
306
357
  if (hasSimple && !hasAnchored) {
307
358
  if (typeof e.nth !== "number" || ![1, 2, 3, 4, -1].includes(e.nth)) return false;
308
- if (typeof e.month !== "number" || e.month < 1 || e.month > 12)
359
+ if (e.month !== void 0 && (typeof e.month !== "number" || e.month < 1 || e.month > 12))
309
360
  return false;
310
361
  } else if (!hasAnchored) {
311
362
  return false;
@@ -329,9 +380,17 @@ var nthWeekdayEventHandler = {
329
380
  }
330
381
  return candidate ? [candidate] : [];
331
382
  }
332
- if (event.nth !== void 0 && event.month !== void 0) {
333
- const date = nthWeekdayInMonth(year, event.month, event.dayOfWeek, event.nth);
334
- return date ? [date] : [];
383
+ if (event.nth !== void 0) {
384
+ if (event.month !== void 0) {
385
+ const date = nthWeekdayInMonth(year, event.month, event.dayOfWeek, event.nth);
386
+ return date ? [date] : [];
387
+ }
388
+ const dates = [];
389
+ for (let month = 1; month <= 12; month++) {
390
+ const date = nthWeekdayInMonth(year, month, event.dayOfWeek, event.nth);
391
+ if (date) dates.push(date);
392
+ }
393
+ return dates;
335
394
  }
336
395
  return [];
337
396
  }
@@ -648,6 +707,27 @@ var SearchBuilder = class {
648
707
  this.options.hasAllCategories = categories;
649
708
  return this;
650
709
  }
710
+ /**
711
+ * Filter by event type(s) — match any (e.g. `"weekly"`, `"daily"`, a plugin type).
712
+ */
713
+ type(...types) {
714
+ this.options.types = types;
715
+ return this;
716
+ }
717
+ /**
718
+ * Filter by status — match any (e.g. hide cancelled by listing the ones you want).
719
+ */
720
+ status(...status) {
721
+ this.options.status = status;
722
+ return this;
723
+ }
724
+ /**
725
+ * Filter by source — match any (the plugin / feed / subscription that produced it).
726
+ */
727
+ source(...source) {
728
+ this.options.source = source;
729
+ return this;
730
+ }
651
731
  /**
652
732
  * Search by metadata key-value pairs.
653
733
  * Supports nested objects and case-insensitive string matching.
@@ -895,6 +975,15 @@ function executeSearch(events, options) {
895
975
  return options.hasAllCategories.every((cat) => e.categories.includes(cat));
896
976
  });
897
977
  }
978
+ if (options.types && options.types.length > 0) {
979
+ results = results.filter((e) => options.types.includes(e.type));
980
+ }
981
+ if (options.status && options.status.length > 0) {
982
+ results = results.filter((e) => e.status !== void 0 && options.status.includes(e.status));
983
+ }
984
+ if (options.source && options.source.length > 0) {
985
+ results = results.filter((e) => e.source !== void 0 && options.source.includes(e.source));
986
+ }
898
987
  if (options.metadata) {
899
988
  results = results.filter((e) => matchesMetadata(e.metadata, options.metadata));
900
989
  }
@@ -988,6 +1077,7 @@ var CalendaryInstance = class _CalendaryInstance {
988
1077
  this.eventTypeHandlers.set("fixed", fixedEventHandler);
989
1078
  this.eventTypeHandlers.set("monthly", monthlyEventHandler);
990
1079
  this.eventTypeHandlers.set("weekly", weeklyEventHandler);
1080
+ this.eventTypeHandlers.set("daily", dailyEventHandler);
991
1081
  this.eventTypeHandlers.set("nth-weekday", nthWeekdayEventHandler);
992
1082
  this.eventTypeHandlers.set(
993
1083
  "formula",
@@ -1084,6 +1174,27 @@ var CalendaryInstance = class _CalendaryInstance {
1084
1174
  events: c.events
1085
1175
  });
1086
1176
  }
1177
+ /**
1178
+ * Export events as a portable {@link Collection} — the inverse of {@link load}.
1179
+ * Serializes the plain event configs plus a manifest of the plugins their event
1180
+ * types need, so the result round-trips back through `load()`. Pass it to
1181
+ * `JSON.stringify` for a `.cdy` document.
1182
+ *
1183
+ * @param options.group - export a single group; omit to export every group's events.
1184
+ * @param options.name - the collection name (also the default group id on re-load).
1185
+ * @param options.version - an informational version stamp.
1186
+ */
1187
+ toCollection(options = {}) {
1188
+ const groups = options.group !== void 0 ? [this.groups.get(options.group)].filter((g) => g != null) : [...this.groups.values()];
1189
+ const events = groups.flatMap((g) => g.events);
1190
+ const usedTypes = new Set(events.map((e) => e.type));
1191
+ const plugins = [...this.plugins.values()].filter((p) => Object.keys(p.eventTypes ?? {}).some((t) => usedTypes.has(t))).map((p) => p.name);
1192
+ const out = { events };
1193
+ if (options.name) out.collection = options.name;
1194
+ if (options.version) out.version = options.version;
1195
+ if (plugins.length) out.plugins = plugins;
1196
+ return out;
1197
+ }
1087
1198
  removeGroup(groupId) {
1088
1199
  this.groups.delete(groupId);
1089
1200
  this.dirty = true;
@@ -1104,12 +1215,18 @@ var CalendaryInstance = class _CalendaryInstance {
1104
1215
  return this;
1105
1216
  }
1106
1217
  generateEvents(from, to) {
1218
+ if (this.dirty) {
1219
+ this.cache.invalidate();
1220
+ this.dirty = false;
1221
+ }
1107
1222
  const cacheKey = `${from}:${to}`;
1108
- if (!this.dirty && this.cache.has(cacheKey)) {
1223
+ if (this.cache.has(cacheKey)) {
1109
1224
  return this.cache.get(cacheKey);
1110
1225
  }
1111
1226
  const events = [];
1112
- const years = getYearsInRange(from, to);
1227
+ const inRange = getYearsInRange(from, to);
1228
+ const years = inRange.length ? [inRange[0] - 1, ...inRange, inRange[inRange.length - 1] + 1] : inRange;
1229
+ const seen = /* @__PURE__ */ new Set();
1113
1230
  for (const group of this.groups.values()) {
1114
1231
  if (!group.enabled) continue;
1115
1232
  for (const eventConfig of group.events) {
@@ -1122,6 +1239,10 @@ var CalendaryInstance = class _CalendaryInstance {
1122
1239
  let dateStr = formatDate(date);
1123
1240
  if (directives.startDate && dateStr < directives.startDate) continue;
1124
1241
  if (directives.endDate && dateStr > directives.endDate) continue;
1242
+ const occYear = +dateStr.slice(0, 4);
1243
+ if (directives.startYear !== void 0 && occYear < directives.startYear) continue;
1244
+ if (directives.endYear !== void 0 && occYear > directives.endYear) continue;
1245
+ if (directives.excludeYears?.includes(occYear)) continue;
1125
1246
  const exception = directives.exceptions?.[dateStr];
1126
1247
  if (exception && "skip" in exception) {
1127
1248
  continue;
@@ -1141,6 +1262,13 @@ var CalendaryInstance = class _CalendaryInstance {
1141
1262
  }
1142
1263
  }
1143
1264
  if (!isDateInRange(dateStr, from, to)) continue;
1265
+ const dedupeKey = JSON.stringify([
1266
+ group.id,
1267
+ eventConfig.id,
1268
+ dateStr
1269
+ ]);
1270
+ if (seen.has(dedupeKey)) continue;
1271
+ seen.add(dedupeKey);
1144
1272
  const calendarEvent = this.createCalendarEvent(
1145
1273
  eventConfig,
1146
1274
  group,
@@ -1159,13 +1287,13 @@ var CalendaryInstance = class _CalendaryInstance {
1159
1287
  });
1160
1288
  this.cache.set(cacheKey, events);
1161
1289
  this.index.index(events);
1162
- this.dirty = false;
1163
1290
  return events;
1164
1291
  }
1165
1292
  createCalendarEvent(config, group, dateStr, override) {
1166
1293
  const base = config;
1167
1294
  const event = {
1168
1295
  id: generateEventId(base.id, dateStr),
1296
+ type: base.type,
1169
1297
  sourceEventId: base.id,
1170
1298
  title: base.title,
1171
1299
  date: dateStr,
@@ -1221,13 +1349,16 @@ var CalendaryInstance = class _CalendaryInstance {
1221
1349
  * Get calendar days for a date range
1222
1350
  */
1223
1351
  getDays(options) {
1224
- const { groups } = options;
1352
+ const { groups, types } = options;
1225
1353
  const fromStr = normalizeDateInput(options.from);
1226
1354
  const toStr = normalizeDateInput(options.to);
1227
1355
  let events = this.generateEvents(fromStr, toStr);
1228
1356
  if (groups && groups.length > 0) {
1229
1357
  events = events.filter((e) => groups.includes(e.groupId));
1230
1358
  }
1359
+ if (types && types.length > 0) {
1360
+ events = events.filter((e) => types.includes(e.type));
1361
+ }
1231
1362
  const eventsByDate = /* @__PURE__ */ new Map();
1232
1363
  for (const event of events) {
1233
1364
  const existing = eventsByDate.get(event.date) || [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calendaryjs",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Composable calendar & recurrence engine with pluggable calendar systems.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -38,7 +38,7 @@
38
38
  "typescript"
39
39
  ],
40
40
  "author": "calendaryjs",
41
- "license": "MIT",
41
+ "license": "PolyForm-Noncommercial-1.0.0",
42
42
  "devDependencies": {
43
43
  "typescript": "^5.3.2",
44
44
  "vitest": "^4.0.18",