ts-time-utils 4.0.1 → 4.4.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 +175 -30
- package/dist/{age.js → age.cjs} +14 -6
- package/dist/{calculate.js → calculate.cjs} +30 -18
- package/dist/{calendar.js → calendar.cjs} +80 -39
- package/dist/{calendars.js → calendars.cjs} +48 -23
- package/dist/{chain.js → chain.cjs} +41 -40
- package/dist/{compare.js → compare.cjs} +58 -28
- package/dist/constants.cjs +19 -0
- package/dist/{countdown.js → countdown.cjs} +16 -7
- package/dist/{cron.js → cron.cjs} +20 -9
- package/dist/{dateRange.js → dateRange.cjs} +42 -26
- package/dist/{duration.js → duration.cjs} +56 -44
- package/dist/esm/chain.js +0 -5
- 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/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/naturalLanguage.d.ts +1 -3
- package/dist/esm/naturalLanguage.d.ts.map +1 -1
- package/dist/esm/naturalLanguage.js +9 -2
- package/dist/esm/plugins.d.ts +0 -6
- package/dist/esm/plugins.d.ts.map +1 -1
- package/dist/esm/plugins.js +36 -42
- package/dist/esm/recurrence.d.ts.map +1 -1
- package/dist/esm/recurrence.js +3 -5
- 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/timezone.d.ts +6 -1
- package/dist/esm/timezone.d.ts.map +1 -1
- package/dist/esm/timezone.js +106 -66
- package/dist/esm/types.d.ts +0 -4
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/finance.cjs +512 -0
- package/dist/finance.d.ts +236 -0
- package/dist/finance.d.ts.map +1 -0
- package/dist/{fiscal.js → fiscal.cjs} +36 -17
- package/dist/{format.js → format.cjs} +83 -70
- package/dist/healthcare.cjs +462 -0
- package/dist/healthcare.d.ts +260 -0
- package/dist/healthcare.d.ts.map +1 -0
- package/dist/{holidays.js → holidays.cjs} +52 -25
- package/dist/index.cjs +595 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/{interval.js → interval.cjs} +24 -11
- package/dist/{iterate.js → iterate.cjs} +84 -41
- package/dist/{locale.js → locale.cjs} +54 -26
- package/dist/{naturalLanguage.js → naturalLanguage.cjs} +36 -23
- package/dist/naturalLanguage.d.ts +1 -3
- package/dist/naturalLanguage.d.ts.map +1 -1
- package/dist/{parse.js → parse.cjs} +24 -11
- package/dist/{performance.js → performance.cjs} +23 -10
- package/dist/{plugins.js → plugins.cjs} +48 -47
- package/dist/plugins.d.ts +0 -6
- package/dist/plugins.d.ts.map +1 -1
- package/dist/{precision.js → precision.cjs} +74 -37
- package/dist/{rangePresets.js → rangePresets.cjs} +40 -19
- package/dist/{recurrence.js → recurrence.cjs} +27 -21
- package/dist/recurrence.d.ts.map +1 -1
- package/dist/scheduling.cjs +344 -0
- package/dist/scheduling.d.ts +206 -0
- package/dist/scheduling.d.ts.map +1 -0
- package/dist/{serialize.js → serialize.cjs} +36 -17
- package/dist/{temporal.js → temporal.cjs} +28 -13
- package/dist/{timezone.js → timezone.cjs} +140 -82
- package/dist/timezone.d.ts +6 -1
- package/dist/timezone.d.ts.map +1 -1
- package/dist/{types.js → types.cjs} +9 -3
- package/dist/types.d.ts +0 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/{validate.js → validate.cjs} +54 -26
- package/dist/{workingHours.js → workingHours.cjs} +36 -17
- package/package.json +52 -34
- package/dist/constants.js +0 -16
- package/dist/index.js +0 -66
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* @fileoverview Recurring events and pattern-based date generation utilities
|
|
3
4
|
* Provides RRULE-inspired recurrence patterns for creating repeating events
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.createRecurrence = createRecurrence;
|
|
8
|
+
exports.getNextOccurrence = getNextOccurrence;
|
|
9
|
+
exports.getOccurrencesBetween = getOccurrencesBetween;
|
|
10
|
+
exports.isRecurrenceDate = isRecurrenceDate;
|
|
11
|
+
exports.isValidRecurrenceRule = isValidRecurrenceRule;
|
|
12
|
+
exports.recurrenceToString = recurrenceToString;
|
|
13
|
+
const calculate_js_1 = require("./calculate.cjs");
|
|
6
14
|
/**
|
|
7
15
|
* Creates a recurrence pattern generator
|
|
8
16
|
* @param rule - The recurrence rule defining the pattern
|
|
@@ -34,7 +42,7 @@ import { addTime } from './calculate.js';
|
|
|
34
42
|
* });
|
|
35
43
|
* ```
|
|
36
44
|
*/
|
|
37
|
-
|
|
45
|
+
function createRecurrence(rule) {
|
|
38
46
|
const startDate = new Date(rule.startDate);
|
|
39
47
|
return {
|
|
40
48
|
rule,
|
|
@@ -43,19 +51,17 @@ export function createRecurrence(rule) {
|
|
|
43
51
|
isRecurrenceDate: (date) => isRecurrenceDate(date, rule),
|
|
44
52
|
getAllOccurrences: (limit = 100) => {
|
|
45
53
|
const occurrences = [];
|
|
46
|
-
let current = new Date(startDate);
|
|
47
|
-
|
|
48
|
-
while (count < limit) {
|
|
54
|
+
let current = new Date(startDate.getTime() - 1);
|
|
55
|
+
while (occurrences.length < limit) {
|
|
49
56
|
if (rule.until && current > new Date(rule.until))
|
|
50
57
|
break;
|
|
51
|
-
if (rule.count &&
|
|
58
|
+
if (rule.count && occurrences.length >= rule.count)
|
|
52
59
|
break;
|
|
53
60
|
const next = getNextOccurrence(rule, current);
|
|
54
61
|
if (!next)
|
|
55
62
|
break;
|
|
56
63
|
occurrences.push(next);
|
|
57
64
|
current = new Date(next.getTime() + 1);
|
|
58
|
-
count++;
|
|
59
65
|
}
|
|
60
66
|
return occurrences;
|
|
61
67
|
}
|
|
@@ -79,7 +85,7 @@ export function createRecurrence(rule) {
|
|
|
79
85
|
* // Returns Date('2024-01-07') - every other day
|
|
80
86
|
* ```
|
|
81
87
|
*/
|
|
82
|
-
|
|
88
|
+
function getNextOccurrence(rule, afterDate) {
|
|
83
89
|
const after = afterDate ? new Date(afterDate) : new Date();
|
|
84
90
|
const start = new Date(rule.startDate);
|
|
85
91
|
// If afterDate is before start, check if start matches
|
|
@@ -94,7 +100,7 @@ export function getNextOccurrence(rule, afterDate) {
|
|
|
94
100
|
}
|
|
95
101
|
// Start searching from the day after 'after'
|
|
96
102
|
// We want strictly greater than after, so start from the next potential occurrence
|
|
97
|
-
let candidate = addTime(after, 1, 'day');
|
|
103
|
+
let candidate = (0, calculate_js_1.addTime)(after, 1, 'day');
|
|
98
104
|
// Preserve time from start date
|
|
99
105
|
candidate.setHours(start.getHours(), start.getMinutes(), start.getSeconds(), start.getMilliseconds());
|
|
100
106
|
const maxIterations = 1000; // Prevent infinite loops
|
|
@@ -137,7 +143,7 @@ export function getNextOccurrence(rule, afterDate) {
|
|
|
137
143
|
* // Returns all Mondays and Fridays in January 2024
|
|
138
144
|
* ```
|
|
139
145
|
*/
|
|
140
|
-
|
|
146
|
+
function getOccurrencesBetween(rule, start, end, limit = 1000) {
|
|
141
147
|
const startDate = new Date(start);
|
|
142
148
|
const endDate = new Date(end);
|
|
143
149
|
const occurrences = [];
|
|
@@ -179,7 +185,7 @@ export function getOccurrencesBetween(rule, start, end, limit = 1000) {
|
|
|
179
185
|
* isRecurrenceDate(new Date('2024-01-09'), rule); // false (Tuesday)
|
|
180
186
|
* ```
|
|
181
187
|
*/
|
|
182
|
-
|
|
188
|
+
function isRecurrenceDate(date, rule) {
|
|
183
189
|
const checkDate = new Date(date);
|
|
184
190
|
const start = new Date(rule.startDate);
|
|
185
191
|
// Must be on or after start date
|
|
@@ -210,7 +216,7 @@ export function isRecurrenceDate(date, rule) {
|
|
|
210
216
|
* }); // false
|
|
211
217
|
* ```
|
|
212
218
|
*/
|
|
213
|
-
|
|
219
|
+
function isValidRecurrenceRule(rule) {
|
|
214
220
|
if (!rule.frequency || !rule.startDate)
|
|
215
221
|
return false;
|
|
216
222
|
const validFrequencies = ['daily', 'weekly', 'monthly', 'yearly'];
|
|
@@ -264,7 +270,7 @@ export function isValidRecurrenceRule(rule) {
|
|
|
264
270
|
* // "Every 2 weeks on Monday, Wednesday, Friday"
|
|
265
271
|
* ```
|
|
266
272
|
*/
|
|
267
|
-
|
|
273
|
+
function recurrenceToString(rule) {
|
|
268
274
|
const interval = rule.interval || 1;
|
|
269
275
|
let result = interval === 1 ? 'Every' : `Every ${interval}`;
|
|
270
276
|
switch (rule.frequency) {
|
|
@@ -378,27 +384,27 @@ function getNextCandidate(date, rule) {
|
|
|
378
384
|
case 'daily':
|
|
379
385
|
// For daily recurrence, always increment by 1 day to check each day
|
|
380
386
|
// matchesRecurrenceRule will filter based on interval
|
|
381
|
-
return addTime(date, 1, 'day');
|
|
387
|
+
return (0, calculate_js_1.addTime)(date, 1, 'day');
|
|
382
388
|
case 'weekly':
|
|
383
389
|
// If we have specific weekdays, try next day, otherwise skip full interval
|
|
384
390
|
if (rule.byWeekday && rule.byWeekday.length > 0) {
|
|
385
|
-
return addTime(date, 1, 'day');
|
|
391
|
+
return (0, calculate_js_1.addTime)(date, 1, 'day');
|
|
386
392
|
}
|
|
387
|
-
return addTime(date, interval * 7, 'day');
|
|
393
|
+
return (0, calculate_js_1.addTime)(date, interval * 7, 'day');
|
|
388
394
|
case 'monthly':
|
|
389
395
|
// If we have specific days of month, try next day, otherwise skip full interval
|
|
390
396
|
if (rule.byMonthDay && rule.byMonthDay.length > 0) {
|
|
391
|
-
return addTime(date, 1, 'day');
|
|
397
|
+
return (0, calculate_js_1.addTime)(date, 1, 'day');
|
|
392
398
|
}
|
|
393
|
-
return addTime(date, interval, 'month');
|
|
399
|
+
return (0, calculate_js_1.addTime)(date, interval, 'month');
|
|
394
400
|
case 'yearly':
|
|
395
401
|
// If we have specific months or days, try next day, otherwise skip full interval
|
|
396
402
|
if ((rule.byMonth && rule.byMonth.length > 0) ||
|
|
397
403
|
(rule.byMonthDay && rule.byMonthDay.length > 0)) {
|
|
398
|
-
return addTime(date, 1, 'day');
|
|
404
|
+
return (0, calculate_js_1.addTime)(date, 1, 'day');
|
|
399
405
|
}
|
|
400
|
-
return addTime(date, interval, 'year');
|
|
406
|
+
return (0, calculate_js_1.addTime)(date, interval, 'year');
|
|
401
407
|
default:
|
|
402
|
-
return addTime(date, 1, 'day');
|
|
408
|
+
return (0, calculate_js_1.addTime)(date, 1, 'day');
|
|
403
409
|
}
|
|
404
410
|
}
|
package/dist/recurrence.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recurrence.d.ts","sourceRoot":"","sources":["../src/recurrence.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAuB,MAAM,YAAY,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc;;oCAKjB,SAAS;mCACV,SAAS,OAAO,SAAS,UAAU,MAAM;6BAE/C,SAAS;;
|
|
1
|
+
{"version":3,"file":"recurrence.d.ts","sourceRoot":"","sources":["../src/recurrence.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAuB,MAAM,YAAY,CAAC;AAGjF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc;;oCAKjB,SAAS;mCACV,SAAS,OAAO,SAAS,UAAU,MAAM;6BAE/C,SAAS;;EAmBrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAyC1F;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,cAAc,EACpB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,EACd,KAAK,SAAO,GACX,IAAI,EAAE,CA6BR;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAW/E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,OAAO,CAgC5E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CA6C/D"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Scheduling and booking utilities
|
|
4
|
+
* Provides slot generation, availability checking, and conflict detection
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DEFAULT_SCHEDULING_CONFIG = void 0;
|
|
8
|
+
exports.generateSlots = generateSlots;
|
|
9
|
+
exports.generateSlotsForRange = generateSlotsForRange;
|
|
10
|
+
exports.getAvailableSlots = getAvailableSlots;
|
|
11
|
+
exports.findNextAvailable = findNextAvailable;
|
|
12
|
+
exports.isSlotAvailable = isSlotAvailable;
|
|
13
|
+
exports.findConflicts = findConflicts;
|
|
14
|
+
exports.hasConflict = hasConflict;
|
|
15
|
+
exports.addBuffer = addBuffer;
|
|
16
|
+
exports.removeBuffer = removeBuffer;
|
|
17
|
+
exports.expandRecurringAvailability = expandRecurringAvailability;
|
|
18
|
+
exports.mergeBookings = mergeBookings;
|
|
19
|
+
exports.splitSlot = splitSlot;
|
|
20
|
+
const dateRange_js_1 = require("./dateRange.cjs");
|
|
21
|
+
const workingHours_js_1 = require("./workingHours.cjs");
|
|
22
|
+
const recurrence_js_1 = require("./recurrence.cjs");
|
|
23
|
+
/** Default scheduling configuration */
|
|
24
|
+
exports.DEFAULT_SCHEDULING_CONFIG = {
|
|
25
|
+
workingHours: workingHours_js_1.DEFAULT_WORKING_HOURS,
|
|
26
|
+
bufferMinutes: 0,
|
|
27
|
+
slotDuration: 30,
|
|
28
|
+
holidays: []
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Helper to convert DateInput to Date
|
|
32
|
+
*/
|
|
33
|
+
function toDate(input) {
|
|
34
|
+
if (input instanceof Date)
|
|
35
|
+
return new Date(input);
|
|
36
|
+
return new Date(input);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a date is a holiday
|
|
40
|
+
*/
|
|
41
|
+
function isHoliday(date, holidays) {
|
|
42
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
43
|
+
return holidays.some(h => h.toISOString().split('T')[0] === dateStr);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generates time slots for a single day
|
|
47
|
+
* @param date - The date to generate slots for
|
|
48
|
+
* @param config - Scheduling configuration
|
|
49
|
+
* @returns Array of slots for the day
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const slots = generateSlots(new Date('2024-01-15'), { slotDuration: 30 });
|
|
54
|
+
* // Returns 30-minute slots during working hours
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function generateSlots(date, config = {}) {
|
|
58
|
+
const d = toDate(date);
|
|
59
|
+
const cfg = { ...exports.DEFAULT_SCHEDULING_CONFIG, ...config };
|
|
60
|
+
const workingHours = cfg.workingHours ?? workingHours_js_1.DEFAULT_WORKING_HOURS;
|
|
61
|
+
// Check if it's a working day and not a holiday
|
|
62
|
+
if (!(0, workingHours_js_1.isWorkingDay)(d, workingHours))
|
|
63
|
+
return [];
|
|
64
|
+
if (cfg.holidays && isHoliday(d, cfg.holidays))
|
|
65
|
+
return [];
|
|
66
|
+
const slots = [];
|
|
67
|
+
const slotDuration = cfg.slotDuration ?? 30;
|
|
68
|
+
const dayStart = (0, workingHours_js_1.getWorkDayStart)(d, workingHours);
|
|
69
|
+
const dayEnd = (0, workingHours_js_1.getWorkDayEnd)(d, workingHours);
|
|
70
|
+
let current = new Date(dayStart);
|
|
71
|
+
while (current < dayEnd) {
|
|
72
|
+
const slotEnd = new Date(current.getTime() + slotDuration * 60 * 1000);
|
|
73
|
+
// Don't create slots that extend past working hours
|
|
74
|
+
if (slotEnd <= dayEnd) {
|
|
75
|
+
// Check if slot is during working time (not during breaks)
|
|
76
|
+
const midpoint = new Date(current.getTime() + (slotDuration * 60 * 1000) / 2);
|
|
77
|
+
const available = (0, workingHours_js_1.isWorkingTime)(midpoint, workingHours);
|
|
78
|
+
slots.push({
|
|
79
|
+
start: new Date(current),
|
|
80
|
+
end: new Date(slotEnd),
|
|
81
|
+
available
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
current = slotEnd;
|
|
85
|
+
}
|
|
86
|
+
return slots;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generates time slots for a date range
|
|
90
|
+
* @param range - The date range to generate slots for
|
|
91
|
+
* @param config - Scheduling configuration
|
|
92
|
+
* @returns Array of slots for all days in range
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const range = { start: new Date('2024-01-15'), end: new Date('2024-01-17') };
|
|
97
|
+
* const slots = generateSlotsForRange(range, { slotDuration: 60 });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
function generateSlotsForRange(range, config = {}) {
|
|
101
|
+
const slots = [];
|
|
102
|
+
const current = new Date(range.start);
|
|
103
|
+
current.setHours(0, 0, 0, 0);
|
|
104
|
+
const endDate = new Date(range.end);
|
|
105
|
+
endDate.setHours(23, 59, 59, 999);
|
|
106
|
+
while (current <= endDate) {
|
|
107
|
+
const daySlots = generateSlots(current, config);
|
|
108
|
+
slots.push(...daySlots);
|
|
109
|
+
current.setDate(current.getDate() + 1);
|
|
110
|
+
}
|
|
111
|
+
return slots;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Gets available slots for a day, excluding existing bookings
|
|
115
|
+
* @param date - The date to check
|
|
116
|
+
* @param bookings - Existing bookings
|
|
117
|
+
* @param config - Scheduling configuration
|
|
118
|
+
* @returns Array of available slots
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const bookings = [{ start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }];
|
|
123
|
+
* const available = getAvailableSlots(new Date('2024-01-15'), bookings);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
function getAvailableSlots(date, bookings, config = {}) {
|
|
127
|
+
const cfg = { ...exports.DEFAULT_SCHEDULING_CONFIG, ...config };
|
|
128
|
+
const slots = generateSlots(date, cfg);
|
|
129
|
+
const bufferMs = (cfg.bufferMinutes ?? 0) * 60 * 1000;
|
|
130
|
+
return slots.map(slot => {
|
|
131
|
+
// Expand slot by buffer for conflict checking
|
|
132
|
+
const checkRange = {
|
|
133
|
+
start: new Date(slot.start.getTime() - bufferMs),
|
|
134
|
+
end: new Date(slot.end.getTime() + bufferMs)
|
|
135
|
+
};
|
|
136
|
+
const hasConflict = bookings.some(booking => (0, dateRange_js_1.dateRangeOverlap)(checkRange, booking));
|
|
137
|
+
return {
|
|
138
|
+
...slot,
|
|
139
|
+
available: slot.available && !hasConflict
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Finds the next available slot of specified duration
|
|
145
|
+
* @param after - Start searching after this date
|
|
146
|
+
* @param bookings - Existing bookings
|
|
147
|
+
* @param duration - Required slot duration in minutes
|
|
148
|
+
* @param config - Scheduling configuration
|
|
149
|
+
* @returns Next available slot or null if none found within 30 days
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const nextSlot = findNextAvailable(new Date(), bookings, 60);
|
|
154
|
+
* if (nextSlot) console.log(`Next 1-hour slot at ${nextSlot.start}`);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
function findNextAvailable(after, bookings, duration, config = {}) {
|
|
158
|
+
const startDate = toDate(after);
|
|
159
|
+
const cfg = { ...exports.DEFAULT_SCHEDULING_CONFIG, ...config, slotDuration: duration };
|
|
160
|
+
// Search up to 30 days ahead
|
|
161
|
+
for (let dayOffset = 0; dayOffset < 30; dayOffset++) {
|
|
162
|
+
const checkDate = new Date(startDate);
|
|
163
|
+
checkDate.setDate(checkDate.getDate() + dayOffset);
|
|
164
|
+
const availableSlots = getAvailableSlots(checkDate, bookings, cfg);
|
|
165
|
+
for (const slot of availableSlots) {
|
|
166
|
+
if (slot.available && slot.start >= startDate) {
|
|
167
|
+
return slot;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Checks if a slot is available (no conflicts with existing bookings)
|
|
175
|
+
* @param slot - The slot to check
|
|
176
|
+
* @param bookings - Existing bookings
|
|
177
|
+
* @returns True if slot is available
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const slot = { start: new Date('2024-01-15T14:00'), end: new Date('2024-01-15T15:00') };
|
|
182
|
+
* if (isSlotAvailable(slot, existingBookings)) {
|
|
183
|
+
* // Book the slot
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
function isSlotAvailable(slot, bookings) {
|
|
188
|
+
return !bookings.some(booking => (0, dateRange_js_1.dateRangeOverlap)(slot, booking));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Finds bookings that conflict with a proposed time range
|
|
192
|
+
* @param bookings - Existing bookings
|
|
193
|
+
* @param proposed - Proposed time range
|
|
194
|
+
* @returns Array of conflicting bookings
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts
|
|
198
|
+
* const conflicts = findConflicts(existingBookings, { start: propStart, end: propEnd });
|
|
199
|
+
* if (conflicts.length > 0) {
|
|
200
|
+
* console.log('Conflicts with:', conflicts);
|
|
201
|
+
* }
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
function findConflicts(bookings, proposed) {
|
|
205
|
+
return bookings.filter(booking => (0, dateRange_js_1.dateRangeOverlap)(booking, proposed));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks if a proposed time range has any conflicts
|
|
209
|
+
* @param bookings - Existing bookings
|
|
210
|
+
* @param proposed - Proposed time range
|
|
211
|
+
* @returns True if there are conflicts
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* if (hasConflict(existingBookings, proposedMeeting)) {
|
|
216
|
+
* console.log('Time slot not available');
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
function hasConflict(bookings, proposed) {
|
|
221
|
+
return bookings.some(booking => (0, dateRange_js_1.dateRangeOverlap)(booking, proposed));
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Adds buffer time around a slot
|
|
225
|
+
* @param slot - The original slot
|
|
226
|
+
* @param bufferMinutes - Buffer time in minutes
|
|
227
|
+
* @returns New slot with buffer added
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* const slot = { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') };
|
|
232
|
+
* const buffered = addBuffer(slot, 15);
|
|
233
|
+
* // buffered.start = 09:45, buffered.end = 11:15
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function addBuffer(slot, bufferMinutes) {
|
|
237
|
+
const bufferMs = bufferMinutes * 60 * 1000;
|
|
238
|
+
return {
|
|
239
|
+
start: new Date(slot.start.getTime() - bufferMs),
|
|
240
|
+
end: new Date(slot.end.getTime() + bufferMs)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Removes buffer time from a slot
|
|
245
|
+
* @param slot - The buffered slot
|
|
246
|
+
* @param bufferMinutes - Buffer time in minutes to remove
|
|
247
|
+
* @returns New slot with buffer removed
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* const bufferedSlot = { start: new Date('2024-01-15T09:45'), end: new Date('2024-01-15T11:15') };
|
|
252
|
+
* const original = removeBuffer(bufferedSlot, 15);
|
|
253
|
+
* // original.start = 10:00, original.end = 11:00
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function removeBuffer(slot, bufferMinutes) {
|
|
257
|
+
const bufferMs = bufferMinutes * 60 * 1000;
|
|
258
|
+
return {
|
|
259
|
+
start: new Date(slot.start.getTime() + bufferMs),
|
|
260
|
+
end: new Date(slot.end.getTime() - bufferMs)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Expands recurring availability pattern into concrete slots
|
|
265
|
+
* @param pattern - Recurrence pattern
|
|
266
|
+
* @param range - Date range to expand within
|
|
267
|
+
* @param config - Scheduling configuration
|
|
268
|
+
* @returns Array of slots from the recurring pattern
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const pattern = {
|
|
273
|
+
* frequency: 'weekly',
|
|
274
|
+
* startDate: new Date('2024-01-01'),
|
|
275
|
+
* byWeekday: [1, 3, 5], // Mon, Wed, Fri
|
|
276
|
+
* until: new Date('2024-12-31')
|
|
277
|
+
* };
|
|
278
|
+
* const slots = expandRecurringAvailability(pattern, range);
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
function expandRecurringAvailability(pattern, range, config = {}) {
|
|
282
|
+
const occurrences = (0, recurrence_js_1.getOccurrencesBetween)(pattern, range.start, range.end);
|
|
283
|
+
const slots = [];
|
|
284
|
+
for (const occurrence of occurrences) {
|
|
285
|
+
const daySlots = generateSlots(occurrence, config);
|
|
286
|
+
slots.push(...daySlots);
|
|
287
|
+
}
|
|
288
|
+
return slots;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Merges adjacent or overlapping bookings
|
|
292
|
+
* @param bookings - Array of bookings to merge
|
|
293
|
+
* @returns Array of merged bookings
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* const bookings = [
|
|
298
|
+
* { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T10:00') },
|
|
299
|
+
* { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }
|
|
300
|
+
* ];
|
|
301
|
+
* const merged = mergeBookings(bookings);
|
|
302
|
+
* // [{ start: 09:00, end: 11:00 }]
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
function mergeBookings(bookings) {
|
|
306
|
+
if (bookings.length === 0)
|
|
307
|
+
return [];
|
|
308
|
+
const ranges = (0, dateRange_js_1.mergeDateRanges)(bookings);
|
|
309
|
+
return ranges.map(range => ({
|
|
310
|
+
start: range.start,
|
|
311
|
+
end: range.end
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Splits a slot at a specific time
|
|
316
|
+
* @param slot - The slot to split
|
|
317
|
+
* @param at - The time to split at
|
|
318
|
+
* @returns Tuple of two slots, or null if split point is outside slot
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* const slot = { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T11:00'), available: true };
|
|
323
|
+
* const [before, after] = splitSlot(slot, new Date('2024-01-15T10:00'));
|
|
324
|
+
* // before: 09:00-10:00, after: 10:00-11:00
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
function splitSlot(slot, at) {
|
|
328
|
+
const splitTime = toDate(at);
|
|
329
|
+
if (splitTime <= slot.start || splitTime >= slot.end) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
start: new Date(slot.start),
|
|
335
|
+
end: new Date(splitTime),
|
|
336
|
+
available: slot.available
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
start: new Date(splitTime),
|
|
340
|
+
end: new Date(slot.end),
|
|
341
|
+
available: slot.available
|
|
342
|
+
}
|
|
343
|
+
];
|
|
344
|
+
}
|
|
@@ -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
|