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.
Files changed (81) hide show
  1. package/README.md +175 -30
  2. package/dist/{age.js → age.cjs} +14 -6
  3. package/dist/{calculate.js → calculate.cjs} +30 -18
  4. package/dist/{calendar.js → calendar.cjs} +80 -39
  5. package/dist/{calendars.js → calendars.cjs} +48 -23
  6. package/dist/{chain.js → chain.cjs} +41 -40
  7. package/dist/{compare.js → compare.cjs} +58 -28
  8. package/dist/constants.cjs +19 -0
  9. package/dist/{countdown.js → countdown.cjs} +16 -7
  10. package/dist/{cron.js → cron.cjs} +20 -9
  11. package/dist/{dateRange.js → dateRange.cjs} +42 -26
  12. package/dist/{duration.js → duration.cjs} +56 -44
  13. package/dist/esm/chain.js +0 -5
  14. package/dist/esm/finance.d.ts +236 -0
  15. package/dist/esm/finance.d.ts.map +1 -0
  16. package/dist/esm/finance.js +495 -0
  17. package/dist/esm/healthcare.d.ts +260 -0
  18. package/dist/esm/healthcare.d.ts.map +1 -0
  19. package/dist/esm/healthcare.js +447 -0
  20. package/dist/esm/index.d.ts +6 -0
  21. package/dist/esm/index.d.ts.map +1 -1
  22. package/dist/esm/index.js +6 -0
  23. package/dist/esm/naturalLanguage.d.ts +1 -3
  24. package/dist/esm/naturalLanguage.d.ts.map +1 -1
  25. package/dist/esm/naturalLanguage.js +9 -2
  26. package/dist/esm/plugins.d.ts +0 -6
  27. package/dist/esm/plugins.d.ts.map +1 -1
  28. package/dist/esm/plugins.js +36 -42
  29. package/dist/esm/recurrence.d.ts.map +1 -1
  30. package/dist/esm/recurrence.js +3 -5
  31. package/dist/esm/scheduling.d.ts +206 -0
  32. package/dist/esm/scheduling.d.ts.map +1 -0
  33. package/dist/esm/scheduling.js +329 -0
  34. package/dist/esm/timezone.d.ts +6 -1
  35. package/dist/esm/timezone.d.ts.map +1 -1
  36. package/dist/esm/timezone.js +106 -66
  37. package/dist/esm/types.d.ts +0 -4
  38. package/dist/esm/types.d.ts.map +1 -1
  39. package/dist/finance.cjs +512 -0
  40. package/dist/finance.d.ts +236 -0
  41. package/dist/finance.d.ts.map +1 -0
  42. package/dist/{fiscal.js → fiscal.cjs} +36 -17
  43. package/dist/{format.js → format.cjs} +83 -70
  44. package/dist/healthcare.cjs +462 -0
  45. package/dist/healthcare.d.ts +260 -0
  46. package/dist/healthcare.d.ts.map +1 -0
  47. package/dist/{holidays.js → holidays.cjs} +52 -25
  48. package/dist/index.cjs +595 -0
  49. package/dist/index.d.ts +6 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/{interval.js → interval.cjs} +24 -11
  52. package/dist/{iterate.js → iterate.cjs} +84 -41
  53. package/dist/{locale.js → locale.cjs} +54 -26
  54. package/dist/{naturalLanguage.js → naturalLanguage.cjs} +36 -23
  55. package/dist/naturalLanguage.d.ts +1 -3
  56. package/dist/naturalLanguage.d.ts.map +1 -1
  57. package/dist/{parse.js → parse.cjs} +24 -11
  58. package/dist/{performance.js → performance.cjs} +23 -10
  59. package/dist/{plugins.js → plugins.cjs} +48 -47
  60. package/dist/plugins.d.ts +0 -6
  61. package/dist/plugins.d.ts.map +1 -1
  62. package/dist/{precision.js → precision.cjs} +74 -37
  63. package/dist/{rangePresets.js → rangePresets.cjs} +40 -19
  64. package/dist/{recurrence.js → recurrence.cjs} +27 -21
  65. package/dist/recurrence.d.ts.map +1 -1
  66. package/dist/scheduling.cjs +344 -0
  67. package/dist/scheduling.d.ts +206 -0
  68. package/dist/scheduling.d.ts.map +1 -0
  69. package/dist/{serialize.js → serialize.cjs} +36 -17
  70. package/dist/{temporal.js → temporal.cjs} +28 -13
  71. package/dist/{timezone.js → timezone.cjs} +140 -82
  72. package/dist/timezone.d.ts +6 -1
  73. package/dist/timezone.d.ts.map +1 -1
  74. package/dist/{types.js → types.cjs} +9 -3
  75. package/dist/types.d.ts +0 -4
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/{validate.js → validate.cjs} +54 -26
  78. package/dist/{workingHours.js → workingHours.cjs} +36 -17
  79. package/package.json +52 -34
  80. package/dist/constants.js +0 -16
  81. package/dist/index.js +0 -66
