ts-time-utils 3.0.4 → 4.1.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/README.md +186 -6
- package/dist/calculate.d.ts +25 -0
- package/dist/calculate.d.ts.map +1 -1
- package/dist/calculate.js +125 -0
- package/dist/calendar.d.ts +45 -0
- package/dist/calendar.d.ts.map +1 -1
- package/dist/calendar.js +68 -0
- package/dist/calendars.d.ts +156 -0
- package/dist/calendars.d.ts.map +1 -0
- package/dist/calendars.js +348 -0
- package/dist/compare.d.ts +27 -0
- package/dist/compare.d.ts.map +1 -1
- package/dist/compare.js +46 -0
- package/dist/esm/calculate.d.ts +25 -0
- package/dist/esm/calculate.d.ts.map +1 -1
- package/dist/esm/calculate.js +125 -0
- package/dist/esm/calendar.d.ts +45 -0
- package/dist/esm/calendar.d.ts.map +1 -1
- package/dist/esm/calendar.js +68 -0
- package/dist/esm/calendars.d.ts +156 -0
- package/dist/esm/calendars.d.ts.map +1 -0
- package/dist/esm/calendars.js +348 -0
- package/dist/esm/compare.d.ts +27 -0
- package/dist/esm/compare.d.ts.map +1 -1
- package/dist/esm/compare.js +46 -0
- package/dist/esm/finance.d.ts +236 -0
- package/dist/esm/finance.d.ts.map +1 -0
- package/dist/esm/finance.js +495 -0
- package/dist/esm/healthcare.d.ts +260 -0
- package/dist/esm/healthcare.d.ts.map +1 -0
- package/dist/esm/healthcare.js +447 -0
- package/dist/esm/holidays.d.ts +11 -1
- package/dist/esm/holidays.d.ts.map +1 -1
- package/dist/esm/holidays.js +220 -1
- package/dist/esm/index.d.ts +19 -7
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +23 -9
- package/dist/esm/iterate.d.ts +55 -0
- package/dist/esm/iterate.d.ts.map +1 -1
- package/dist/esm/iterate.js +86 -0
- package/dist/esm/locale.d.ts +53 -0
- package/dist/esm/locale.d.ts.map +1 -1
- package/dist/esm/locale.js +141 -0
- package/dist/esm/precision.d.ts +225 -0
- package/dist/esm/precision.d.ts.map +1 -0
- package/dist/esm/precision.js +491 -0
- package/dist/esm/scheduling.d.ts +206 -0
- package/dist/esm/scheduling.d.ts.map +1 -0
- package/dist/esm/scheduling.js +329 -0
- package/dist/esm/temporal.d.ts +237 -0
- package/dist/esm/temporal.d.ts.map +1 -0
- package/dist/esm/temporal.js +660 -0
- package/dist/esm/validate.d.ts +30 -0
- package/dist/esm/validate.d.ts.map +1 -1
- package/dist/esm/validate.js +48 -0
- package/dist/finance.d.ts +236 -0
- package/dist/finance.d.ts.map +1 -0
- package/dist/finance.js +495 -0
- package/dist/healthcare.d.ts +260 -0
- package/dist/healthcare.d.ts.map +1 -0
- package/dist/healthcare.js +447 -0
- package/dist/holidays.d.ts +11 -1
- package/dist/holidays.d.ts.map +1 -1
- package/dist/holidays.js +220 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -9
- package/dist/iterate.d.ts +55 -0
- package/dist/iterate.d.ts.map +1 -1
- package/dist/iterate.js +86 -0
- package/dist/locale.d.ts +53 -0
- package/dist/locale.d.ts.map +1 -1
- package/dist/locale.js +141 -0
- package/dist/precision.d.ts +225 -0
- package/dist/precision.d.ts.map +1 -0
- package/dist/precision.js +491 -0
- package/dist/scheduling.d.ts +206 -0
- package/dist/scheduling.d.ts.map +1 -0
- package/dist/scheduling.js +329 -0
- package/dist/temporal.d.ts +237 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +660 -0
- package/dist/validate.d.ts +30 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +48 -0
- package/package.json +31 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview High-precision time utilities
|
|
3
|
+
* Handles nanoseconds, BigInt timestamps, sub-millisecond operations
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a nanosecond timestamp from components
|
|
7
|
+
*/
|
|
8
|
+
export function createNanosecondTimestamp(milliseconds, nanoseconds = 0) {
|
|
9
|
+
// Normalize: ensure nanoseconds is 0-999999
|
|
10
|
+
const extraMs = Math.floor(nanoseconds / 1000000);
|
|
11
|
+
const normalizedNs = nanoseconds % 1000000;
|
|
12
|
+
const totalMs = milliseconds + extraMs;
|
|
13
|
+
const totalNanoseconds = BigInt(totalMs) * BigInt(1000000) + BigInt(normalizedNs);
|
|
14
|
+
return {
|
|
15
|
+
milliseconds: totalMs,
|
|
16
|
+
nanoseconds: normalizedNs,
|
|
17
|
+
totalNanoseconds,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a nanosecond timestamp from BigInt
|
|
22
|
+
*/
|
|
23
|
+
export function fromNanoseconds(totalNs) {
|
|
24
|
+
const ms = Number(totalNs / BigInt(1000000));
|
|
25
|
+
const ns = Number(totalNs % BigInt(1000000));
|
|
26
|
+
return {
|
|
27
|
+
milliseconds: ms,
|
|
28
|
+
nanoseconds: ns,
|
|
29
|
+
totalNanoseconds: totalNs,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create a nanosecond timestamp from a Date
|
|
34
|
+
*/
|
|
35
|
+
export function dateToNanoseconds(date) {
|
|
36
|
+
return createNanosecondTimestamp(date.getTime(), 0);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert nanosecond timestamp to Date (loses sub-millisecond precision)
|
|
40
|
+
*/
|
|
41
|
+
export function nanosecondsToDate(timestamp) {
|
|
42
|
+
return new Date(timestamp.milliseconds);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Add two nanosecond timestamps
|
|
46
|
+
*/
|
|
47
|
+
export function addNanoseconds(a, b) {
|
|
48
|
+
return fromNanoseconds(a.totalNanoseconds + b.totalNanoseconds);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Subtract nanosecond timestamps
|
|
52
|
+
*/
|
|
53
|
+
export function subtractNanoseconds(a, b) {
|
|
54
|
+
return fromNanoseconds(a.totalNanoseconds - b.totalNanoseconds);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compare nanosecond timestamps
|
|
58
|
+
* @returns -1 if a < b, 0 if equal, 1 if a > b
|
|
59
|
+
*/
|
|
60
|
+
export function compareNanoseconds(a, b) {
|
|
61
|
+
if (a.totalNanoseconds < b.totalNanoseconds)
|
|
62
|
+
return -1;
|
|
63
|
+
if (a.totalNanoseconds > b.totalNanoseconds)
|
|
64
|
+
return 1;
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get current time with nanosecond precision (if available)
|
|
69
|
+
* Falls back to millisecond precision if performance.now() not available
|
|
70
|
+
*/
|
|
71
|
+
export function nowNanoseconds() {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
// Try to get sub-millisecond precision from performance.now()
|
|
74
|
+
if (typeof performance !== 'undefined' && performance.now) {
|
|
75
|
+
const perfNow = performance.now();
|
|
76
|
+
const microSeconds = Math.round((perfNow % 1) * 1000);
|
|
77
|
+
return createNanosecondTimestamp(now, microSeconds * 1000);
|
|
78
|
+
}
|
|
79
|
+
return createNanosecondTimestamp(now, 0);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Format nanosecond timestamp to ISO string with sub-millisecond precision
|
|
83
|
+
*/
|
|
84
|
+
export function formatNanoseconds(timestamp) {
|
|
85
|
+
const date = nanosecondsToDate(timestamp);
|
|
86
|
+
const isoBase = date.toISOString().slice(0, -1); // Remove trailing 'Z'
|
|
87
|
+
// Add nanoseconds (6 additional digits after milliseconds)
|
|
88
|
+
const nsStr = timestamp.nanoseconds.toString().padStart(6, '0');
|
|
89
|
+
return `${isoBase}${nsStr}Z`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse ISO string with nanosecond precision
|
|
93
|
+
*/
|
|
94
|
+
export function parseNanoseconds(isoString) {
|
|
95
|
+
// Match ISO format: 2024-03-25T14:30:45.123456789Z
|
|
96
|
+
const match = isoString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3,9})Z?$/);
|
|
97
|
+
if (!match) {
|
|
98
|
+
// Try standard parsing
|
|
99
|
+
const date = new Date(isoString);
|
|
100
|
+
if (isNaN(date.getTime()))
|
|
101
|
+
return null;
|
|
102
|
+
return dateToNanoseconds(date);
|
|
103
|
+
}
|
|
104
|
+
const [, year, month, day, hour, minute, second, fraction] = match;
|
|
105
|
+
// Build base date with milliseconds
|
|
106
|
+
const ms = parseInt(fraction.slice(0, 3).padEnd(3, '0'), 10);
|
|
107
|
+
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10), parseInt(hour, 10), parseInt(minute, 10), parseInt(second, 10), ms);
|
|
108
|
+
// Calculate nanoseconds beyond milliseconds
|
|
109
|
+
const nsFraction = fraction.slice(3).padEnd(6, '0');
|
|
110
|
+
const nanoseconds = parseInt(nsFraction, 10);
|
|
111
|
+
return createNanosecondTimestamp(date.getTime(), nanoseconds);
|
|
112
|
+
}
|
|
113
|
+
// ============= High-Resolution Duration =============
|
|
114
|
+
/**
|
|
115
|
+
* Create a high-resolution duration
|
|
116
|
+
*/
|
|
117
|
+
export function createHighResDuration(seconds, nanoseconds = 0) {
|
|
118
|
+
// Normalize: carry over extra seconds from nanoseconds
|
|
119
|
+
const extraSeconds = Math.floor(nanoseconds / 1000000000);
|
|
120
|
+
const normalizedNs = nanoseconds % 1000000000;
|
|
121
|
+
return {
|
|
122
|
+
seconds: seconds + extraSeconds,
|
|
123
|
+
nanoseconds: normalizedNs,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Add two high-resolution durations
|
|
128
|
+
*/
|
|
129
|
+
export function addHighResDuration(a, b) {
|
|
130
|
+
const totalNs = a.nanoseconds + b.nanoseconds;
|
|
131
|
+
const extraSeconds = Math.floor(totalNs / 1000000000);
|
|
132
|
+
const ns = totalNs % 1000000000;
|
|
133
|
+
return {
|
|
134
|
+
seconds: a.seconds + b.seconds + extraSeconds,
|
|
135
|
+
nanoseconds: ns,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Subtract high-resolution durations
|
|
140
|
+
*/
|
|
141
|
+
export function subtractHighResDuration(a, b) {
|
|
142
|
+
let totalNs = a.nanoseconds - b.nanoseconds;
|
|
143
|
+
let seconds = a.seconds - b.seconds;
|
|
144
|
+
if (totalNs < 0) {
|
|
145
|
+
seconds -= 1;
|
|
146
|
+
totalNs += 1000000000;
|
|
147
|
+
}
|
|
148
|
+
return { seconds, nanoseconds: totalNs };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Convert high-resolution duration to milliseconds
|
|
152
|
+
*/
|
|
153
|
+
export function highResDurationToMs(duration) {
|
|
154
|
+
return duration.seconds * 1000 + duration.nanoseconds / 1000000;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Convert milliseconds to high-resolution duration
|
|
158
|
+
*/
|
|
159
|
+
export function msToHighResDuration(ms) {
|
|
160
|
+
const seconds = Math.floor(ms / 1000);
|
|
161
|
+
const nanoseconds = Math.round((ms % 1000) * 1000000);
|
|
162
|
+
return { seconds, nanoseconds };
|
|
163
|
+
}
|
|
164
|
+
// ============= BigInt Timestamp Support =============
|
|
165
|
+
/**
|
|
166
|
+
* Convert Unix epoch milliseconds to BigInt
|
|
167
|
+
*/
|
|
168
|
+
export function toBigIntMs(date) {
|
|
169
|
+
return BigInt(date.getTime());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Convert BigInt milliseconds to Date
|
|
173
|
+
*/
|
|
174
|
+
export function fromBigIntMs(ms) {
|
|
175
|
+
return new Date(Number(ms));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Convert Unix epoch seconds to BigInt
|
|
179
|
+
*/
|
|
180
|
+
export function toBigIntSeconds(date) {
|
|
181
|
+
return BigInt(Math.floor(date.getTime() / 1000));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Convert BigInt seconds to Date
|
|
185
|
+
*/
|
|
186
|
+
export function fromBigIntSeconds(seconds) {
|
|
187
|
+
return new Date(Number(seconds) * 1000);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Add milliseconds (as BigInt) to a date
|
|
191
|
+
*/
|
|
192
|
+
export function addBigIntMs(date, ms) {
|
|
193
|
+
const current = toBigIntMs(date);
|
|
194
|
+
return fromBigIntMs(current + ms);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Subtract milliseconds (as BigInt) from a date
|
|
198
|
+
*/
|
|
199
|
+
export function subtractBigIntMs(date, ms) {
|
|
200
|
+
const current = toBigIntMs(date);
|
|
201
|
+
return fromBigIntMs(current - ms);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Calculate difference between dates in BigInt milliseconds
|
|
205
|
+
*/
|
|
206
|
+
export function diffBigIntMs(a, b) {
|
|
207
|
+
return toBigIntMs(a) - toBigIntMs(b);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Check if a date falls in a DST transition gap (skipped time)
|
|
211
|
+
* @param date - Date to check
|
|
212
|
+
* @param timeZone - IANA timezone (optional, uses local if not provided)
|
|
213
|
+
*/
|
|
214
|
+
export function isInDSTGap(date, timeZone) {
|
|
215
|
+
if (timeZone) {
|
|
216
|
+
// For specific timezone, we need to check around this time
|
|
217
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
218
|
+
timeZone,
|
|
219
|
+
hour: 'numeric',
|
|
220
|
+
minute: 'numeric',
|
|
221
|
+
year: 'numeric',
|
|
222
|
+
month: 'numeric',
|
|
223
|
+
day: 'numeric',
|
|
224
|
+
hour12: false,
|
|
225
|
+
});
|
|
226
|
+
// Get 1 minute before and after
|
|
227
|
+
const before = new Date(date.getTime() - 60000);
|
|
228
|
+
const after = new Date(date.getTime() + 60000);
|
|
229
|
+
const beforeParts = formatter.formatToParts(before);
|
|
230
|
+
const afterParts = formatter.formatToParts(after);
|
|
231
|
+
const getMinuteValue = (parts) => {
|
|
232
|
+
const h = parseInt(parts.find(p => p.type === 'hour')?.value || '0', 10);
|
|
233
|
+
const m = parseInt(parts.find(p => p.type === 'minute')?.value || '0', 10);
|
|
234
|
+
return h * 60 + m;
|
|
235
|
+
};
|
|
236
|
+
// If there's a jump of more than 2 minutes between 1 min before and 1 min after,
|
|
237
|
+
// we're likely in a gap
|
|
238
|
+
const beforeMinute = getMinuteValue(beforeParts);
|
|
239
|
+
const afterMinute = getMinuteValue(afterParts);
|
|
240
|
+
const diff = afterMinute - beforeMinute;
|
|
241
|
+
// Account for day boundary
|
|
242
|
+
const adjustedDiff = diff < 0 ? diff + 1440 : diff;
|
|
243
|
+
return adjustedDiff > 62; // Allow 2 minute variance + DST gap (typically 60 min)
|
|
244
|
+
}
|
|
245
|
+
// For local timezone, compare UTC offset around this time
|
|
246
|
+
const before = new Date(date.getTime() - 60000);
|
|
247
|
+
const after = new Date(date.getTime() + 60000);
|
|
248
|
+
const offsetBefore = before.getTimezoneOffset();
|
|
249
|
+
const offsetAfter = after.getTimezoneOffset();
|
|
250
|
+
// If offset changes and goes from larger to smaller (spring forward), it's a gap
|
|
251
|
+
return offsetBefore > offsetAfter;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Check if a date falls in a DST overlap (ambiguous time)
|
|
255
|
+
* @param date - Date to check
|
|
256
|
+
* @param timeZone - IANA timezone (optional, uses local if not provided)
|
|
257
|
+
*/
|
|
258
|
+
export function isInDSTOverlap(date, timeZone) {
|
|
259
|
+
if (timeZone) {
|
|
260
|
+
// Similar approach as isInDSTGap but for fall-back
|
|
261
|
+
// This is harder to detect accurately without full TZ database
|
|
262
|
+
return false; // Simplified: would need TZ data for accurate detection
|
|
263
|
+
}
|
|
264
|
+
// For local timezone
|
|
265
|
+
const before = new Date(date.getTime() - 60000);
|
|
266
|
+
const after = new Date(date.getTime() + 60000);
|
|
267
|
+
const offsetBefore = before.getTimezoneOffset();
|
|
268
|
+
const offsetAfter = after.getTimezoneOffset();
|
|
269
|
+
// If offset goes from smaller to larger (fall back), it's an overlap
|
|
270
|
+
return offsetBefore < offsetAfter;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Find DST transitions in a year for local timezone
|
|
274
|
+
*/
|
|
275
|
+
export function getDSTTransitionsInYear(year) {
|
|
276
|
+
const transitions = [];
|
|
277
|
+
let prevOffset = new Date(year, 0, 1).getTimezoneOffset();
|
|
278
|
+
// Check each day of the year
|
|
279
|
+
for (let month = 0; month < 12; month++) {
|
|
280
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
281
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
282
|
+
const date = new Date(year, month, day);
|
|
283
|
+
const currentOffset = date.getTimezoneOffset();
|
|
284
|
+
if (currentOffset !== prevOffset) {
|
|
285
|
+
// Found a transition - now find exact hour
|
|
286
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
287
|
+
const hourDate = new Date(year, month, day, hour);
|
|
288
|
+
const hourOffset = hourDate.getTimezoneOffset();
|
|
289
|
+
if (hourOffset !== prevOffset) {
|
|
290
|
+
const toDST = hourOffset < prevOffset;
|
|
291
|
+
const adjustmentMinutes = Math.abs(hourOffset - prevOffset);
|
|
292
|
+
transitions.push({
|
|
293
|
+
utc: new Date(Date.UTC(year, month, day, hour)),
|
|
294
|
+
local: hourDate,
|
|
295
|
+
toDST,
|
|
296
|
+
offsetBefore: -prevOffset, // Convert to positive = UTC+X
|
|
297
|
+
offsetAfter: -hourOffset,
|
|
298
|
+
adjustmentMinutes,
|
|
299
|
+
});
|
|
300
|
+
prevOffset = hourOffset;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
prevOffset = currentOffset;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return transitions;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Resolve ambiguous time in DST overlap
|
|
312
|
+
* @param date - Potentially ambiguous date
|
|
313
|
+
* @param prefer - Prefer 'earlier' or 'later' interpretation
|
|
314
|
+
*/
|
|
315
|
+
export function resolveAmbiguousTime(date, prefer = 'earlier') {
|
|
316
|
+
if (!isInDSTOverlap(date)) {
|
|
317
|
+
return date;
|
|
318
|
+
}
|
|
319
|
+
// During overlap, the same local time occurs twice
|
|
320
|
+
// The 'earlier' occurrence is DST (smaller offset)
|
|
321
|
+
// The 'later' occurrence is standard time (larger offset)
|
|
322
|
+
const offset = date.getTimezoneOffset();
|
|
323
|
+
if (prefer === 'earlier') {
|
|
324
|
+
// Return DST interpretation (typically 1 hour earlier in UTC)
|
|
325
|
+
return new Date(date.getTime() - 60 * 60 * 1000);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
// Return standard time interpretation
|
|
329
|
+
return date;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ============= Invalid Date Handling =============
|
|
333
|
+
/**
|
|
334
|
+
* Validated Date wrapper that guarantees a valid date
|
|
335
|
+
*/
|
|
336
|
+
export class ValidDate {
|
|
337
|
+
constructor(date) {
|
|
338
|
+
this._date = date;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Create a ValidDate, throws if invalid
|
|
342
|
+
*/
|
|
343
|
+
static from(date) {
|
|
344
|
+
if (isNaN(date.getTime())) {
|
|
345
|
+
throw new Error('Invalid Date');
|
|
346
|
+
}
|
|
347
|
+
return new ValidDate(date);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Create a ValidDate, returns null if invalid
|
|
351
|
+
*/
|
|
352
|
+
static tryFrom(date) {
|
|
353
|
+
if (isNaN(date.getTime())) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return new ValidDate(date);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Create from timestamp, throws if invalid
|
|
360
|
+
*/
|
|
361
|
+
static fromTimestamp(ms) {
|
|
362
|
+
const date = new Date(ms);
|
|
363
|
+
return ValidDate.from(date);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Create from ISO string, throws if invalid
|
|
367
|
+
*/
|
|
368
|
+
static fromISO(isoString) {
|
|
369
|
+
const date = new Date(isoString);
|
|
370
|
+
return ValidDate.from(date);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get the underlying Date object
|
|
374
|
+
*/
|
|
375
|
+
get value() {
|
|
376
|
+
return new Date(this._date.getTime()); // Return copy for immutability
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get timestamp
|
|
380
|
+
*/
|
|
381
|
+
get time() {
|
|
382
|
+
return this._date.getTime();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Format to ISO string
|
|
386
|
+
*/
|
|
387
|
+
toISOString() {
|
|
388
|
+
return this._date.toISOString();
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Format to locale string
|
|
392
|
+
*/
|
|
393
|
+
toLocaleString(locale, options) {
|
|
394
|
+
return this._date.toLocaleString(locale, options);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Ensure a date is valid, with fallback
|
|
399
|
+
*/
|
|
400
|
+
export function ensureValidDate(date, fallback = new Date()) {
|
|
401
|
+
return isNaN(date.getTime()) ? fallback : date;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Parse date with validation
|
|
405
|
+
*/
|
|
406
|
+
export function parseValidDate(input) {
|
|
407
|
+
if (input instanceof Date) {
|
|
408
|
+
return isNaN(input.getTime()) ? null : input;
|
|
409
|
+
}
|
|
410
|
+
const date = new Date(input);
|
|
411
|
+
return isNaN(date.getTime()) ? null : date;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Assert date is valid, throws if not
|
|
415
|
+
*/
|
|
416
|
+
export function assertValidDate(date, message) {
|
|
417
|
+
if (isNaN(date.getTime())) {
|
|
418
|
+
throw new Error(message || 'Invalid Date');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ============= Leap Second Awareness =============
|
|
422
|
+
/**
|
|
423
|
+
* Known leap seconds (added at end of these dates, 23:59:60 UTC)
|
|
424
|
+
* List from https://www.ietf.org/timezones/data/leap-seconds.list
|
|
425
|
+
*/
|
|
426
|
+
export const LEAP_SECONDS = [
|
|
427
|
+
new Date('1972-06-30T23:59:59Z'),
|
|
428
|
+
new Date('1972-12-31T23:59:59Z'),
|
|
429
|
+
new Date('1973-12-31T23:59:59Z'),
|
|
430
|
+
new Date('1974-12-31T23:59:59Z'),
|
|
431
|
+
new Date('1975-12-31T23:59:59Z'),
|
|
432
|
+
new Date('1976-12-31T23:59:59Z'),
|
|
433
|
+
new Date('1977-12-31T23:59:59Z'),
|
|
434
|
+
new Date('1978-12-31T23:59:59Z'),
|
|
435
|
+
new Date('1979-12-31T23:59:59Z'),
|
|
436
|
+
new Date('1981-06-30T23:59:59Z'),
|
|
437
|
+
new Date('1982-06-30T23:59:59Z'),
|
|
438
|
+
new Date('1983-06-30T23:59:59Z'),
|
|
439
|
+
new Date('1985-06-30T23:59:59Z'),
|
|
440
|
+
new Date('1987-12-31T23:59:59Z'),
|
|
441
|
+
new Date('1989-12-31T23:59:59Z'),
|
|
442
|
+
new Date('1990-12-31T23:59:59Z'),
|
|
443
|
+
new Date('1992-06-30T23:59:59Z'),
|
|
444
|
+
new Date('1993-06-30T23:59:59Z'),
|
|
445
|
+
new Date('1994-06-30T23:59:59Z'),
|
|
446
|
+
new Date('1995-12-31T23:59:59Z'),
|
|
447
|
+
new Date('1997-06-30T23:59:59Z'),
|
|
448
|
+
new Date('1998-12-31T23:59:59Z'),
|
|
449
|
+
new Date('2005-12-31T23:59:59Z'),
|
|
450
|
+
new Date('2008-12-31T23:59:59Z'),
|
|
451
|
+
new Date('2012-06-30T23:59:59Z'),
|
|
452
|
+
new Date('2015-06-30T23:59:59Z'),
|
|
453
|
+
new Date('2016-12-31T23:59:59Z'),
|
|
454
|
+
];
|
|
455
|
+
/**
|
|
456
|
+
* Get number of leap seconds between two dates
|
|
457
|
+
*/
|
|
458
|
+
export function leapSecondsBetween(start, end) {
|
|
459
|
+
let count = 0;
|
|
460
|
+
for (const ls of LEAP_SECONDS) {
|
|
461
|
+
if (ls >= start && ls < end) {
|
|
462
|
+
count++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return count;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Check if a date is near a leap second (within 1 second)
|
|
469
|
+
*/
|
|
470
|
+
export function isNearLeapSecond(date) {
|
|
471
|
+
const time = date.getTime();
|
|
472
|
+
return LEAP_SECONDS.some(ls => Math.abs(ls.getTime() - time) <= 1000);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Convert TAI (International Atomic Time) to UTC
|
|
476
|
+
* TAI = UTC + accumulated leap seconds + 10 (initial offset)
|
|
477
|
+
*/
|
|
478
|
+
export function taiToUtc(taiMs) {
|
|
479
|
+
// Count leap seconds before this TAI time
|
|
480
|
+
const utcApprox = new Date(taiMs);
|
|
481
|
+
const leapSeconds = leapSecondsBetween(new Date(0), utcApprox);
|
|
482
|
+
// TAI started 10 seconds ahead of UTC at Unix epoch
|
|
483
|
+
return new Date(taiMs - (leapSeconds + 10) * 1000);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Convert UTC to TAI
|
|
487
|
+
*/
|
|
488
|
+
export function utcToTai(date) {
|
|
489
|
+
const leapSeconds = leapSecondsBetween(new Date(0), date);
|
|
490
|
+
return date.getTime() + (leapSeconds + 10) * 1000;
|
|
491
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Scheduling and booking utilities
|
|
3
|
+
* Provides slot generation, availability checking, and conflict detection
|
|
4
|
+
*/
|
|
5
|
+
import type { DateRange, DateInput, WorkingHoursConfig, RecurrenceRule } from './types.js';
|
|
6
|
+
/** Configuration for scheduling operations */
|
|
7
|
+
export interface SchedulingConfig {
|
|
8
|
+
/** Working hours configuration */
|
|
9
|
+
workingHours?: WorkingHoursConfig;
|
|
10
|
+
/** Buffer time between appointments in minutes */
|
|
11
|
+
bufferMinutes?: number;
|
|
12
|
+
/** Default slot duration in minutes */
|
|
13
|
+
slotDuration?: number;
|
|
14
|
+
/** Holidays to exclude */
|
|
15
|
+
holidays?: Date[];
|
|
16
|
+
}
|
|
17
|
+
/** A time slot with availability status */
|
|
18
|
+
export interface Slot {
|
|
19
|
+
start: Date;
|
|
20
|
+
end: Date;
|
|
21
|
+
available: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** A booking with optional metadata */
|
|
24
|
+
export interface Booking extends DateRange {
|
|
25
|
+
id?: string;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** Default scheduling configuration */
|
|
29
|
+
export declare const DEFAULT_SCHEDULING_CONFIG: SchedulingConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Generates time slots for a single day
|
|
32
|
+
* @param date - The date to generate slots for
|
|
33
|
+
* @param config - Scheduling configuration
|
|
34
|
+
* @returns Array of slots for the day
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const slots = generateSlots(new Date('2024-01-15'), { slotDuration: 30 });
|
|
39
|
+
* // Returns 30-minute slots during working hours
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function generateSlots(date: DateInput, config?: SchedulingConfig): Slot[];
|
|
43
|
+
/**
|
|
44
|
+
* Generates time slots for a date range
|
|
45
|
+
* @param range - The date range to generate slots for
|
|
46
|
+
* @param config - Scheduling configuration
|
|
47
|
+
* @returns Array of slots for all days in range
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const range = { start: new Date('2024-01-15'), end: new Date('2024-01-17') };
|
|
52
|
+
* const slots = generateSlotsForRange(range, { slotDuration: 60 });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function generateSlotsForRange(range: DateRange, config?: SchedulingConfig): Slot[];
|
|
56
|
+
/**
|
|
57
|
+
* Gets available slots for a day, excluding existing bookings
|
|
58
|
+
* @param date - The date to check
|
|
59
|
+
* @param bookings - Existing bookings
|
|
60
|
+
* @param config - Scheduling configuration
|
|
61
|
+
* @returns Array of available slots
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const bookings = [{ start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }];
|
|
66
|
+
* const available = getAvailableSlots(new Date('2024-01-15'), bookings);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function getAvailableSlots(date: DateInput, bookings: Booking[], config?: SchedulingConfig): Slot[];
|
|
70
|
+
/**
|
|
71
|
+
* Finds the next available slot of specified duration
|
|
72
|
+
* @param after - Start searching after this date
|
|
73
|
+
* @param bookings - Existing bookings
|
|
74
|
+
* @param duration - Required slot duration in minutes
|
|
75
|
+
* @param config - Scheduling configuration
|
|
76
|
+
* @returns Next available slot or null if none found within 30 days
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const nextSlot = findNextAvailable(new Date(), bookings, 60);
|
|
81
|
+
* if (nextSlot) console.log(`Next 1-hour slot at ${nextSlot.start}`);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function findNextAvailable(after: DateInput, bookings: Booking[], duration: number, config?: SchedulingConfig): Slot | null;
|
|
85
|
+
/**
|
|
86
|
+
* Checks if a slot is available (no conflicts with existing bookings)
|
|
87
|
+
* @param slot - The slot to check
|
|
88
|
+
* @param bookings - Existing bookings
|
|
89
|
+
* @returns True if slot is available
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* const slot = { start: new Date('2024-01-15T14:00'), end: new Date('2024-01-15T15:00') };
|
|
94
|
+
* if (isSlotAvailable(slot, existingBookings)) {
|
|
95
|
+
* // Book the slot
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function isSlotAvailable(slot: DateRange, bookings: Booking[]): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Finds bookings that conflict with a proposed time range
|
|
102
|
+
* @param bookings - Existing bookings
|
|
103
|
+
* @param proposed - Proposed time range
|
|
104
|
+
* @returns Array of conflicting bookings
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const conflicts = findConflicts(existingBookings, { start: propStart, end: propEnd });
|
|
109
|
+
* if (conflicts.length > 0) {
|
|
110
|
+
* console.log('Conflicts with:', conflicts);
|
|
111
|
+
* }
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export declare function findConflicts(bookings: Booking[], proposed: DateRange): Booking[];
|
|
115
|
+
/**
|
|
116
|
+
* Checks if a proposed time range has any conflicts
|
|
117
|
+
* @param bookings - Existing bookings
|
|
118
|
+
* @param proposed - Proposed time range
|
|
119
|
+
* @returns True if there are conflicts
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* if (hasConflict(existingBookings, proposedMeeting)) {
|
|
124
|
+
* console.log('Time slot not available');
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare function hasConflict(bookings: Booking[], proposed: DateRange): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Adds buffer time around a slot
|
|
131
|
+
* @param slot - The original slot
|
|
132
|
+
* @param bufferMinutes - Buffer time in minutes
|
|
133
|
+
* @returns New slot with buffer added
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* const slot = { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') };
|
|
138
|
+
* const buffered = addBuffer(slot, 15);
|
|
139
|
+
* // buffered.start = 09:45, buffered.end = 11:15
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export declare function addBuffer(slot: DateRange, bufferMinutes: number): DateRange;
|
|
143
|
+
/**
|
|
144
|
+
* Removes buffer time from a slot
|
|
145
|
+
* @param slot - The buffered slot
|
|
146
|
+
* @param bufferMinutes - Buffer time in minutes to remove
|
|
147
|
+
* @returns New slot with buffer removed
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* const bufferedSlot = { start: new Date('2024-01-15T09:45'), end: new Date('2024-01-15T11:15') };
|
|
152
|
+
* const original = removeBuffer(bufferedSlot, 15);
|
|
153
|
+
* // original.start = 10:00, original.end = 11:00
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export declare function removeBuffer(slot: DateRange, bufferMinutes: number): DateRange;
|
|
157
|
+
/**
|
|
158
|
+
* Expands recurring availability pattern into concrete slots
|
|
159
|
+
* @param pattern - Recurrence pattern
|
|
160
|
+
* @param range - Date range to expand within
|
|
161
|
+
* @param config - Scheduling configuration
|
|
162
|
+
* @returns Array of slots from the recurring pattern
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* const pattern = {
|
|
167
|
+
* frequency: 'weekly',
|
|
168
|
+
* startDate: new Date('2024-01-01'),
|
|
169
|
+
* byWeekday: [1, 3, 5], // Mon, Wed, Fri
|
|
170
|
+
* until: new Date('2024-12-31')
|
|
171
|
+
* };
|
|
172
|
+
* const slots = expandRecurringAvailability(pattern, range);
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export declare function expandRecurringAvailability(pattern: RecurrenceRule, range: DateRange, config?: SchedulingConfig): Slot[];
|
|
176
|
+
/**
|
|
177
|
+
* Merges adjacent or overlapping bookings
|
|
178
|
+
* @param bookings - Array of bookings to merge
|
|
179
|
+
* @returns Array of merged bookings
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* const bookings = [
|
|
184
|
+
* { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T10:00') },
|
|
185
|
+
* { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }
|
|
186
|
+
* ];
|
|
187
|
+
* const merged = mergeBookings(bookings);
|
|
188
|
+
* // [{ start: 09:00, end: 11:00 }]
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export declare function mergeBookings(bookings: Booking[]): Booking[];
|
|
192
|
+
/**
|
|
193
|
+
* Splits a slot at a specific time
|
|
194
|
+
* @param slot - The slot to split
|
|
195
|
+
* @param at - The time to split at
|
|
196
|
+
* @returns Tuple of two slots, or null if split point is outside slot
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const slot = { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T11:00'), available: true };
|
|
201
|
+
* const [before, after] = splitSlot(slot, new Date('2024-01-15T10:00'));
|
|
202
|
+
* // before: 09:00-10:00, after: 10:00-11:00
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export declare function splitSlot(slot: Slot, at: DateInput): [Slot, Slot] | null;
|
|
206
|
+
//# sourceMappingURL=scheduling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduling.d.ts","sourceRoot":"","sources":["../src/scheduling.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAK3F,8CAA8C;AAC9C,MAAM,WAAW,gBAAgB;IAC/B,kCAAkC;IAClC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,IAAI,CAAC;IACZ,GAAG,EAAE,IAAI,CAAC;IACV,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,uCAAuC;AACvC,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,uCAAuC;AACvC,eAAO,MAAM,yBAAyB,EAAE,gBAKvC,CAAC;AAkBF;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,GAAE,gBAAqB,GAAG,IAAI,EAAE,CAqCpF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,GAAE,gBAAqB,GAAG,IAAI,EAAE,CAe7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,OAAO,EAAE,EACnB,MAAM,GAAE,gBAAqB,GAC5B,IAAI,EAAE,CAmBR;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,gBAAqB,GAC5B,IAAI,GAAG,IAAI,CAmBb;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,GAAG,OAAO,EAAE,CAEjF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,GAAG,OAAO,CAE7E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,CAM3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,SAAS,CAM9E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,SAAS,EAChB,MAAM,GAAE,gBAAqB,GAC5B,IAAI,EAAE,CAUR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAS5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,CAmBxE"}
|