@ultimat3/time 1.0.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/package.json +35 -0
  4. package/src/business.d.ts +37 -0
  5. package/src/business.d.ts.map +1 -0
  6. package/src/business.js +68 -0
  7. package/src/business.js.map +1 -0
  8. package/src/business.ts +90 -0
  9. package/src/context.d.ts +40 -0
  10. package/src/context.d.ts.map +1 -0
  11. package/src/context.js +61 -0
  12. package/src/context.js.map +1 -0
  13. package/src/context.ts +94 -0
  14. package/src/cron-describe.ts +159 -0
  15. package/src/cron-occurrence.ts +203 -0
  16. package/src/cron-parse.ts +193 -0
  17. package/src/cron.d.ts +60 -0
  18. package/src/cron.d.ts.map +1 -0
  19. package/src/cron.js +390 -0
  20. package/src/cron.js.map +1 -0
  21. package/src/cron.ts +16 -0
  22. package/src/duration.d.ts +32 -0
  23. package/src/duration.d.ts.map +1 -0
  24. package/src/duration.js +134 -0
  25. package/src/duration.js.map +1 -0
  26. package/src/duration.ts +156 -0
  27. package/src/errors.d.ts +26 -0
  28. package/src/errors.d.ts.map +1 -0
  29. package/src/errors.js +78 -0
  30. package/src/errors.js.map +1 -0
  31. package/src/errors.ts +121 -0
  32. package/src/format.d.ts +54 -0
  33. package/src/format.d.ts.map +1 -0
  34. package/src/format.js +133 -0
  35. package/src/format.js.map +1 -0
  36. package/src/format.ts +175 -0
  37. package/src/index.d.ts +12 -0
  38. package/src/index.d.ts.map +1 -0
  39. package/src/index.js +12 -0
  40. package/src/index.js.map +1 -0
  41. package/src/index.ts +137 -0
  42. package/src/instant.d.ts +38 -0
  43. package/src/instant.d.ts.map +1 -0
  44. package/src/instant.js +76 -0
  45. package/src/instant.js.map +1 -0
  46. package/src/instant.ts +94 -0
  47. package/src/schedule.d.ts +24 -0
  48. package/src/schedule.d.ts.map +1 -0
  49. package/src/schedule.js +67 -0
  50. package/src/schedule.js.map +1 -0
  51. package/src/schedule.ts +90 -0
  52. package/src/zoned.d.ts +80 -0
  53. package/src/zoned.d.ts.map +1 -0
  54. package/src/zoned.js +146 -0
  55. package/src/zoned.js.map +1 -0
  56. package/src/zoned.ts +268 -0
  57. package/src/zones.d.ts +40 -0
  58. package/src/zones.d.ts.map +1 -0
  59. package/src/zones.js +116 -0
  60. package/src/zones.js.map +1 -0
  61. package/src/zones.ts +167 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Durations as milliseconds, parsed from a human string.