@@ -19,38 +19,20 @@
19
19
  * chain(new Date()).nextMonday().format('YYYY-MM-DD');
20
20
  * ```
21
21
  */
22
- // Import ChainedDate class directly - safe because chain.js doesn't import plugins.js
23
- // We need the actual class (not just the type) to modify its prototype
24
- let ChainedDateConstructor = null;
22
+ import { ChainedDate } from './chain.js';
25
23
  /**
26
- * Get ChainedDate class lazily to ensure it's fully initialized
24
+ * Get ChainedDate class for prototype mutation.
25
+ * Importing `plugins` now brings in `chain` directly, so the class is available
26
+ * without a hidden global handshake.
27
27
  */
28
28
  function getChainedDate() {
29
- if (!ChainedDateConstructor) {
30
- // Use dynamic import that works in both CJS and ESM
31
- try {
32
- // Try ESM import path first
33
- ChainedDateConstructor = globalThis.__chainedDateClass;
34
- if (!ChainedDateConstructor) {
35
- // Fallback: The class will be set by chain.js when it loads
36
- throw new Error('ChainedDate not yet loaded. Import chain.js before using plugins.');
37
- }
38
- }
39
- catch (e) {
40
- throw new Error('ChainedDate class not available. Ensure chain.js is imported before registering plugins.');
41
- }
29
+ if (!ChainedDate) {
30
+ throw new Error('ChainedDate export is not available yet. Import chain.js before registering plugins.');
42
31
  }
43
- return ChainedDateConstructor;
44
- }
45
- /**
46
- * Initialize the plugin system with ChainedDate class
47
- * This is called automatically when chain.js is imported
48
- * @internal
49
- */
50
- export function __initPluginSystem(ChainedDateClass) {
51
- ChainedDateConstructor = ChainedDateClass;
32
+ return ChainedDate;
52
33
  }
53
34
  const registry = {};
35
+ const methodStates = {};
54
36
  /**
55
37
  * Extend ChainedDate with custom methods
56
38
  *
@@ -80,23 +62,23 @@ export function extend(pluginName, methods) {
80
62
  if (registry[pluginName]) {
81
63
  throw new Error(`Plugin "${pluginName}" is already registered. Use a different name or uninstall first.`);
82
64
  }
83
- // Get ChainedDate class and save original methods before overwriting
84
65
  const ChainedDateClass = getChainedDate();
85
- const originalMethods = new Map();
86
66
  Object.entries(methods).forEach(([methodName, fn]) => {
87
- // Save original method if it exists
67
+ const current = ChainedDateClass.prototype[methodName];
68
+ if (!methodStates[methodName]) {
69
+ methodStates[methodName] = {
70
+ original: typeof current === 'function' ? current : undefined,
71
+ owners: []
72
+ };
73
+ }
88
74
  if (methodName in ChainedDateClass.prototype) {
89
- const original = ChainedDateClass.prototype[methodName];
90
- if (typeof original === 'function') {
91
- originalMethods.set(methodName, original);
92
- }
93
75
  console.warn(`Method "${methodName}" already exists on ChainedDate and will be overwritten`);
94
76
  }
77
+ methodStates[methodName].owners.push(pluginName);
95
78
  // Add the plugin method
96
79
  ChainedDateClass.prototype[methodName] = fn;
97
80
  });
98
- // Register the plugin with its original methods
99
- registry[pluginName] = { plugin: methods, originalMethods };
81
+ registry[pluginName] = { plugin: methods };
100
82
  }
101
83
  /**
102
84
  * Remove a plugin and its methods from ChainedDate
@@ -113,20 +95,32 @@ export function uninstall(pluginName) {
113
95
  if (!entry) {
114
96
  throw new Error(`Plugin "${pluginName}" is not registered`);
115
97
  }
116
- // Get ChainedDate class and restore/remove methods
117
98
  const ChainedDateClass = getChainedDate();
118
99
  Object.keys(entry.plugin).forEach((methodName) => {
119
- // If there was an original method, restore it
120
- const original = entry.originalMethods.get(methodName);
121
- if (original) {
122
- ChainedDateClass.prototype[methodName] = original;
100
+ const state = methodStates[methodName];
101
+ if (!state) {
102
+ delete ChainedDateClass.prototype[methodName];
103
+ return;
104
+ }
105
+ state.owners = state.owners.filter((owner) => owner !== pluginName);
106
+ const nextOwner = state.owners[state.owners.length - 1];
107
+ if (nextOwner) {
108
+ const nextPlugin = registry[nextOwner];
109
+ if (nextPlugin && methodName in nextPlugin.plugin) {
110
+ ChainedDateClass.prototype[methodName] = nextPlugin.plugin[methodName];
111
+ return;
112
+ }
113
+ delete ChainedDateClass.prototype[methodName];
114
+ return;
115
+ }
116
+ if (state.original) {
117
+ ChainedDateClass.prototype[methodName] = state.original;
123
118
  }
124
119
  else {
125
- // Otherwise, delete the method entirely
126
120
  delete ChainedDateClass.prototype[methodName];
127
121
  }
122
+ delete methodStates[methodName];
128
123
  });
129
- // Remove from registry
130
124
  delete registry[pluginName];
131
125
  }
132
126
  /**
@@ -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;;EAqBrC;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"}
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"}
@@ -43,19 +43,17 @@ export function createRecurrence(rule) {
43
43
  isRecurrenceDate: (date) => isRecurrenceDate(date, rule),
44
44
  getAllOccurrences: (limit = 100) => {
45
45
  const occurrences = [];
46
- let current = new Date(startDate);
47
- let count = 0;
48
- while (count < limit) {
46
+ let current = new Date(startDate.getTime() - 1);
47
+ while (occurrences.length < limit) {
49
48
  if (rule.until && current > new Date(rule.until))
50
49
  break;
51
- if (rule.count && count >= rule.count)
50
+ if (rule.count && occurrences.length >= rule.count)
52
51
  break;
53
52
  const next = getNextOccurrence(rule, current);
54
53
  if (!next)
55
54
  break;
56
55
  occurrences.push(next);
57
56
  current = new Date(next.getTime() + 1);
58
- count++;
59
57
  }
60
58
  return occurrences;
61
59
  }
@@ -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"}
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @fileoverview Scheduling and booking utilities
3
+ * Provides slot generation, availability checking, and conflict detection
4
+ */
5
+ import { dateRangeOverlap, mergeDateRanges } from './dateRange.js';
6
+ import { isWorkingDay, isWorkingTime, DEFAULT_WORKING_HOURS, getWorkDayStart, getWorkDayEnd } from './workingHours.js';
7
+ import { getOccurrencesBetween } from './recurrence.js';
8
+ /** Default scheduling configuration */
9
+ export const DEFAULT_SCHEDULING_CONFIG = {
10
+ workingHours: DEFAULT_WORKING_HOURS,
11
+ bufferMinutes: 0,
12
+ slotDuration: 30,
13
+ holidays: []
14
+ };
15
+ /**
16
+ * Helper to convert DateInput to Date
17
+ */
18
+ function toDate(input) {
19
+ if (input instanceof Date)
20
+ return new Date(input);
21
+ return new Date(input);
22
+ }
23
+ /**
24
+ * Check if a date is a holiday
25
+ */
26
+ function isHoliday(date, holidays) {
27
+ const dateStr = date.toISOString().split('T')[0];
28
+ return holidays.some(h => h.toISOString().split('T')[0] === dateStr);
29
+ }
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 function generateSlots(date, config = {}) {
43
+ const d = toDate(date);
44
+ const cfg = { ...DEFAULT_SCHEDULING_CONFIG, ...config };
45
+ const workingHours = cfg.workingHours ?? DEFAULT_WORKING_HOURS;
46
+ // Check if it's a working day and not a holiday
47
+ if (!isWorkingDay(d, workingHours))
48
+ return [];
49
+ if (cfg.holidays && isHoliday(d, cfg.holidays))
50
+ return [];
51
+ const slots = [];
52
+ const slotDuration = cfg.slotDuration ?? 30;
53
+ const dayStart = getWorkDayStart(d, workingHours);
54
+ const dayEnd = getWorkDayEnd(d, workingHours);
55
+ let current = new Date(dayStart);
56
+ while (current < dayEnd) {
57
+ const slotEnd = new Date(current.getTime() + slotDuration * 60 * 1000);
58
+ // Don't create slots that extend past working hours
59
+ if (slotEnd <= dayEnd) {
60
+ // Check if slot is during working time (not during breaks)
61
+ const midpoint = new Date(current.getTime() + (slotDuration * 60 * 1000) / 2);
62
+ const available = isWorkingTime(midpoint, workingHours);
63
+ slots.push({
64
+ start: new Date(current),
65
+ end: new Date(slotEnd),
66
+ available
67
+ });
68
+ }
69
+ current = slotEnd;
70
+ }
71
+ return slots;
72
+ }
73
+ /**
74
+ * Generates time slots for a date range
75
+ * @param range - The date range to generate slots for
76
+ * @param config - Scheduling configuration
77
+ * @returns Array of slots for all days in range
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const range = { start: new Date('2024-01-15'), end: new Date('2024-01-17') };
82
+ * const slots = generateSlotsForRange(range, { slotDuration: 60 });
83
+ * ```
84
+ */
85
+ export function generateSlotsForRange(range, config = {}) {
86
+ const slots = [];
87
+ const current = new Date(range.start);
88
+ current.setHours(0, 0, 0, 0);
89
+ const endDate = new Date(range.end);
90
+ endDate.setHours(23, 59, 59, 999);
91
+ while (current <= endDate) {
92
+ const daySlots = generateSlots(current, config);
93
+ slots.push(...daySlots);
94
+ current.setDate(current.getDate() + 1);
95
+ }
96
+ return slots;
97
+ }
98
+ /**
99
+ * Gets available slots for a day, excluding existing bookings
100
+ * @param date - The date to check
101
+ * @param bookings - Existing bookings
102
+ * @param config - Scheduling configuration
103
+ * @returns Array of available slots
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const bookings = [{ start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }];
108
+ * const available = getAvailableSlots(new Date('2024-01-15'), bookings);
109
+ * ```
110
+ */
111
+ export function getAvailableSlots(date, bookings, config = {}) {
112
+ const cfg = { ...DEFAULT_SCHEDULING_CONFIG, ...config };
113
+ const slots = generateSlots(date, cfg);
114
+ const bufferMs = (cfg.bufferMinutes ?? 0) * 60 * 1000;
115
+ return slots.map(slot => {
116
+ // Expand slot by buffer for conflict checking
117
+ const checkRange = {
118
+ start: new Date(slot.start.getTime() - bufferMs),
119
+ end: new Date(slot.end.getTime() + bufferMs)
120
+ };
121
+ const hasConflict = bookings.some(booking => dateRangeOverlap(checkRange, booking));
122
+ return {
123
+ ...slot,
124
+ available: slot.available && !hasConflict
125
+ };
126
+ });
127
+ }
128
+ /**
129
+ * Finds the next available slot of specified duration
130
+ * @param after - Start searching after this date
131
+ * @param bookings - Existing bookings
132
+ * @param duration - Required slot duration in minutes
133
+ * @param config - Scheduling configuration
134
+ * @returns Next available slot or null if none found within 30 days
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const nextSlot = findNextAvailable(new Date(), bookings, 60);
139
+ * if (nextSlot) console.log(`Next 1-hour slot at ${nextSlot.start}`);
140
+ * ```
141
+ */
142
+ export function findNextAvailable(after, bookings, duration, config = {}) {
143
+ const startDate = toDate(after);
144
+ const cfg = { ...DEFAULT_SCHEDULING_CONFIG, ...config, slotDuration: duration };
145
+ // Search up to 30 days ahead
146
+ for (let dayOffset = 0; dayOffset < 30; dayOffset++) {
147
+ const checkDate = new Date(startDate);
148
+ checkDate.setDate(checkDate.getDate() + dayOffset);
149
+ const availableSlots = getAvailableSlots(checkDate, bookings, cfg);
150
+ for (const slot of availableSlots) {
151
+ if (slot.available && slot.start >= startDate) {
152
+ return slot;
153
+ }
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ /**
159
+ * Checks if a slot is available (no conflicts with existing bookings)
160
+ * @param slot - The slot to check
161
+ * @param bookings - Existing bookings
162
+ * @returns True if slot is available
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const slot = { start: new Date('2024-01-15T14:00'), end: new Date('2024-01-15T15:00') };
167
+ * if (isSlotAvailable(slot, existingBookings)) {
168
+ * // Book the slot
169
+ * }
170
+ * ```
171
+ */
172
+ export function isSlotAvailable(slot, bookings) {
173
+ return !bookings.some(booking => dateRangeOverlap(slot, booking));
174
+ }
175
+ /**
176
+ * Finds bookings that conflict with a proposed time range
177
+ * @param bookings - Existing bookings
178
+ * @param proposed - Proposed time range
179
+ * @returns Array of conflicting bookings
180
+ *
181
+ * @example
182
+ * ```ts
183
+ * const conflicts = findConflicts(existingBookings, { start: propStart, end: propEnd });
184
+ * if (conflicts.length > 0) {
185
+ * console.log('Conflicts with:', conflicts);
186
+ * }
187
+ * ```
188
+ */
189
+ export function findConflicts(bookings, proposed) {
190
+ return bookings.filter(booking => dateRangeOverlap(booking, proposed));
191
+ }
192
+ /**
193
+ * Checks if a proposed time range has any conflicts
194
+ * @param bookings - Existing bookings
195
+ * @param proposed - Proposed time range
196
+ * @returns True if there are conflicts
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * if (hasConflict(existingBookings, proposedMeeting)) {
201
+ * console.log('Time slot not available');
202
+ * }
203
+ * ```
204
+ */
205
+ export function hasConflict(bookings, proposed) {
206
+ return bookings.some(booking => dateRangeOverlap(booking, proposed));
207
+ }
208
+ /**
209
+ * Adds buffer time around a slot
210
+ * @param slot - The original slot
211
+ * @param bufferMinutes - Buffer time in minutes
212
+ * @returns New slot with buffer added
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const slot = { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') };
217
+ * const buffered = addBuffer(slot, 15);
218
+ * // buffered.start = 09:45, buffered.end = 11:15
219
+ * ```
220
+ */
221
+ export function addBuffer(slot, bufferMinutes) {
222
+ const bufferMs = bufferMinutes * 60 * 1000;
223
+ return {
224
+ start: new Date(slot.start.getTime() - bufferMs),
225
+ end: new Date(slot.end.getTime() + bufferMs)
226
+ };
227
+ }
228
+ /**
229
+ * Removes buffer time from a slot
230
+ * @param slot - The buffered slot
231
+ * @param bufferMinutes - Buffer time in minutes to remove
232
+ * @returns New slot with buffer removed
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const bufferedSlot = { start: new Date('2024-01-15T09:45'), end: new Date('2024-01-15T11:15') };
237
+ * const original = removeBuffer(bufferedSlot, 15);
238
+ * // original.start = 10:00, original.end = 11:00
239
+ * ```
240
+ */
241
+ export function removeBuffer(slot, bufferMinutes) {
242
+ const bufferMs = bufferMinutes * 60 * 1000;
243
+ return {
244
+ start: new Date(slot.start.getTime() + bufferMs),
245
+ end: new Date(slot.end.getTime() - bufferMs)
246
+ };
247
+ }
248
+ /**
249
+ * Expands recurring availability pattern into concrete slots
250
+ * @param pattern - Recurrence pattern
251
+ * @param range - Date range to expand within
252
+ * @param config - Scheduling configuration
253
+ * @returns Array of slots from the recurring pattern
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const pattern = {
258
+ * frequency: 'weekly',
259
+ * startDate: new Date('2024-01-01'),
260
+ * byWeekday: [1, 3, 5], // Mon, Wed, Fri
261
+ * until: new Date('2024-12-31')
262
+ * };
263
+ * const slots = expandRecurringAvailability(pattern, range);
264
+ * ```
265
+ */
266
+ export function expandRecurringAvailability(pattern, range, config = {}) {
267
+ const occurrences = getOccurrencesBetween(pattern, range.start, range.end);
268
+ const slots = [];
269
+ for (const occurrence of occurrences) {
270
+ const daySlots = generateSlots(occurrence, config);
271
+ slots.push(...daySlots);
272
+ }
273
+ return slots;
274
+ }
275
+ /**
276
+ * Merges adjacent or overlapping bookings
277
+ * @param bookings - Array of bookings to merge
278
+ * @returns Array of merged bookings
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * const bookings = [
283
+ * { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T10:00') },
284
+ * { start: new Date('2024-01-15T10:00'), end: new Date('2024-01-15T11:00') }
285
+ * ];
286
+ * const merged = mergeBookings(bookings);
287
+ * // [{ start: 09:00, end: 11:00 }]
288
+ * ```
289
+ */
290
+ export function mergeBookings(bookings) {
291
+ if (bookings.length === 0)
292
+ return [];
293
+ const ranges = mergeDateRanges(bookings);
294
+ return ranges.map(range => ({
295
+ start: range.start,
296
+ end: range.end
297
+ }));
298
+ }
299
+ /**
300
+ * Splits a slot at a specific time
301
+ * @param slot - The slot to split
302
+ * @param at - The time to split at
303
+ * @returns Tuple of two slots, or null if split point is outside slot
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * const slot = { start: new Date('2024-01-15T09:00'), end: new Date('2024-01-15T11:00'), available: true };
308
+ * const [before, after] = splitSlot(slot, new Date('2024-01-15T10:00'));
309
+ * // before: 09:00-10:00, after: 10:00-11:00
310
+ * ```
311
+ */
312
+ export function splitSlot(slot, at) {
313
+ const splitTime = toDate(at);
314
+ if (splitTime <= slot.start || splitTime >= slot.end) {
315
+ return null;
316
+ }
317
+ return [
318
+ {
319
+ start: new Date(slot.start),
320
+ end: new Date(splitTime),
321
+ available: slot.available
322
+ },
323
+ {
324
+ start: new Date(splitTime),
325
+ end: new Date(slot.end),
326
+ available: slot.available
327
+ }
328
+ ];
329
+ }