@c9up/chronos 0.1.3 → 0.1.5

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/native.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Universal engine loader — auto-detects Node (NAPI) vs Browser (WASM).
3
+ *
4
+ * **No fallback.** If neither NAPI nor WASM loads, `nativeChronos()` throws
5
+ * immediately. Chronos operations (timezone, DST, RFC 5545) are too complex
6
+ * for a pure-TS fallback.
7
+ */
8
+ let native;
9
+ let loadError;
10
+ const isNode = typeof globalThis.process !== "undefined" &&
11
+ typeof globalThis.process.versions?.node === "string";
12
+ if (isNode) {
13
+ try {
14
+ const { createRequire } = await import("node:module");
15
+ const { dirname, join } = await import("node:path");
16
+ const { fileURLToPath } = await import("node:url");
17
+ const { arch, platform } = await import("node:process");
18
+ const nodeRequire = createRequire(import.meta.url);
19
+ const currentDir = dirname(fileURLToPath(import.meta.url));
20
+ const platformMap = {
21
+ "linux-x64": "linux-x64-gnu",
22
+ "linux-arm64": "linux-arm64-gnu",
23
+ "darwin-x64": "darwin-x64",
24
+ "darwin-arm64": "darwin-arm64",
25
+ "win32-x64": "win32-x64-msvc",
26
+ };
27
+ const suffix = platformMap[`${platform}-${arch}`];
28
+ if (suffix) {
29
+ native = nodeRequire(join(currentDir, `../index.${suffix}.node`));
30
+ }
31
+ }
32
+ catch (e) {
33
+ loadError = e;
34
+ }
35
+ }
36
+ else {
37
+ try {
38
+ // wasm-bindgen preserves Rust snake_case names (start_of, end_of, etc.)
39
+ // while NAPI-RS auto-converts to camelCase. We adapt the WASM module
40
+ // shape (which uses bigint for large integers) to the NativeChronos
41
+ // interface (number-based), so consumers don't deal with the difference.
42
+ const wasm = await import("../wasm/chronos_engine_wasm.js");
43
+ await wasm.default();
44
+ native = {
45
+ add: (iso, amount, unit) => wasm.add(iso, BigInt(amount), unit),
46
+ diff: (a, b, unit) => Number(wasm.diff(a, b, unit)),
47
+ startOf: (iso, unit) => wasm.start_of(iso, unit),
48
+ endOf: (iso, unit) => wasm.end_of(iso, unit),
49
+ format: (iso, pattern) => wasm.format(iso, pattern),
50
+ validateTimezone: (zone) => wasm.validate_timezone(zone),
51
+ toZone: (iso, zone) => wasm.to_zone(iso, zone),
52
+ addInZone: (iso, amount, unit, zone) => wasm.add_in_zone(iso, BigInt(amount), unit, zone),
53
+ diffInZone: (a, b, unit, zone) => Number(wasm.diff_in_zone(a, b, unit, zone)),
54
+ zoneOffset: (iso, zone) => wasm.zone_offset(iso, zone),
55
+ fromLocal: (naive, zone) => wasm.from_local(naive, zone),
56
+ parseRfc2822: (input) => wasm.parse_rfc2822(input),
57
+ parseSql: (input) => wasm.parse_sql(input),
58
+ parseHttp: (input) => wasm.parse_http(input),
59
+ rruleExpand: (start, rrule, limit) => wasm.rrule_expand(start, rrule, limit),
60
+ calendarParts: (iso) => wasm.calendar_parts(iso),
61
+ };
62
+ }
63
+ catch (e) {
64
+ loadError = e;
65
+ }
66
+ }
67
+ export function nativeChronos() {
68
+ if (!native) {
69
+ throw new Error(`[CHRONOS_ENGINE_REQUIRED] The Chronos engine is required but not loaded.\n` +
70
+ ` Environment: ${isNode ? "Node" : "Browser"}\n` +
71
+ ` Reason: ${loadError ?? "binary not found"}\n` +
72
+ ` Fix (Node): cd packages/chronos && pnpm build:napi\n` +
73
+ ` Fix (Browser): cd packages/chronos && pnpm build:wasm`);
74
+ }
75
+ return native;
76
+ }
77
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ export type RRuleWeekday = "MO" | "TU" | "WE" | "TH" | "FR" | "SA" | "SU";
2
+ export type RRuleByDayToken = RRuleWeekday | `${number}${RRuleWeekday}`;
3
+ export interface RRuleBuild {
4
+ freq: "SECONDLY" | "MINUTELY" | "HOURLY" | "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
5
+ interval?: number;
6
+ wkst?: RRuleWeekday;
7
+ byDay?: RRuleByDayToken[];
8
+ byMonthDay?: number[];
9
+ byMonth?: number[];
10
+ byWeekNo?: number[];
11
+ byYearDay?: number[];
12
+ bySetPos?: number[];
13
+ byHour?: number[];
14
+ byMinute?: number[];
15
+ bySecond?: number[];
16
+ count?: number;
17
+ until?: string;
18
+ }
19
+ export declare function toRRuleString(rule: RRuleBuild): string;
20
+ export declare function expandRRule(startIso: string, rrule: string | RRuleBuild, limit?: number): string[];
21
+ //# sourceMappingURL=rrule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rrule.d.ts","sourceRoot":"","sources":["../src/rrule.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC1E,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,GAAG,MAAM,GAAG,YAAY,EAAE,CAAC;AAExE,MAAM,WAAW,UAAU;IAC1B,IAAI,EACD,UAAU,GACV,UAAU,GACV,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAWD,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA6BtD;AAgBD,wBAAgB,WAAW,CAC1B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GAAG,UAAU,EAC1B,KAAK,SAAM,GACT,MAAM,EAAE,CAcV"}
package/dist/rrule.js ADDED
@@ -0,0 +1,58 @@
1
+ import { nativeChronos } from "./native.js";
2
+ function pushNumericList(parts, key, values) {
3
+ if (!values || values.length === 0)
4
+ return;
5
+ parts.push(`${key}=${values.join(",")}`);
6
+ }
7
+ export function toRRuleString(rule) {
8
+ const parts = [`FREQ=${rule.freq}`];
9
+ if (rule.interval && rule.interval !== 1)
10
+ parts.push(`INTERVAL=${rule.interval}`);
11
+ if (rule.wkst)
12
+ parts.push(`WKST=${rule.wkst}`);
13
+ if (rule.byDay?.length)
14
+ parts.push(`BYDAY=${rule.byDay.join(",")}`);
15
+ pushNumericList(parts, "BYMONTHDAY", rule.byMonthDay);
16
+ pushNumericList(parts, "BYMONTH", rule.byMonth);
17
+ pushNumericList(parts, "BYWEEKNO", rule.byWeekNo);
18
+ pushNumericList(parts, "BYYEARDAY", rule.byYearDay);
19
+ pushNumericList(parts, "BYSETPOS", rule.bySetPos);
20
+ pushNumericList(parts, "BYHOUR", rule.byHour);
21
+ pushNumericList(parts, "BYMINUTE", rule.byMinute);
22
+ pushNumericList(parts, "BYSECOND", rule.bySecond);
23
+ if (rule.count !== undefined)
24
+ parts.push(`COUNT=${rule.count}`);
25
+ if (rule.until) {
26
+ // RFC 5545 §3.3.10 UNTIL format: YYYYMMDDTHHMMSSZ (no hyphens/colons/ms).
27
+ parts.push(`UNTIL=${new Date(rule.until)
28
+ .toISOString()
29
+ .replace(/[-:]/g, "")
30
+ .replace(/\.\d+Z$/, "Z")}`);
31
+ }
32
+ return parts.join(";");
33
+ }
34
+ /**
35
+ * Expand an RRULE into concrete occurrence dates. **NAPI-only** — the Rust
36
+ * engine handles the full RFC 5545 spec (BYDAY, BYSETPOS, BYMONTHDAY,
37
+ * BYWEEKNO, BYYEARDAY, UNTIL, COUNT, INTERVAL, WKST). No TS fallback.
38
+ */
39
+ /**
40
+ * Hard cap on `expandRRule(... , limit)` so a caller passing an absurd
41
+ * number (or one derived from user input) can't ask the Rust engine to
42
+ * allocate gigabytes / run for seconds. 10_000 covers any realistic
43
+ * pagination + cushion (a year of every-minute occurrences = ~525k, so
44
+ * apps wanting that should iterate or stream, not call once).
45
+ */
46
+ const MAX_RRULE_EXPAND = 10_000;
47
+ export function expandRRule(startIso, rrule, limit = 100) {
48
+ if (!Number.isInteger(limit) || limit < 1) {
49
+ throw new RangeError(`expandRRule(limit) must be a positive integer (got ${String(limit)})`);
50
+ }
51
+ if (limit > MAX_RRULE_EXPAND) {
52
+ throw new RangeError(`expandRRule(limit=${limit}) exceeds the safety cap of ${MAX_RRULE_EXPAND}. ` +
53
+ "Page through the result set instead of asking for everything at once.");
54
+ }
55
+ const built = typeof rrule === "string" ? rrule : toRRuleString(rrule);
56
+ return nativeChronos().rruleExpand(startIso, built, limit);
57
+ }
58
+ //# sourceMappingURL=rrule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rrule.js","sourceRoot":"","sources":["../src/rrule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared helpers for DateTime and RRule modules.
3
+ */
4
+ /**
5
+ * Normalize an ISO 8601 string: strip the `.000` when subseconds are zero
6
+ * for compact output that matches `new Date(iso).toISOString().replace('.000Z', 'Z')`.
7
+ * When subseconds are non-zero, preserve them so precision isn't silently lost.
8
+ */
9
+ export declare function normalizeIso(iso: string): string;
10
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD"}
package/dist/utils.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared helpers for DateTime and RRule modules.
3
+ */
4
+ /**
5
+ * Normalize an ISO 8601 string: strip the `.000` when subseconds are zero
6
+ * for compact output that matches `new Date(iso).toISOString().replace('.000Z', 'Z')`.
7
+ * When subseconds are non-zero, preserve them so precision isn't silently lost.
8
+ */
9
+ export function normalizeIso(iso) {
10
+ return iso.replace(".000Z", "Z");
11
+ }
12
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACvC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC"}
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/chronos",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Chronos — advanced date/time and recurrence engine (TypeScript + Rust N-API)",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/Duration.ts CHANGED
@@ -84,14 +84,18 @@ export class Duration {
84
84
  * that cannot be mixed with other designators per the spec.
85
85
  */
86
86
  static fromISO(iso: string): Duration {
87
- const s = iso.trim();
87
+ const trimmed = iso.trim();
88
+ // A leading '-' marks a negative duration (the extension toISO emits).
89
+ const negative = trimmed.startsWith("-P");
90
+ const s = negative ? trimmed.slice(1) : trimmed;
88
91
  if (!s.startsWith("P"))
89
92
  throw new Error(`Invalid ISO 8601 duration: ${iso}`);
93
+ const apply = (d: Duration): Duration => (negative ? d.negate() : d);
90
94
 
91
95
  // Week form: P3W
92
96
  const weekMatch = /^P(\d+(?:\.\d+)?)W$/.exec(s);
93
97
  if (weekMatch) {
94
- return new Duration({ weeks: Number.parseFloat(weekMatch[1]) });
98
+ return apply(new Duration({ weeks: Number.parseFloat(weekMatch[1]) }));
95
99
  }
96
100
 
97
101
  const match =
@@ -101,19 +105,26 @@ export class Duration {
101
105
  if (!match) throw new Error(`Invalid ISO 8601 duration: ${iso}`);
102
106
 
103
107
  const [, y, mo, d, h, mi, sec] = match;
108
+ // The all-optional regex also matches bare "P"/"PT" — a duration needs at
109
+ // least one component. An explicit zero ("PT0S", "P0D") still parses.
110
+ if (!y && !mo && !d && !h && !mi && !sec) {
111
+ throw new Error(`Invalid ISO 8601 duration: ${iso}`);
112
+ }
104
113
  const seconds = sec ? Math.floor(Number.parseFloat(sec)) : 0;
105
114
  const milliseconds = sec
106
115
  ? Math.round((Number.parseFloat(sec) - seconds) * 1000)
107
116
  : 0;
108
- return new Duration({
109
- years: y ? Number(y) : 0,
110
- months: mo ? Number(mo) : 0,
111
- days: d ? Number(d) : 0,
112
- hours: h ? Number(h) : 0,
113
- minutes: mi ? Number(mi) : 0,
114
- seconds,
115
- milliseconds,
116
- });
117
+ return apply(
118
+ new Duration({
119
+ years: y ? Number(y) : 0,
120
+ months: mo ? Number(mo) : 0,
121
+ days: d ? Number(d) : 0,
122
+ hours: h ? Number(h) : 0,
123
+ minutes: mi ? Number(mi) : 0,
124
+ seconds,
125
+ milliseconds,
126
+ }),
127
+ );
117
128
  }
118
129
 
119
130
  // ─── Accessors ──────────────────────────────────────────
@@ -297,42 +308,102 @@ export class Duration {
297
308
 
298
309
  // ─── Formatting ─────────────────────────────────────────
299
310
 
300
- /** ISO 8601 duration string (`P1Y2M3DT4H5M6S`). */
311
+ /**
312
+ * ISO 8601 duration string (`P1Y2M3DT4H5M6S`). Always round-trips through
313
+ * {@link fromISO}.
314
+ *
315
+ * Two spec constraints are honoured: the week form (`PnW`) cannot mix with
316
+ * other designators — so weeks are emitted alone as `PnW`, otherwise folded
317
+ * into days; and durations are non-negative — a uniformly-negative duration
318
+ * (e.g. from {@link negate}) is emitted with a single leading `-` over
319
+ * absolute components, which `fromISO` parses back.
320
+ */
301
321
  toISO(): string {
302
322
  const v = this.#values;
323
+ const units = ORDERED_UNITS.map((u) => v[u]);
324
+ const negative = units.some((n) => n < 0) && units.every((n) => n <= 0);
325
+ const sign = negative ? "-" : "";
326
+ const a = (n: number): number => (negative ? Math.abs(n) : n);
327
+
328
+ // Week form only when weeks is the sole unit; never combined with others.
329
+ const onlyWeeks =
330
+ v.weeks !== 0 &&
331
+ v.years === 0 &&
332
+ v.months === 0 &&
333
+ v.days === 0 &&
334
+ v.hours === 0 &&
335
+ v.minutes === 0 &&
336
+ v.seconds === 0 &&
337
+ v.milliseconds === 0;
338
+ if (onlyWeeks) return `${sign}P${a(v.weeks)}W`;
339
+
340
+ const days = v.days + v.weeks * 7;
303
341
  let date = "";
304
- if (v.years) date += `${v.years}Y`;
305
- if (v.months) date += `${v.months}M`;
306
- if (v.weeks) date += `${v.weeks}W`;
307
- if (v.days) date += `${v.days}D`;
342
+ if (v.years) date += `${a(v.years)}Y`;
343
+ if (v.months) date += `${a(v.months)}M`;
344
+ if (days) date += `${a(days)}D`;
308
345
  let time = "";
309
- if (v.hours) time += `${v.hours}H`;
310
- if (v.minutes) time += `${v.minutes}M`;
346
+ if (v.hours) time += `${a(v.hours)}H`;
347
+ if (v.minutes) time += `${a(v.minutes)}M`;
311
348
  const totalSec = v.seconds + v.milliseconds / 1000;
312
- if (totalSec) time += `${totalSec}S`;
349
+ if (totalSec) time += `${a(totalSec)}S`;
313
350
  if (!date && !time) return "PT0S";
314
- return `P${date}${time ? `T${time}` : ""}`;
351
+ return `${sign}P${date}${time ? `T${time}` : ""}`;
315
352
  }
316
353
 
317
354
  /**
318
355
  * Simple pattern format: `hh:mm:ss`, `HH:mm`, etc. Supported tokens:
319
356
  * `Y` (years), `M` (months), `d` (days), `h`/`hh` (hours), `m`/`mm`
320
357
  * (minutes), `s`/`ss` (seconds), `S`/`SSS` (milliseconds).
358
+ *
359
+ * Wrap literal text in square brackets so its letters aren't read as tokens:
360
+ * `"h [hours], m [min]"` → `"2 hours, 30 min"`. A single left-to-right scan
361
+ * (longest token first) means substituted values are never re-scanned and
362
+ * literal/overlapping text is left intact.
321
363
  */
322
364
  toFormat(pattern: string): string {
323
365
  const v = this.normalize().#values;
324
- return pattern
325
- .replace(/hh/g, String(v.hours).padStart(2, "0"))
326
- .replace(/mm/g, String(v.minutes).padStart(2, "0"))
327
- .replace(/ss/g, String(v.seconds).padStart(2, "0"))
328
- .replace(/SSS/g, String(v.milliseconds).padStart(3, "0"))
329
- .replace(/h/g, String(v.hours))
330
- .replace(/m/g, String(v.minutes))
331
- .replace(/s/g, String(v.seconds))
332
- .replace(/S/g, String(v.milliseconds))
333
- .replace(/Y/g, String(v.years))
334
- .replace(/M/g, String(v.months))
335
- .replace(/d/g, String(v.days));
366
+ const tokens: Record<string, string> = {
367
+ SSS: String(v.milliseconds).padStart(3, "0"),
368
+ hh: String(v.hours).padStart(2, "0"),
369
+ mm: String(v.minutes).padStart(2, "0"),
370
+ ss: String(v.seconds).padStart(2, "0"),
371
+ S: String(v.milliseconds),
372
+ h: String(v.hours),
373
+ m: String(v.minutes),
374
+ s: String(v.seconds),
375
+ Y: String(v.years),
376
+ M: String(v.months),
377
+ d: String(v.days),
378
+ };
379
+ const order = ["SSS", "hh", "mm", "ss", "S", "h", "m", "s", "Y", "M", "d"];
380
+ let out = "";
381
+ let i = 0;
382
+ while (i < pattern.length) {
383
+ if (pattern[i] === "[") {
384
+ i++;
385
+ while (i < pattern.length && pattern[i] !== "]") {
386
+ out += pattern[i];
387
+ i++;
388
+ }
389
+ if (i < pattern.length) i++; // skip the closing ']'
390
+ continue;
391
+ }
392
+ let matched = false;
393
+ for (const key of order) {
394
+ if (pattern.startsWith(key, i)) {
395
+ out += tokens[key];
396
+ i += key.length;
397
+ matched = true;
398
+ break;
399
+ }
400
+ }
401
+ if (!matched) {
402
+ out += pattern[i];
403
+ i++;
404
+ }
405
+ }
406
+ return out;
336
407
  }
337
408
 
338
409
  /**