3
+ * `step.sleep('3d')` in @ultimat3/jobs and every `retry.backoff` value comes through here.
4
+ */
5
+
6
+ import { durationInvalid } from './errors';
7
+
8
+ export const MS = 1;
9
+ export const SECOND = 1000;
10
+ export const MINUTE = 60 * SECOND;
11
+ export const HOUR = 60 * MINUTE;
12
+ export const DAY = 24 * HOUR;
13
+ export const WEEK = 7 * DAY;
14
+
15
+ /** Accepted suffixes. `m` is minutes and `ms` is milliseconds — never months. */
16
+ const UNITS: Readonly<Record<string, number>> = {
17
+ ms: MS,
18
+ s: SECOND,
19
+ sec: SECOND,
20
+ m: MINUTE,
21
+ min: MINUTE,
22
+ h: HOUR,
23
+ hr: HOUR,
24
+ d: DAY,
25
+ w: WEEK,
26
+ };
27
+
28
+ const COMPONENT = /(\d+(?:\.\d+)?)\s*(ms|sec|min|hr|[smhdw])\s*/iy;
29
+ const ISO_8601 = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i;
30
+
31
+ /**
32
+ * `'90s'` → 90000 · `'2h30m'` → 9000000 · `'3d'` → 259200000 · `'PT2H30M'` → 9000000.
33
+ * A bare number is rejected: `sleep(3)` is ambiguous, `sleep('3s')` is not.
34
+ */
35
+ export function parseDuration(input: string): number {
36
+ const value = input.trim();
37
+ if (value === '') throw durationInvalid(input);
38
+
39
+ const negative = value.startsWith('-');
40
+ const body = negative ? value.slice(1) : value;
41
+
42
+ if (/^p/i.test(body)) return (negative ? -1 : 1) * parseIso8601Duration(body, input);
43
+
44
+ COMPONENT.lastIndex = 0;
45
+ let total = 0;
46
+ let matched = 0;
47
+ let match = COMPONENT.exec(body);
48
+ while (match !== null) {
49
+ const amount = Number.parseFloat(match[1] ?? '');
50
+ const unit = (match[2] ?? '').toLowerCase();
51
+ const scale = UNITS[unit];
52
+ if (!Number.isFinite(amount) || scale === undefined) throw durationInvalid(input);
53
+ total += amount * scale;
54
+ matched = COMPONENT.lastIndex;
55
+ match = COMPONENT.exec(body);
56
+ }
57
+ // Sticky matching starts at 0, so anything short of the end is trailing junk, and a
58
+ // zero-length match means there was no unit at all (`'3'` must not mean 3 of anything).
59
+ if (matched === 0 || matched !== body.length) throw durationInvalid(input);
60
+ return (negative ? -1 : 1) * Math.round(total);
61
+ }
62
+
63
+ /** Parse-or-passthrough for APIs that accept either form. */
64
+ export function toMs(duration: string | number): number {
65
+ return typeof duration === 'number' ? duration : parseDuration(duration);
66
+ }
67
+
68
+ export function toSeconds(duration: string | number): number {
69
+ return Math.round(toMs(duration) / SECOND);
70
+ }
71
+
72
+ export interface FormatDurationOptions {
73
+ /** Largest unit count to show: `2h 30m` with 2, `2h` with 1. */
74
+ maxUnits?: number;
75
+ style?: 'long' | 'short' | 'narrow';
76
+ }
77
+
78
+ const FORMAT_UNITS: readonly [string, number][] = [
79
+ ['day', DAY],
80
+ ['hour', HOUR],
81
+ ['minute', MINUTE],
82
+ ['second', SECOND],
83
+ ['millisecond', MS],
84
+ ];
85
+
86
+ /**
87
+ * `formatDuration(9_000_000, 'de-DE')` → `2 Std., 30 Min.`
88
+ * Built from `Intl.NumberFormat` unit style + `Intl.ListFormat`, so unit names and the
89
+ * list separator are both localized. `Intl.DurationFormat` is not yet everywhere.
90
+ */
91
+ export function formatDuration(
92
+ ms: number,
93
+ locale: string,
94
+ options: FormatDurationOptions = {},
95
+ ): string {
96
+ const style = options.style ?? 'short';
97
+ const maxUnits = options.maxUnits ?? 2;
98
+ let remaining = Math.abs(Math.round(ms));
99
+ const pieces: string[] = [];
100
+
101
+ for (const [unit, scale] of FORMAT_UNITS) {
102
+ if (pieces.length >= maxUnits) break;
103
+ const count = Math.floor(remaining / scale);
104
+ if (count === 0) continue;
105
+ remaining -= count * scale;
106
+ pieces.push(
107
+ new Intl.NumberFormat(locale, { style: 'unit', unit, unitDisplay: style }).format(count),
108
+ );
109
+ }
110
+
111
+ if (pieces.length === 0) {
112
+ return new Intl.NumberFormat(locale, {
113
+ style: 'unit',
114
+ unit: 'second',
115
+ unitDisplay: style,
116
+ }).format(0);
117
+ }
118
+
119
+ const joined = new Intl.ListFormat(locale, { style: 'narrow', type: 'unit' }).format(pieces);
120
+ return ms < 0 ? `-${joined}` : joined;
121
+ }
122
+
123
+ /** `PT2H30M` — for OpenAPI schemas and cron metadata. */
124
+ export function formatDurationIso(ms: number): string {
125
+ const total = Math.abs(Math.round(ms));
126
+ const days = Math.floor(total / DAY);
127
+ const hours = Math.floor((total % DAY) / HOUR);
128
+ const minutes = Math.floor((total % HOUR) / MINUTE);
129
+ const seconds = (total % MINUTE) / SECOND;
130
+ const date = days > 0 ? `${days}D` : '';
131
+ const time = [
132
+ hours > 0 ? `${hours}H` : '',
133
+ minutes > 0 ? `${minutes}M` : '',
134
+ seconds > 0 ? `${seconds}S` : '',
135
+ ].join('');
136
+ const body = `${date}${time === '' ? '' : `T${time}`}`;
137
+ return `${ms < 0 ? '-' : ''}P${body === '' ? '0D' : body}`;
138
+ }
139
+
140
+ function parseIso8601Duration(body: string, original: string): number {
141
+ const match = ISO_8601.exec(body);
142
+ if (match === null) throw durationInvalid(original);
143
+ const [, weeks, days, hours, minutes, seconds] = match;
144
+ const total =
145
+ number(weeks) * WEEK +
146
+ number(days) * DAY +
147
+ number(hours) * HOUR +
148
+ number(minutes) * MINUTE +
149
+ number(seconds) * SECOND;
150
+ if (total === 0 && body.toUpperCase() !== 'P0D') throw durationInvalid(original);
151
+ return Math.round(total);
152
+ }
153
+
154
+ function number(value: string | undefined): number {
155
+ return value === undefined ? 0 : Number.parseFloat(value);
156
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The X_* error codes owned by @ultimat3/time.
3
+ * DST ambiguity is a real state of the world, so it gets a code instead of a guess.
4
+ */
5
+ import { UltimateError } from '@ultimat3/core';
6
+ export declare const TIME_ERROR_CODES: readonly ['X_TIMEZONE_INVALID', 'X_CRON_INVALID', 'X_DURATION_INVALID', 'X_DST_AMBIGUOUS', 'X_DST_NONEXISTENT', 'X_INSTANT_INVALID', 'X_SCHEDULE_INVALID'];
7
+ export type TimeErrorCode = (typeof TIME_ERROR_CODES)[number];
8
+ export declare class TimeError extends UltimateError {
9
+ constructor(init: {
10
+ code: TimeErrorCode;
11
+ cause: string;
12
+ fix: string;
13
+ });
14
+ }
15
+ /**
16
+ * A wall-clock field outside its range. Separate from `X_DST_*`, which are about times that
17
+ * are legitimately absent or doubled — this one is a spec the caller got wrong.
18
+ */
19
+ export declare function scheduleInvalid(field: string, value: unknown, range: string): TimeError;
20
+ export declare function timezoneInvalid(zone: string): TimeError;
21
+ export declare function cronInvalid(expression: string, reason: string): TimeError;
22
+ export declare function durationInvalid(input: string): TimeError;
23
+ export declare function dstAmbiguous(wall: string, zone: string): TimeError;
24
+ export declare function dstNonexistent(wall: string, zone: string): TimeError;
25
+ export declare function instantInvalid(input: string): TimeError;
26
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,eAAO,MAAM,gBAAgB,YAC3B,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,CACZ,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,qBAAa,SAAU,SAAQ,aAAa;IAC1C,YAAY,IAAI,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAOpE;CACF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAMvF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAMvD;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,CAMzE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAMxD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAMlE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAMpE;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAMvD"}
package/src/errors.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The X_* error codes owned by @ultimat3/time.
3
+ * DST ambiguity is a real state of the world, so it gets a code instead of a guess.
4
+ */
5
+ import { UltimateError } from '@ultimat3/core';
6
+ export const TIME_ERROR_CODES = [
7
+ 'X_TIMEZONE_INVALID',
8
+ 'X_CRON_INVALID',
9
+ 'X_DURATION_INVALID',
10
+ 'X_DST_AMBIGUOUS',
11
+ 'X_DST_NONEXISTENT',
12
+ 'X_INSTANT_INVALID',
13
+ 'X_SCHEDULE_INVALID',
14
+ ];
15
+ export class TimeError extends UltimateError {
16
+ constructor(init) {
17
+ super({
18
+ code: init.code,
19
+ cause: init.cause,
20
+ fix: init.fix,
21
+ docs: `https://ultimate.dev/errors/${init.code}`,
22
+ });
23
+ }
24
+ }
25
+ /**
26
+ * A wall-clock field outside its range. Separate from `X_DST_*`, which are about times that
27
+ * are legitimately absent or doubled — this one is a spec the caller got wrong.
28
+ */
29
+ export function scheduleInvalid(field, value, range) {
30
+ return new TimeError({
31
+ code: 'X_SCHEDULE_INVALID',
32
+ cause: `${field} must be ${range}, got ${String(value)}`,
33
+ fix: `pass an integer in ${range} for ${field} — wall-clock fields are not wrapped or clamped, because a silently shifted schedule is worse than a failed one`,
34
+ });
35
+ }
36
+ export function timezoneInvalid(zone) {
37
+ return new TimeError({
38
+ code: 'X_TIMEZONE_INVALID',
39
+ cause: `"${zone}" is not an IANA timezone name`,
40
+ fix: 'use an IANA identifier such as Europe/Berlin, America/New_York or UTC — never an abbreviation like CET or a numeric offset',
41
+ });
42
+ }
43
+ export function cronInvalid(expression, reason) {
44
+ return new TimeError({
45
+ code: 'X_CRON_INVALID',
46
+ cause: `cron "${expression}": ${reason}`,
47
+ fix: "use 5 fields (m h dom mon dow) or 6 with seconds, e.g. '0 3 * * *' for 03:00 daily, '*/15 * * * *' every 15 minutes, '0 9 * * MON-FRI' weekday mornings",
48
+ });
49
+ }
50
+ export function durationInvalid(input) {
51
+ return new TimeError({
52
+ code: 'X_DURATION_INVALID',
53
+ cause: `"${input}" is not a duration`,
54
+ fix: "use a unit-suffixed duration: '90s', '2h30m', '3d', '1w', '250ms' — or an ISO-8601 form like 'PT2H30M'",
55
+ });
56
+ }
57
+ export function dstAmbiguous(wall, zone) {
58
+ return new TimeError({
59
+ code: 'X_DST_AMBIGUOUS',
60
+ cause: `${wall} happens twice in ${zone} (the fall-back hour repeats)`,
61
+ fix: "pass { overlap: 'first' } for the pre-transition instant or { overlap: 'second' } for the post-transition one",
62
+ });
63
+ }
64
+ export function dstNonexistent(wall, zone) {
65
+ return new TimeError({
66
+ code: 'X_DST_NONEXISTENT',
67
+ cause: `${wall} never happens in ${zone} (the spring-forward gap skips it)`,
68
+ fix: "pass { gap: 'next' } to shift forward past the gap or { gap: 'previous' } to shift back before it",
69
+ });
70
+ }
71
+ export function instantInvalid(input) {
72
+ return new TimeError({
73
+ code: 'X_INSTANT_INVALID',
74
+ cause: `"${input}" is not a valid instant`,
75
+ fix: 'pass an ISO-8601 timestamp with an offset or Z, e.g. 2026-03-14T09:00:00Z',
76
+ });
77
+ }
78
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,oBAAoB;IACpB,gBAAgB;IAChB,oBAAoB;IACpB,iBAAiB;IACjB,mBAAmB;IACnB,mBAAmB;IACnB,oBAAoB;CACZ,CAAC;AAIX,MAAM,OAAO,SAAU,SAAQ,aAAa;IAC1C,YAAY,IAAyD;QACnE,KAAK,CAAC;YACJ,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,IAAI,EAAE,+BAA+B,IAAI,CAAC,IAAI,EAAE;SACjD,CAAC,CAAC;IACL,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa,EAAE,KAAc,EAAE,KAAa;IAC1E,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,GAAG,KAAK,YAAY,KAAK,SAAS,MAAM,CAAC,KAAK,CAAC,EAAE;QACxD,GAAG,EAAE,sBAAsB,KAAK,QAAQ,KAAK,iHAAiH;KAC/J,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,IAAI,IAAI,gCAAgC;QAC/C,GAAG,EAAE,4HAA4H;KAClI,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,UAAkB,EAAE,MAAc;IAC5D,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,gBAAgB;QACtB,KAAK,EAAE,SAAS,UAAU,MAAM,MAAM,EAAE;QACxC,GAAG,EAAE,yJAAyJ;KAC/J,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,IAAI,KAAK,qBAAqB;QACrC,GAAG,EAAE,wGAAwG;KAC9G,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,IAAY;IACrD,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE,GAAG,IAAI,qBAAqB,IAAI,+BAA+B;QACtE,GAAG,EAAE,+GAA+G;KACrH,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAY;IACvD,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,GAAG,IAAI,qBAAqB,IAAI,oCAAoC;QAC3E,GAAG,EAAE,mGAAmG;KACzG,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,IAAI,SAAS,CAAC;QACnB,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,IAAI,KAAK,0BAA0B;QAC1C,GAAG,EAAE,2EAA2E;KACjF,CAAC,CAAC;AACL,CAAC"}
package/src/errors.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The X_* error codes owned by @ultimat3/time.
3
+ * DST ambiguity is a real state of the world, so it gets a code instead of a guess.
4
+ */
5
+
6
+ import { registerErrorCodes, UltimateError } from '@ultimat3/core';
7
+
8
+ export const TIME_ERROR_CODES = [
9
+ 'X_TIMEZONE_INVALID',
10
+ 'X_CRON_INVALID',
11
+ 'X_DURATION_INVALID',
12
+ 'X_DST_AMBIGUOUS',
13
+ 'X_DST_NONEXISTENT',
14
+ 'X_INSTANT_INVALID',
15
+ 'X_SCHEDULE_INVALID',
16
+ 'X_LOCALE_INVALID',
17
+ ] as const;
18
+
19
+ export type TimeErrorCode = (typeof TIME_ERROR_CODES)[number];
20
+
21
+ export const TIME_ERROR_TITLES: Readonly<Record<TimeErrorCode, string>> = {
22
+ X_TIMEZONE_INVALID: 'not an IANA zone',
23
+ X_CRON_INVALID: 'not a parseable cron expression',
24
+ X_DURATION_INVALID: 'not a parseable duration',
25
+ X_DST_AMBIGUOUS: 'the local time occurs twice',
26
+ X_DST_NONEXISTENT: 'the local time does not exist',
27
+ X_INSTANT_INVALID: 'not a parseable instant',
28
+ X_SCHEDULE_INVALID: 'a wall-clock field is out of range',
29
+ X_LOCALE_INVALID: 'not a well-formed BCP 47 tag',
30
+ };
31
+
32
+ // Titles must be registered for `format()` to render the contract's first line. Every code above is
33
+ // owned here and none is borrowed, so the call is unconditional: a second package claiming one has
34
+ // to fail as X_ERROR_CODE_DUPLICATE, not quietly keep whichever title was registered first.
35
+ registerErrorCodes(
36
+ Object.fromEntries(Object.entries(TIME_ERROR_TITLES).map(([code, title]) => [code, { title }])),
37
+ );
38
+
39
+ export class TimeError extends UltimateError {
40
+ constructor(init: { code: TimeErrorCode; cause: string; fix: string }) {
41
+ super({
42
+ code: init.code,
43
+ cause: init.cause,
44
+ fix: init.fix,
45
+ docs: `https://ultimate.dev/errors/${init.code}`,
46
+ });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * A wall-clock field outside its range. Separate from `X_DST_*`, which are about times that
52
+ * are legitimately absent or doubled — this one is a spec the caller got wrong.
53
+ */
54
+ export function scheduleInvalid(field: string, value: unknown, range: string): TimeError {
55
+ return new TimeError({
56
+ code: 'X_SCHEDULE_INVALID',
57
+ cause: `${field} must be ${range}, got ${String(value)}`,
58
+ fix: `pass an integer in ${range} for ${field} — wall-clock fields are not wrapped or clamped, because a silently shifted schedule is worse than a failed one`,
59
+ });
60
+ }
61
+
62
+ export function timezoneInvalid(zone: string): TimeError {
63
+ return new TimeError({
64
+ code: 'X_TIMEZONE_INVALID',
65
+ cause: `"${zone}" is not an IANA timezone name`,
66
+ fix: 'use an IANA identifier such as Europe/Berlin, America/New_York or UTC — never an abbreviation like CET or a numeric offset',
67
+ });
68
+ }
69
+
70
+ export function cronInvalid(expression: string, reason: string): TimeError {
71
+ return new TimeError({
72
+ code: 'X_CRON_INVALID',
73
+ cause: `cron "${expression}": ${reason}`,
74
+ fix: "use 5 fields (m h dom mon dow) or 6 with seconds, e.g. '0 3 * * *' for 03:00 daily, '*/15 * * * *' every 15 minutes, '0 9 * * MON-FRI' weekday mornings",
75
+ });
76
+ }
77
+
78
+ export function durationInvalid(input: string): TimeError {
79
+ return new TimeError({
80
+ code: 'X_DURATION_INVALID',
81
+ cause: `"${input}" is not a duration`,
82
+ fix: "use a unit-suffixed duration: '90s', '2h30m', '3d', '1w', '250ms' — or an ISO-8601 form like 'PT2H30M'",
83
+ });
84
+ }
85
+
86
+ export function dstAmbiguous(wall: string, zone: string): TimeError {
87
+ return new TimeError({
88
+ code: 'X_DST_AMBIGUOUS',
89
+ cause: `${wall} happens twice in ${zone} (the fall-back hour repeats)`,
90
+ fix: "pass { overlap: 'first' } for the pre-transition instant or { overlap: 'second' } for the post-transition one",
91
+ });
92
+ }
93
+
94
+ export function dstNonexistent(wall: string, zone: string): TimeError {
95
+ return new TimeError({
96
+ code: 'X_DST_NONEXISTENT',
97
+ cause: `${wall} never happens in ${zone} (the spring-forward gap skips it)`,
98
+ fix: "pass { gap: 'next' } to shift forward past the gap or { gap: 'previous' } to shift back before it",
99
+ });
100
+ }
101
+
102
+ /**
103
+ * A tag `Intl` cannot parse. Distinct from i18n's `X_LOCALE_UNSUPPORTED`, which is a
104
+ * well-formed tag outside the app's supported set — this one is not a tag at all, and a raw
105
+ * `RangeError` from a formatter says nothing about which caller supplied it.
106
+ */
107
+ export function localeInvalid(locale: string): TimeError {
108
+ return new TimeError({
109
+ code: 'X_LOCALE_INVALID',
110
+ cause: `"${locale}" is not a well-formed BCP 47 language tag`,
111
+ fix: "pass a tag like 'en', 'en-GB' or 'de-DE' — screen a header-supplied value with Intl.DateTimeFormat.supportedLocalesOf([tag]) before it reaches a formatter",
112
+ });
113
+ }
114
+
115
+ export function instantInvalid(input: string): TimeError {
116
+ return new TimeError({
117
+ code: 'X_INSTANT_INVALID',
118
+ cause: `"${input}" is not a valid instant`,
119
+ fix: 'pass an ISO-8601 timestamp with an offset or Z, e.g. 2026-03-14T09:00:00Z',
120
+ });
121
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `Intl.DateTimeFormat` at the edge. Every function takes an explicit `zone` and
3
+ * `locale` — there is no ambient default and no `toLocaleString()` without options,
4
+ * because "the server's timezone" is never the answer to "what time is it for the user".
5
+ */
6
+ import { type Instant } from './instant';
7
+ import { type TimeZone } from './zones';
8
+ export type DateTimeStyle = 'short' | 'medium' | 'long' | 'full';
9
+ export interface FormatContext {
10
+ locale: string;
11
+ /** IANA zone. Required, always. */
12
+ zone: TimeZone;
13
+ }
14
+ export interface FormatDateTimeOptions extends FormatContext {
15
+ /** Sets both date and time style; `dateStyle`/`timeStyle` override it. */
16
+ style?: DateTimeStyle;
17
+ dateStyle?: DateTimeStyle;
18
+ timeStyle?: DateTimeStyle;
19
+ hour12?: boolean;
20
+ }
21
+ /** `14 Mar 2026, 09:00` in `en-GB` / `Europe/Berlin`. */
22
+ export declare function formatDateTime(at: Instant, options: FormatDateTimeOptions): string;
23
+ export declare function formatDate(at: Instant, options: FormatContext & {
24
+ style?: DateTimeStyle;
25
+ }): string;
26
+ export declare function formatTime(at: Instant, options: FormatContext & {
27
+ style?: DateTimeStyle;
28
+ hour12?: boolean;
29
+ }): string;
30
+ /**
31
+ * `14 Mar 2026, 09:00 (GMT+1)` — the offset made visible.
32
+ * Built with `timeZoneName: 'shortOffset'` + `formatToParts` so the offset is appended
33
+ * in a fixed position instead of wherever the locale pattern happens to put it.
34
+ */
35
+ export declare function formatWithOffset(at: Instant, options: FormatDateTimeOptions): string;
36
+ /** ISO-8601 date parts in a zone, for `<input type="date">` and CSV columns. */
37
+ export declare function formatIsoDate(at: Instant, zone: TimeZone): string;
38
+ export interface FormatRelativeOptions extends Omit<FormatContext, 'zone'> {
39
+ /** The reference point. Pass `now(clock)` — never let this default to a live clock. */
40
+ now: Instant;
41
+ numeric?: 'always' | 'auto';
42
+ style?: 'long' | 'short' | 'narrow';
43
+ }
44
+ /** `in 3 days` / `2 hours ago`, picking the largest unit that fits. */
45
+ export declare function formatRelative(at: Instant, options: FormatRelativeOptions): string;
46
+ /** `14–16 Mar 2026` — one call, so the locale decides how to collapse the range. */
47
+ export declare function formatRange(from: Instant, to: Instant, options: FormatDateTimeOptions): string;
48
+ /**
49
+ * `Intl` renders `November 5, 2011`, never `5th of November`. When a design asks for the
50
+ * ordinal, build it from `Intl.PluralRules` with `type: 'ordinal'` — English-only by
51
+ * nature, which is why it is a helper and not the default date format.
52
+ */
53
+ export declare function ordinal(value: number, locale?: string): string;
54
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAgB,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,0EAA0E;IAC1E,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAQlF;AAED,wBAAgB,UAAU,CACxB,EAAE,EAAE,OAAO,EACX,OAAO,EAAE,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,aAAa,CAAA;CAAE,GACjD,MAAM,CAKR;AAED,wBAAgB,UAAU,CACxB,EAAE,EAAE,OAAO,EACX,OAAO,EAAE,aAAa,GAAG;IAAE,KAAK,CAAC,EAAE,aAAa,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GACnE,MAAM,CAMR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAuBpF;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,CAQjE;AAED,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC;IACxE,uFAAuF;IACvF,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;CACrC;AAYD,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAalF;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAY9F;AAWD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAO,GAAG,MAAM,CAG5D"}
package/src/format.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `Intl.DateTimeFormat` at the edge. Every function takes an explicit `zone` and
3
+ * `locale` — there is no ambient default and no `toLocaleString()` without options,
4
+ * because "the server's timezone" is never the answer to "what time is it for the user".
5
+ */
6
+ import { differenceMs } from './instant';
7
+ import { assertTimeZone } from './zones';
8
+ /** `14 Mar 2026, 09:00` in `en-GB` / `Europe/Berlin`. */
9
+ export function formatDateTime(at, options) {
10
+ const style = options.style ?? 'medium';
11
+ return formatterFor(options.locale, {
12
+ timeZone: assertTimeZone(options.zone),
13
+ dateStyle: options.dateStyle ?? style,
14
+ timeStyle: options.timeStyle ?? (style === 'full' || style === 'long' ? 'medium' : style),
15
+ ...(options.hour12 === undefined ? {} : { hour12: options.hour12 }),
16
+ }).format(at);
17
+ }
18
+ export function formatDate(at, options) {
19
+ return formatterFor(options.locale, {
20
+ timeZone: assertTimeZone(options.zone),
21
+ dateStyle: options.style ?? 'medium',
22
+ }).format(at);
23
+ }
24
+ export function formatTime(at, options) {
25
+ return formatterFor(options.locale, {
26
+ timeZone: assertTimeZone(options.zone),
27
+ timeStyle: options.style ?? 'short',
28
+ ...(options.hour12 === undefined ? {} : { hour12: options.hour12 }),
29
+ }).format(at);
30
+ }
31
+ /**
32
+ * `14 Mar 2026, 09:00 (GMT+1)` — the offset made visible.
33
+ * Built with `timeZoneName: 'shortOffset'` + `formatToParts` so the offset is appended
34
+ * in a fixed position instead of wherever the locale pattern happens to put it.
35
+ */
36
+ export function formatWithOffset(at, options) {
37
+ const style = options.style ?? 'medium';
38
+ // `timeZoneName` is a component option, and Intl forbids mixing those with dateStyle /
39
+ // timeStyle — so the components are spelled out here instead.
40
+ const parts = formatterFor(options.locale, {
41
+ timeZone: assertTimeZone(options.zone),
42
+ year: 'numeric',
43
+ month: style === 'short' ? 'numeric' : style === 'medium' ? 'short' : 'long',
44
+ day: 'numeric',
45
+ hour: '2-digit',
46
+ minute: '2-digit',
47
+ ...(options.hour12 === undefined ? { hourCycle: 'h23' } : { hour12: options.hour12 }),
48
+ timeZoneName: 'shortOffset',
49
+ }).formatToParts(at);
50
+ const offset = parts.find((part) => part.type === 'timeZoneName')?.value ?? '';
51
+ const text = parts
52
+ .filter((part) => part.type !== 'timeZoneName')
53
+ .map((part) => part.value)
54
+ .join('')
55
+ .replace(/[\s,]+$/u, '')
56
+ .trim();
57
+ return offset === '' ? text : `${text} (${offset})`;
58
+ }
59
+ /** ISO-8601 date parts in a zone, for `<input type="date">` and CSV columns. */
60
+ export function formatIsoDate(at, zone) {
61
+ const parts = formatterFor('en-CA', {
62
+ timeZone: assertTimeZone(zone),
63
+ year: 'numeric',
64
+ month: '2-digit',
65
+ day: '2-digit',
66
+ }).format(at);
67
+ return parts.replace(/\//g, '-');
68
+ }
69
+ const RELATIVE_UNITS = [
70
+ ['year', 31_536_000_000],
71
+ ['month', 2_592_000_000],
72
+ ['week', 604_800_000],
73
+ ['day', 86_400_000],
74
+ ['hour', 3_600_000],
75
+ ['minute', 60_000],
76
+ ['second', 1000],
77
+ ];
78
+ /** `in 3 days` / `2 hours ago`, picking the largest unit that fits. */
79
+ export function formatRelative(at, options) {
80
+ const delta = differenceMs(options.now, at);
81
+ const formatter = new Intl.RelativeTimeFormat(options.locale, {
82
+ numeric: options.numeric ?? 'auto',
83
+ style: options.style ?? 'long',
84
+ });
85
+ const magnitude = Math.abs(delta);
86
+ for (const [unit, ms] of RELATIVE_UNITS) {
87
+ if (magnitude >= ms) {
88
+ return formatter.format(Math.trunc(delta / ms), unit);
89
+ }
90
+ }
91
+ return formatter.format(0, 'second');
92
+ }
93
+ /** `14–16 Mar 2026` — one call, so the locale decides how to collapse the range. */
94
+ export function formatRange(from, to, options) {
95
+ const style = options.style ?? 'medium';
96
+ const formatter = formatterFor(options.locale, {
97
+ timeZone: assertTimeZone(options.zone),
98
+ dateStyle: options.dateStyle ?? style,
99
+ ...(options.timeStyle === undefined ? {} : { timeStyle: options.timeStyle }),
100
+ });
101
+ // `formatRange` is ES2021; fall back to two formatted endpoints on older engines.
102
+ if (typeof formatter.formatRange === 'function')
103
+ return formatter.formatRange(from, to);
104
+ return `${formatter.format(from)} – ${formatter.format(to)}`;
105
+ }
106
+ const ORDINAL_SUFFIX = {
107
+ one: 'st',
108
+ two: 'nd',
109
+ few: 'rd',
110
+ other: 'th',
111
+ zero: 'th',
112
+ many: 'th',
113
+ };
114
+ /**
115
+ * `Intl` renders `November 5, 2011`, never `5th of November`. When a design asks for the
116
+ * ordinal, build it from `Intl.PluralRules` with `type: 'ordinal'` — English-only by
117
+ * nature, which is why it is a helper and not the default date format.
118
+ */
119
+ export function ordinal(value, locale = 'en') {
120
+ const category = new Intl.PluralRules(locale, { type: 'ordinal' }).select(value);
121
+ return `${value}${ORDINAL_SUFFIX[category]}`;
122
+ }
123
+ const cache = new Map();
124
+ function formatterFor(locale, options) {
125
+ const key = `${locale}|${JSON.stringify(options)}`;
126
+ const cached = cache.get(key);
127
+ if (cached !== undefined)
128
+ return cached;
129
+ const formatter = new Intl.DateTimeFormat(locale, options);
130
+ cache.set(key, formatter);
131
+ return formatter;
132
+ }
133
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["format.ts"],"names":[],"mappings":"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"}