@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
package/src/zones.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * IANA timezone primitives. The UTC offset of a zone is derived from
3
+ * `Intl.DateTimeFormat.formatToParts` — the runtime already ships the tzdata, so there
4
+ * is no offset table to keep in sync and no `date-fns-tz` dependency.
5
+ */
6
+ import { timezoneInvalid } from './errors';
7
+ export const UTC = 'UTC';
8
+ /** ES2024 `Intl` accepts `+01:00` as a zone; we do not — a fixed offset has no DST rules. */
9
+ const NUMERIC_OFFSET = /^[+-]/;
10
+ export function isValidTimeZone(zone) {
11
+ if (zone === '' || NUMERIC_OFFSET.test(zone))
12
+ return false;
13
+ try {
14
+ new Intl.DateTimeFormat('en-US', { timeZone: zone });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export function assertTimeZone(zone) {
22
+ if (!isValidTimeZone(zone))
23
+ throw timezoneInvalid(zone);
24
+ return zone;
25
+ }
26
+ /**
27
+ * Read the zone's wall clock for an instant. `hourCycle: 'h23'` is essential: without it
28
+ * some locales render midnight as hour 24 and every calculation downstream drifts a day.
29
+ */
30
+ export function zonePartsAt(zone, at) {
31
+ const parts = partsFormatterFor(zone).formatToParts(at);
32
+ const read = (type) => {
33
+ const value = parts.find((part) => part.type === type)?.value;
34
+ if (value === undefined)
35
+ throw timezoneInvalid(zone);
36
+ return Number.parseInt(value, 10);
37
+ };
38
+ return {
39
+ year: read('year'),
40
+ month: read('month'),
41
+ day: read('day'),
42
+ hour: read('hour') % 24,
43
+ minute: read('minute'),
44
+ second: read('second'),
45
+ };
46
+ }
47
+ /**
48
+ * Offset in **minutes east of UTC** at a given instant: `Europe/Berlin` → 60 or 120,
49
+ * `Asia/Kathmandu` → 345, `America/New_York` → -300 or -240.
50
+ *
51
+ * The trick: format the instant in the zone, then re-read those wall-clock fields *as if
52
+ * they were UTC*. The difference between that and the real epoch is the offset.
53
+ */
54
+ export function offsetAt(zone, at) {
55
+ const parts = zonePartsAt(zone, at);
56
+ const asIfUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
57
+ // Offsets are whole minutes; rounding absorbs the sub-second part `asIfUtc` drops.
58
+ return Math.round((asIfUtc - at.getTime()) / 60_000);
59
+ }
60
+ /** `+01:00`, `-04:00`, `+05:45`, `Z`. */
61
+ export function offsetLabel(minutes) {
62
+ if (minutes === 0)
63
+ return 'Z';
64
+ const sign = minutes < 0 ? '-' : '+';
65
+ const absolute = Math.abs(minutes);
66
+ const hours = Math.floor(absolute / 60);
67
+ return `${sign}${pad2(hours)}:${pad2(absolute % 60)}`;
68
+ }
69
+ /** Locale-aware zone label: `CET`, `GMT+5:45`, `Central European Standard Time`. */
70
+ export function zoneAbbrev(zone, at, locale = 'en-US', style = 'short') {
71
+ const formatter = new Intl.DateTimeFormat(locale, {
72
+ timeZone: zone,
73
+ timeZoneName: style,
74
+ hourCycle: 'h23',
75
+ });
76
+ const label = formatter.formatToParts(at).find((part) => part.type === 'timeZoneName')?.value;
77
+ return label ?? offsetLabel(offsetAt(zone, at));
78
+ }
79
+ /** True when the zone observes a different offset at some point in the surrounding year. */
80
+ export function observesDst(zone, at) {
81
+ const offsets = new Set();
82
+ for (let month = 0; month < 12; month += 1) {
83
+ const probe = new Date(at.getTime());
84
+ probe.setUTCMonth(probe.getUTCMonth() + month);
85
+ offsets.add(offsetAt(zone, probe));
86
+ }
87
+ return offsets.size > 1;
88
+ }
89
+ const formatters = new Map();
90
+ function partsFormatterFor(zone) {
91
+ const cached = formatters.get(zone);
92
+ if (cached !== undefined)
93
+ return cached;
94
+ let formatter;
95
+ try {
96
+ formatter = new Intl.DateTimeFormat('en-US', {
97
+ timeZone: zone,
98
+ hourCycle: 'h23',
99
+ year: 'numeric',
100
+ month: '2-digit',
101
+ day: '2-digit',
102
+ hour: '2-digit',
103
+ minute: '2-digit',
104
+ second: '2-digit',
105
+ });
106
+ }
107
+ catch {
108
+ throw timezoneInvalid(zone);
109
+ }
110
+ formatters.set(zone, formatter);
111
+ return formatter;
112
+ }
113
+ function pad2(value) {
114
+ return String(value).padStart(2, '0');
115
+ }
116
+ //# sourceMappingURL=zones.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zones.js","sourceRoot":"","sources":["zones.ts"],"names":[],"mappings":"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"}
package/src/zones.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * IANA timezone primitives. The UTC offset of a zone is derived from
3
+ * `Intl.DateTimeFormat.formatToParts` — the runtime already ships the tzdata, so there
4
+ * is no offset table to keep in sync and no `date-fns-tz` dependency.
5
+ */
6
+
7
+ import { timezoneInvalid } from './errors';
8
+ import type { Instant } from './instant';
9
+
10
+ /** An IANA identifier: `Europe/Berlin`, `Asia/Kathmandu`, `UTC`. Never `CET`, never `+01:00`. */
11
+ export type TimeZone = string;
12
+
13
+ export const UTC: TimeZone = 'UTC';
14
+
15
+ /** ES2024 `Intl` accepts `+01:00` as a zone; we do not — a fixed offset has no DST rules. */
16
+ const NUMERIC_OFFSET = /^[+-]/;
17
+
18
+ /**
19
+ * The package's only builder of a UTC epoch from calendar fields, because `Date.UTC` remaps
20
+ * years 0–99 onto 1900–1999 — silently, so a first-century wall clock resolves 1900 years off
21
+ * and every derived answer (offset, weekday, day count) is wrong without ever throwing.
22
+ * Overflow still carries exactly as `Date.UTC` does: day 0, day 32 and hour 24 all roll.
23
+ */
24
+ export function utcEpoch(
25
+ year: number,
26
+ month: number,
27
+ day: number,
28
+ hour = 0,
29
+ minute = 0,
30
+ second = 0,
31
+ ): number {
32
+ const at = new Date(0);
33
+ at.setUTCFullYear(year, month - 1, day);
34
+ at.setUTCHours(hour, minute, second, 0);
35
+ return at.getTime();
36
+ }
37
+
38
+ export function isValidTimeZone(zone: string): boolean {
39
+ if (zone === '' || NUMERIC_OFFSET.test(zone)) return false;
40
+ try {
41
+ new Intl.DateTimeFormat('en-US', { timeZone: zone });
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function assertTimeZone(zone: string): TimeZone {
49
+ if (!isValidTimeZone(zone)) throw timezoneInvalid(zone);
50
+ return zone;
51
+ }
52
+
53
+ /** Wall-clock fields of an instant in a zone, seconds precision. */
54
+ export interface ZoneParts {
55
+ year: number;
56
+ month: number;
57
+ day: number;
58
+ hour: number;
59
+ minute: number;
60
+ second: number;
61
+ }
62
+
63
+ /**
64
+ * Read the zone's wall clock for an instant. `hourCycle: 'h23'` is essential: without it
65
+ * some locales render midnight as hour 24 and every calculation downstream drifts a day.
66
+ */
67
+ export function zonePartsAt(zone: TimeZone, at: Instant): ZoneParts {
68
+ const parts = partsFormatterFor(zone).formatToParts(at);
69
+ const read = (type: Intl.DateTimeFormatPartTypes): number => {
70
+ const value = parts.find((part) => part.type === type)?.value;
71
+ if (value === undefined) throw timezoneInvalid(zone);
72
+ return Number.parseInt(value, 10);
73
+ };
74
+ return {
75
+ year: read('year'),
76
+ month: read('month'),
77
+ day: read('day'),
78
+ hour: read('hour') % 24,
79
+ minute: read('minute'),
80
+ second: read('second'),
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Offset in **minutes east of UTC** at a given instant: `Europe/Berlin` → 60 or 120,
86
+ * `Asia/Kathmandu` → 345, `America/New_York` → -300 or -240.
87
+ *
88
+ * The trick: format the instant in the zone, then re-read those wall-clock fields *as if
89
+ * they were UTC*. The difference between that and the real epoch is the offset.
90
+ */
91
+ export function offsetAt(zone: TimeZone, at: Instant): number {
92
+ const parts = zonePartsAt(zone, at);
93
+ const asIfUtc = utcEpoch(
94
+ parts.year,
95
+ parts.month,
96
+ parts.day,
97
+ parts.hour,
98
+ parts.minute,
99
+ parts.second,
100
+ );
101
+ // Offsets are whole minutes; rounding absorbs the sub-second part `asIfUtc` drops.
102
+ return Math.round((asIfUtc - at.getTime()) / 60_000);
103
+ }
104
+
105
+ /** `+01:00`, `-04:00`, `+05:45`, `Z`. */
106
+ export function offsetLabel(minutes: number): string {
107
+ if (minutes === 0) return 'Z';
108
+ const sign = minutes < 0 ? '-' : '+';
109
+ const absolute = Math.abs(minutes);
110
+ const hours = Math.floor(absolute / 60);
111
+ return `${sign}${pad2(hours)}:${pad2(absolute % 60)}`;
112
+ }
113
+
114
+ /** Locale-aware zone label: `CET`, `GMT+5:45`, `Central European Standard Time`. */
115
+ export function zoneAbbrev(
116
+ zone: TimeZone,
117
+ at: Instant,
118
+ locale = 'en-US',
119
+ style: 'short' | 'long' | 'shortOffset' | 'longOffset' = 'short',
120
+ ): string {
121
+ const formatter = new Intl.DateTimeFormat(locale, {
122
+ timeZone: zone,
123
+ timeZoneName: style,
124
+ hourCycle: 'h23',
125
+ });
126
+ const label = formatter.formatToParts(at).find((part) => part.type === 'timeZoneName')?.value;
127
+ return label ?? offsetLabel(offsetAt(zone, at));
128
+ }
129
+
130
+ /** True when the zone observes a different offset at some point in the surrounding year. */
131
+ export function observesDst(zone: TimeZone, at: Instant): boolean {
132
+ const offsets = new Set<number>();
133
+ for (let month = 0; month < 12; month += 1) {
134
+ const probe = new Date(at.getTime());
135
+ probe.setUTCMonth(probe.getUTCMonth() + month);
136
+ offsets.add(offsetAt(zone, probe as Instant));
137
+ }
138
+ return offsets.size > 1;
139
+ }
140
+
141
+ const formatters = new Map<string, Intl.DateTimeFormat>();
142
+
143
+ function partsFormatterFor(zone: TimeZone): Intl.DateTimeFormat {
144
+ const cached = formatters.get(zone);
145
+ if (cached !== undefined) return cached;
146
+ let formatter: Intl.DateTimeFormat;
147
+ try {
148
+ formatter = new Intl.DateTimeFormat('en-US', {
149
+ timeZone: zone,
150
+ hourCycle: 'h23',
151
+ year: 'numeric',
152
+ month: '2-digit',
153
+ day: '2-digit',
154
+ hour: '2-digit',
155
+ minute: '2-digit',
156
+ second: '2-digit',
157
+ });
158
+ } catch {
159
+ throw timezoneInvalid(zone);
160
+ }
161
+ formatters.set(zone, formatter);
162
+ return formatter;
163
+ }
164
+
165
+ function pad2(value: number): string {
166
+ return String(value).padStart(2, '0');
167
+ }