scschedule 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -41
- package/dist/cleanupExpiredOverridesFromSchedule.d.ts +4 -0
- package/dist/cleanupExpiredOverridesFromSchedule.js +4 -0
- package/dist/constants.d.ts +1 -3
- package/dist/constants.js +1 -3
- package/dist/getAvailableRangesFromSchedule.d.ts +10 -0
- package/dist/getAvailableRangesFromSchedule.js +19 -0
- package/dist/getNextAvailableFromSchedule.d.ts +9 -8
- package/dist/getNextAvailableFromSchedule.js +14 -8
- package/dist/getNextUnavailableFromSchedule.d.ts +19 -9
- package/dist/getNextUnavailableFromSchedule.js +124 -63
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/internal/getApplicableRuleForDate.d.ts +11 -11
- package/dist/internal/getApplicableRuleForDate.js +11 -7
- package/dist/internal/index.d.ts +1 -3
- package/dist/internal/index.js +1 -3
- package/dist/internal/validateNoEmptyWeekdays.js +2 -1
- package/dist/internal/validateNoOverlappingRules.js +5 -4
- package/dist/internal/validateNoOverlappingTimesInRule.js +2 -1
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +4 -2
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +71 -54
- package/dist/internal/validateNonEmptyTimes.js +2 -1
- package/dist/internal/validateScDateFormats.js +2 -1
- package/dist/isScheduleAvailable.d.ts +9 -0
- package/dist/isScheduleAvailable.js +19 -2
- package/dist/types.d.ts +12 -18
- package/dist/validateSchedule.d.ts +4 -2
- package/dist/validateSchedule.js +4 -4
- package/package.json +7 -7
- package/dist/internal/isValidTimezone.d.ts +0 -4
- package/dist/internal/isValidTimezone.js +0 -12
- package/dist/internal/validateTimezone.d.ts +0 -5
- package/dist/internal/validateTimezone.js +0 -16
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ scschedule is a TypeScript library for managing time-based availability patterns
|
|
|
16
16
|
- **Recurring patterns**: Define weekly schedules with different hours for different days
|
|
17
17
|
- **Override system**: Add exceptions for holidays, special events, or schedule changes
|
|
18
18
|
- **Cross-midnight support**: Handle time ranges that span midnight (e.g., 22:00-02:00)
|
|
19
|
-
- **
|
|
19
|
+
- **Time zone aware**: Time zone passed at the call site where needed (DST-safe arithmetic)
|
|
20
20
|
- **DST handling**: Properly handles daylight saving time transitions
|
|
21
21
|
- **Immutable**: All operations return new instances
|
|
22
22
|
- **Type-safe validation**: Discriminated union errors with detailed information
|
|
@@ -46,12 +46,11 @@ import {
|
|
|
46
46
|
} from 'scschedule'
|
|
47
47
|
import { sDate, sTime, sWeekdays, getTimestampNow } from 'scdate'
|
|
48
48
|
|
|
49
|
-
// Define a restaurant schedule:
|
|
49
|
+
// Define a restaurant schedule: Monday-Saturday, 11:00-22:00
|
|
50
50
|
const restaurant: Schedule = {
|
|
51
|
-
timezone: 'America/Puerto_Rico',
|
|
52
51
|
weekly: [
|
|
53
52
|
{
|
|
54
|
-
weekdays: sWeekdays('-MTWTFS'), //
|
|
53
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
55
54
|
times: [{ from: sTime('11:00'), to: sTime('22:00') }],
|
|
56
55
|
},
|
|
57
56
|
],
|
|
@@ -68,8 +67,8 @@ const now = getTimestampNow('America/Puerto_Rico')
|
|
|
68
67
|
const isOpen = isScheduleAvailable(restaurant, now)
|
|
69
68
|
console.log(`Restaurant is ${isOpen ? 'open' : 'closed'}`)
|
|
70
69
|
|
|
71
|
-
// Find next opening time
|
|
72
|
-
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
70
|
+
// Find next opening time (search up to 30 days ahead)
|
|
71
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now, 30)
|
|
73
72
|
if (nextOpen) {
|
|
74
73
|
console.log(`Next opening: ${nextOpen.timestamp}`)
|
|
75
74
|
}
|
|
@@ -81,14 +80,12 @@ if (nextOpen) {
|
|
|
81
80
|
|
|
82
81
|
A `Schedule` consists of:
|
|
83
82
|
|
|
84
|
-
- **
|
|
85
|
-
- **weekly**: Base recurring schedule (array of `WeeklyScheduleRule`)
|
|
83
|
+
- **weekly**: Base recurring schedule — `true` (available 24/7), an array of `WeeklyScheduleRule` (time-based), or `[]` (never available; overrides can open windows)
|
|
86
84
|
- **overrides** (optional): Date-specific exceptions (array of `OverrideScheduleRule`)
|
|
87
85
|
|
|
88
86
|
```typescript
|
|
89
87
|
interface Schedule {
|
|
90
|
-
|
|
91
|
-
weekly: WeeklyScheduleRule[]
|
|
88
|
+
weekly: WeeklyScheduleRule[] | true
|
|
92
89
|
overrides?: OverrideScheduleRule[]
|
|
93
90
|
}
|
|
94
91
|
```
|
|
@@ -139,6 +136,17 @@ Rules are evaluated in order of priority:
|
|
|
139
136
|
|
|
140
137
|
### Validation
|
|
141
138
|
|
|
139
|
+
#### `isValidTimeZone(timeZone: string): boolean`
|
|
140
|
+
|
|
141
|
+
Re-exported from `scdate`. Checks if a string is a valid IANA time zone identifier (using `Intl.supportedValuesOf('timeZone')`).
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { isValidTimeZone } from 'scschedule' // or from 'scdate'
|
|
145
|
+
|
|
146
|
+
isValidTimeZone('America/New_York') // true
|
|
147
|
+
isValidTimeZone('Invalid/Timezone') // false
|
|
148
|
+
```
|
|
149
|
+
|
|
142
150
|
#### `validateSchedule(schedule: Schedule): ValidationResult`
|
|
143
151
|
|
|
144
152
|
Validates a schedule and returns detailed errors.
|
|
@@ -151,9 +159,6 @@ const result = validateSchedule(mySchedule)
|
|
|
151
159
|
if (!result.valid) {
|
|
152
160
|
result.errors.forEach((error) => {
|
|
153
161
|
switch (error.issue) {
|
|
154
|
-
case ValidationIssue.InvalidTimezone:
|
|
155
|
-
console.error(`Invalid timezone: ${error.timezone}`)
|
|
156
|
-
break
|
|
157
162
|
case ValidationIssue.OverlappingSpecificOverrides:
|
|
158
163
|
console.error(
|
|
159
164
|
`Overlapping overrides at indexes: ${error.overrideIndexes}`,
|
|
@@ -167,13 +172,16 @@ if (!result.valid) {
|
|
|
167
172
|
|
|
168
173
|
**Validation checks**:
|
|
169
174
|
|
|
170
|
-
- Valid
|
|
175
|
+
- Valid scdate formats (SDate, STime, SWeekdays)
|
|
176
|
+
- Override `to` date must not be before `from` date
|
|
171
177
|
- No duplicate overrides (identical from/to dates)
|
|
172
178
|
- No overlapping specific overrides (hierarchical nesting allowed)
|
|
173
|
-
- No overlapping time ranges within
|
|
179
|
+
- No overlapping time ranges within a single rule (same weekday)
|
|
180
|
+
- No overlapping rules within the weekly schedule (shared weekdays with overlapping times)
|
|
181
|
+
- No overlapping rules within the same override (shared weekdays with overlapping times)
|
|
182
|
+
- No cross-midnight spillover conflicts at override boundaries
|
|
174
183
|
- All rules have at least one time range
|
|
175
|
-
-
|
|
176
|
-
- No empty weekdays patterns (e.g., '-------' with no days selected)
|
|
184
|
+
- No empty weekdays patterns (e.g., `'-------'` with no days selected)
|
|
177
185
|
- Override weekdays must match at least one date in the override's date range
|
|
178
186
|
|
|
179
187
|
### Schedule Management
|
|
@@ -208,27 +216,32 @@ const isOpen = isScheduleAvailable(
|
|
|
208
216
|
)
|
|
209
217
|
```
|
|
210
218
|
|
|
211
|
-
#### `getNextAvailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch
|
|
219
|
+
#### `getNextAvailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch: number): STimestamp | undefined`
|
|
212
220
|
|
|
213
|
-
Find the next available timestamp from a given time. Searches up to `maxDaysToSearch` days
|
|
221
|
+
Find the next available timestamp from a given time. Searches up to `maxDaysToSearch` days forward.
|
|
214
222
|
|
|
215
223
|
```typescript
|
|
216
224
|
import { getNextAvailableFromSchedule } from 'scschedule'
|
|
217
225
|
|
|
218
|
-
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
226
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now, 30)
|
|
219
227
|
if (nextOpen) {
|
|
220
228
|
console.log(`Opens at: ${nextOpen.timestamp}`)
|
|
221
229
|
}
|
|
222
230
|
```
|
|
223
231
|
|
|
224
|
-
#### `getNextUnavailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string): STimestamp | undefined`
|
|
232
|
+
#### `getNextUnavailableFromSchedule(schedule: Schedule, timeZone: string, fromTimestamp: STimestamp | string, maxDaysToSearch: number): STimestamp | undefined`
|
|
225
233
|
|
|
226
|
-
Find the next unavailable timestamp from a given time.
|
|
234
|
+
Find the next unavailable timestamp from a given time. Requires a time zone for DST-safe timestamp arithmetic. Searches up to `maxDaysToSearch` days forward.
|
|
227
235
|
|
|
228
236
|
```typescript
|
|
229
237
|
import { getNextUnavailableFromSchedule } from 'scschedule'
|
|
230
238
|
|
|
231
|
-
const nextClosed = getNextUnavailableFromSchedule(
|
|
239
|
+
const nextClosed = getNextUnavailableFromSchedule(
|
|
240
|
+
restaurant,
|
|
241
|
+
'America/Puerto_Rico',
|
|
242
|
+
now,
|
|
243
|
+
30,
|
|
244
|
+
)
|
|
232
245
|
if (nextClosed) {
|
|
233
246
|
console.log(`Closes at: ${nextClosed.timestamp}`)
|
|
234
247
|
}
|
|
@@ -262,10 +275,9 @@ import { Schedule } from 'scschedule'
|
|
|
262
275
|
import { sTime, sWeekdays } from 'scdate'
|
|
263
276
|
|
|
264
277
|
const restaurant: Schedule = {
|
|
265
|
-
timezone: 'America/Puerto_Rico',
|
|
266
278
|
weekly: [
|
|
267
279
|
{
|
|
268
|
-
weekdays: sWeekdays('-MTWTFS'), //
|
|
280
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
269
281
|
times: [{ from: sTime('11:00'), to: sTime('22:00') }],
|
|
270
282
|
},
|
|
271
283
|
],
|
|
@@ -276,10 +288,9 @@ const restaurant: Schedule = {
|
|
|
276
288
|
|
|
277
289
|
```typescript
|
|
278
290
|
const restaurant: Schedule = {
|
|
279
|
-
timezone: 'America/Puerto_Rico',
|
|
280
291
|
weekly: [
|
|
281
292
|
{
|
|
282
|
-
weekdays: sWeekdays('-MTWTFS'), //
|
|
293
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
283
294
|
times: [
|
|
284
295
|
{ from: sTime('11:00'), to: sTime('14:00') }, // Lunch
|
|
285
296
|
{ from: sTime('17:00'), to: sTime('22:00') }, // Dinner
|
|
@@ -293,7 +304,6 @@ const restaurant: Schedule = {
|
|
|
293
304
|
|
|
294
305
|
```typescript
|
|
295
306
|
const restaurant: Schedule = {
|
|
296
|
-
timezone: 'America/Puerto_Rico',
|
|
297
307
|
weekly: [
|
|
298
308
|
{
|
|
299
309
|
// Weekdays: longer hours
|
|
@@ -371,7 +381,6 @@ const newSchedule: Schedule = {
|
|
|
371
381
|
|
|
372
382
|
```typescript
|
|
373
383
|
const lateNightBar: Schedule = {
|
|
374
|
-
timezone: 'America/Puerto_Rico',
|
|
375
384
|
weekly: [
|
|
376
385
|
{
|
|
377
386
|
weekdays: sWeekdays('----TFS'), // Thu-Sat
|
|
@@ -386,16 +395,65 @@ const lateNightBar: Schedule = {
|
|
|
386
395
|
// - Saturday: 20:00-23:59, Sunday: 00:00-03:00
|
|
387
396
|
```
|
|
388
397
|
|
|
398
|
+
### Always Available (`weekly: true`)
|
|
399
|
+
|
|
400
|
+
Use `weekly: true` when an entity is available 24/7 by default. This is useful for items that inherit availability from a parent schedule (e.g., menu items that go through the restaurant's schedule filter first).
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
// Menu item available 24/7 (restaurant hours handle the filtering)
|
|
404
|
+
const menuItem: Schedule = {
|
|
405
|
+
weekly: true,
|
|
406
|
+
overrides: [
|
|
407
|
+
{
|
|
408
|
+
// Except Christmas Day
|
|
409
|
+
from: sDate('2025-12-25'),
|
|
410
|
+
to: sDate('2025-12-25'),
|
|
411
|
+
rules: [],
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Closed by Default (`weekly: []`)
|
|
418
|
+
|
|
419
|
+
Use `weekly: []` when an entity is unavailable by default and only opens during specific override periods.
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// Pop-up shop: closed by default, open only during specific events
|
|
423
|
+
const popUpShop: Schedule = {
|
|
424
|
+
weekly: [],
|
|
425
|
+
overrides: [
|
|
426
|
+
{
|
|
427
|
+
from: sDate('2025-12-20'),
|
|
428
|
+
to: sDate('2025-12-24'),
|
|
429
|
+
rules: [
|
|
430
|
+
{
|
|
431
|
+
weekdays: sWeekdays('SMTWTFS'),
|
|
432
|
+
times: [{ from: sTime('10:00'), to: sTime('18:00') }],
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
389
440
|
### Multiple Schedules (Layered Availability)
|
|
390
441
|
|
|
391
442
|
```typescript
|
|
392
443
|
import { isScheduleAvailable } from 'scschedule'
|
|
393
444
|
|
|
394
445
|
const businessHours: Schedule = {
|
|
395
|
-
|
|
446
|
+
weekly: [
|
|
447
|
+
{
|
|
448
|
+
weekdays: sWeekdays('-MTWTFS'),
|
|
449
|
+
times: [{ from: sTime('11:00'), to: sTime('22:00') }],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
396
452
|
}
|
|
453
|
+
|
|
454
|
+
// Menu available 24/7 — restaurant hours do the filtering
|
|
397
455
|
const breakfastMenu: Schedule = {
|
|
398
|
-
|
|
456
|
+
weekly: true,
|
|
399
457
|
}
|
|
400
458
|
|
|
401
459
|
// Both must be available
|
|
@@ -411,8 +469,16 @@ The library uses discriminated unions for type-safe error handling:
|
|
|
411
469
|
```typescript
|
|
412
470
|
type ValidationError =
|
|
413
471
|
| {
|
|
414
|
-
issue: ValidationIssue.
|
|
415
|
-
|
|
472
|
+
issue: ValidationIssue.InvalidScDateFormat
|
|
473
|
+
field: string
|
|
474
|
+
value: string
|
|
475
|
+
expectedFormat: string
|
|
476
|
+
}
|
|
477
|
+
| {
|
|
478
|
+
issue: ValidationIssue.InvalidOverrideDateOrder
|
|
479
|
+
overrideIndex: number
|
|
480
|
+
from: string
|
|
481
|
+
to: string
|
|
416
482
|
}
|
|
417
483
|
| {
|
|
418
484
|
issue: ValidationIssue.DuplicateOverrides
|
|
@@ -433,6 +499,17 @@ type ValidationError =
|
|
|
433
499
|
}
|
|
434
500
|
timeRangeIndexes: [number, number]
|
|
435
501
|
}
|
|
502
|
+
| {
|
|
503
|
+
issue: ValidationIssue.OverlappingRulesInWeekly
|
|
504
|
+
ruleIndexes: [number, number]
|
|
505
|
+
weekday: Weekday
|
|
506
|
+
}
|
|
507
|
+
| {
|
|
508
|
+
issue: ValidationIssue.OverlappingRulesInOverride
|
|
509
|
+
overrideIndex: number
|
|
510
|
+
ruleIndexes: [number, number]
|
|
511
|
+
weekday: Weekday
|
|
512
|
+
}
|
|
436
513
|
| {
|
|
437
514
|
issue: ValidationIssue.EmptyTimes
|
|
438
515
|
location:
|
|
@@ -443,12 +520,6 @@ type ValidationError =
|
|
|
443
520
|
ruleIndex: number
|
|
444
521
|
}
|
|
445
522
|
}
|
|
446
|
-
| {
|
|
447
|
-
issue: ValidationIssue.InvalidScDateFormat
|
|
448
|
-
field: string
|
|
449
|
-
value: string
|
|
450
|
-
expectedFormat: string
|
|
451
|
-
}
|
|
452
523
|
| {
|
|
453
524
|
issue: ValidationIssue.EmptyWeekdays
|
|
454
525
|
location:
|
|
@@ -466,6 +537,24 @@ type ValidationError =
|
|
|
466
537
|
weekdays: string
|
|
467
538
|
dateRange: { from: string; to: string }
|
|
468
539
|
}
|
|
540
|
+
| {
|
|
541
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay
|
|
542
|
+
overrideIndex: number
|
|
543
|
+
date: string
|
|
544
|
+
overrideRuleIndex: number
|
|
545
|
+
sourceWeeklyRuleIndex?: number
|
|
546
|
+
sourceOverrideIndex?: number
|
|
547
|
+
sourceOverrideRuleIndex?: number
|
|
548
|
+
}
|
|
549
|
+
| {
|
|
550
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext
|
|
551
|
+
overrideIndex: number
|
|
552
|
+
date: string
|
|
553
|
+
overrideRuleIndex: number
|
|
554
|
+
nextDayWeeklyRuleIndex?: number
|
|
555
|
+
nextDayOverrideIndex?: number
|
|
556
|
+
nextDayOverrideRuleIndex?: number
|
|
557
|
+
}
|
|
469
558
|
```
|
|
470
559
|
|
|
471
560
|
## Best Practices
|
|
@@ -475,14 +564,14 @@ type ValidationError =
|
|
|
475
564
|
3. **Use specific date ranges** for overrides when possible - indefinite overrides are useful for permanent schedule changes
|
|
476
565
|
4. **When using multiple indefinite overrides**, remember that the most recent one (latest `from` date) takes precedence
|
|
477
566
|
5. **Test cross-midnight ranges** thoroughly if your schedule uses them
|
|
478
|
-
6. **
|
|
567
|
+
6. **Validate time zones** separately using `isValidTimeZone()` before passing them to functions that require one
|
|
479
568
|
7. **Handle DST transitions** by testing schedules during spring forward and fall back
|
|
480
569
|
|
|
481
570
|
## Edge Cases
|
|
482
571
|
|
|
483
572
|
### DST Transitions
|
|
484
573
|
|
|
485
|
-
The library handles DST transitions using scdate's
|
|
574
|
+
The library handles DST transitions using scdate's time zone functions. Times that fall in "missing hours" (spring forward) are treated as unavailable.
|
|
486
575
|
|
|
487
576
|
### Cross-Midnight Ranges
|
|
488
577
|
|
|
@@ -505,7 +594,19 @@ import type {
|
|
|
505
594
|
AvailabilityRange,
|
|
506
595
|
ValidationError,
|
|
507
596
|
ValidationResult,
|
|
597
|
+
SDateString,
|
|
598
|
+
STimeString,
|
|
599
|
+
STimestampString,
|
|
600
|
+
SWeekdaysString,
|
|
508
601
|
} from 'scschedule'
|
|
602
|
+
|
|
603
|
+
import { ValidationIssue, RuleLocationType } from 'scschedule'
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Note: The `Weekday` enum (used in some `ValidationError` variants) is exported from `scdate`, not `scschedule`:
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { Weekday } from 'scdate'
|
|
509
610
|
```
|
|
510
611
|
|
|
511
612
|
## Dependencies
|
|
@@ -3,5 +3,9 @@ import type { Schedule } from './types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Removes expired overrides from a schedule that ended before the
|
|
5
5
|
* specified date. It does not remove indefinite overrides (no to date).
|
|
6
|
+
*
|
|
7
|
+
* @param schedule The schedule to clean up.
|
|
8
|
+
* @param beforeDate Overrides that ended before this date are removed. It
|
|
9
|
+
* can be an SDate or a string in the YYYY-MM-DD format.
|
|
6
10
|
*/
|
|
7
11
|
export declare const cleanupExpiredOverridesFromSchedule: (schedule: Schedule, beforeDate: SDate | string) => Schedule;
|
|
@@ -2,6 +2,10 @@ import { isSameDateOrAfter } from 'scdate';
|
|
|
2
2
|
/**
|
|
3
3
|
* Removes expired overrides from a schedule that ended before the
|
|
4
4
|
* specified date. It does not remove indefinite overrides (no to date).
|
|
5
|
+
*
|
|
6
|
+
* @param schedule The schedule to clean up.
|
|
7
|
+
* @param beforeDate Overrides that ended before this date are removed. It
|
|
8
|
+
* can be an SDate or a string in the YYYY-MM-DD format.
|
|
5
9
|
*/
|
|
6
10
|
export const cleanupExpiredOverridesFromSchedule = (schedule, beforeDate) => {
|
|
7
11
|
// If there are no overrides, return the schedule as-is
|
package/dist/constants.d.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* validation failure.
|
|
5
5
|
*/
|
|
6
6
|
export declare enum ValidationIssue {
|
|
7
|
-
/** The timezone string is not a valid IANA timezone identifier */
|
|
8
|
-
InvalidTimezone = "invalid-timezone",
|
|
9
7
|
/** Two or more specific overrides have identical date ranges */
|
|
10
8
|
DuplicateOverrides = "duplicate-overrides",
|
|
11
9
|
/** Two or more specific overrides have overlapping date ranges */
|
|
@@ -49,7 +47,7 @@ export declare enum ValidationIssue {
|
|
|
49
47
|
SpilloverConflictIntoOverrideFirstDay = "spillover-conflict-into-override-first-day",
|
|
50
48
|
/**
|
|
51
49
|
* Cross-midnight spillover from override's last day conflicts with next
|
|
52
|
-
* day's time ranges (weekly or another override)
|
|
50
|
+
* day's time ranges (weekly rules, weekly: true, or another override)
|
|
53
51
|
*/
|
|
54
52
|
SpilloverConflictOverrideIntoNext = "spillover-conflict-override-into-next",
|
|
55
53
|
/**
|
package/dist/constants.js
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export var ValidationIssue;
|
|
7
7
|
(function (ValidationIssue) {
|
|
8
|
-
/** The timezone string is not a valid IANA timezone identifier */
|
|
9
|
-
ValidationIssue["InvalidTimezone"] = "invalid-timezone";
|
|
10
8
|
/** Two or more specific overrides have identical date ranges */
|
|
11
9
|
ValidationIssue["DuplicateOverrides"] = "duplicate-overrides";
|
|
12
10
|
/** Two or more specific overrides have overlapping date ranges */
|
|
@@ -50,7 +48,7 @@ export var ValidationIssue;
|
|
|
50
48
|
ValidationIssue["SpilloverConflictIntoOverrideFirstDay"] = "spillover-conflict-into-override-first-day";
|
|
51
49
|
/**
|
|
52
50
|
* Cross-midnight spillover from override's last day conflicts with next
|
|
53
|
-
* day's time ranges (weekly or another override)
|
|
51
|
+
* day's time ranges (weekly rules, weekly: true, or another override)
|
|
54
52
|
*/
|
|
55
53
|
ValidationIssue["SpilloverConflictOverrideIntoNext"] = "spillover-conflict-override-into-next";
|
|
56
54
|
/**
|
|
@@ -3,5 +3,15 @@ import type { AvailabilityRange, Schedule } from './types.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns all available time ranges within a schedule for the specified
|
|
5
5
|
* date range.
|
|
6
|
+
*
|
|
7
|
+
* Iterates day-by-day from startDate to endDate (inclusive). For each day,
|
|
8
|
+
* when rules are `true` (always available), emits a full-day range
|
|
9
|
+
* (00:00-23:59). Otherwise, emits each matching time range, including
|
|
10
|
+
* cross-midnight ranges that extend into the next day.
|
|
11
|
+
*
|
|
12
|
+
* @param schedule The schedule to get availability from.
|
|
13
|
+
* @param startDate The start of the date range (inclusive).
|
|
14
|
+
* @param endDate The end of the date range (inclusive).
|
|
15
|
+
* @returns An array of availability ranges within the date range.
|
|
6
16
|
*/
|
|
7
17
|
export declare const getAvailableRangesFromSchedule: (schedule: Schedule, startDate: SDate | string, endDate: SDate | string) => AvailabilityRange[];
|
|
@@ -3,6 +3,16 @@ import { getApplicableRuleForDate } from './internal/getApplicableRuleForDate.js
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns all available time ranges within a schedule for the specified
|
|
5
5
|
* date range.
|
|
6
|
+
*
|
|
7
|
+
* Iterates day-by-day from startDate to endDate (inclusive). For each day,
|
|
8
|
+
* when rules are `true` (always available), emits a full-day range
|
|
9
|
+
* (00:00-23:59). Otherwise, emits each matching time range, including
|
|
10
|
+
* cross-midnight ranges that extend into the next day.
|
|
11
|
+
*
|
|
12
|
+
* @param schedule The schedule to get availability from.
|
|
13
|
+
* @param startDate The start of the date range (inclusive).
|
|
14
|
+
* @param endDate The end of the date range (inclusive).
|
|
15
|
+
* @returns An array of availability ranges within the date range.
|
|
6
16
|
*/
|
|
7
17
|
export const getAvailableRangesFromSchedule = (schedule, startDate, endDate) => {
|
|
8
18
|
const ranges = [];
|
|
@@ -11,6 +21,15 @@ export const getAvailableRangesFromSchedule = (schedule, startDate, endDate) =>
|
|
|
11
21
|
while (isSameDateOrBefore(currentDate, endDate)) {
|
|
12
22
|
const weekday = getWeekdayFromDate(currentDate);
|
|
13
23
|
const { rules } = getApplicableRuleForDate(schedule, currentDate);
|
|
24
|
+
// If weekly is true, the entire day is available
|
|
25
|
+
if (rules === true) {
|
|
26
|
+
ranges.push({
|
|
27
|
+
from: getTimestampFromDateAndTime(currentDate, '00:00'),
|
|
28
|
+
to: getTimestampFromDateAndTime(currentDate, '23:59'),
|
|
29
|
+
});
|
|
30
|
+
currentDate = addDaysToDate(currentDate, 1);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
14
33
|
// Find all time ranges for this day
|
|
15
34
|
for (const rule of rules) {
|
|
16
35
|
// Skip if this weekday is not in the rule
|
|
@@ -6,6 +6,7 @@ import type { Schedule } from './types.js';
|
|
|
6
6
|
*
|
|
7
7
|
* This function searches forward from the given timestamp to find when the
|
|
8
8
|
* schedule next becomes available. It handles:
|
|
9
|
+
* - Always-available days (`weekly: true` or `rules: true` from an override)
|
|
9
10
|
* - Same-day availability (finding the next time range on the current day)
|
|
10
11
|
* - Cross-midnight spillover (ranges that extend past midnight are detected
|
|
11
12
|
* via isScheduleAvailable)
|
|
@@ -15,8 +16,9 @@ import type { Schedule } from './types.js';
|
|
|
15
16
|
* The algorithm works by:
|
|
16
17
|
* 1. Checking if fromTimestamp is already available (including spillover from
|
|
17
18
|
* the previous day's cross-midnight ranges)
|
|
18
|
-
* 2. If not,
|
|
19
|
-
*
|
|
19
|
+
* 2. If not, iterating day-by-day:
|
|
20
|
+
* - If rules are `true`, the entire day is available (returns 00:00)
|
|
21
|
+
* - Otherwise, finding the earliest time range start after fromTimestamp
|
|
20
22
|
* 3. If no ranges found on current day, moving to the next day and repeating
|
|
21
23
|
*
|
|
22
24
|
* Note: Spillover ranges don't need explicit tracking because:
|
|
@@ -26,12 +28,11 @@ import type { Schedule } from './types.js';
|
|
|
26
28
|
* - The "next available" time is always a time range start, never a spillover
|
|
27
29
|
* timestamp
|
|
28
30
|
*
|
|
29
|
-
* @param schedule
|
|
30
|
-
* @param fromTimestamp
|
|
31
|
-
* @param maxDaysToSearch
|
|
32
|
-
* 365)
|
|
31
|
+
* @param schedule The schedule to check availability against.
|
|
32
|
+
* @param fromTimestamp The starting timestamp to search from.
|
|
33
|
+
* @param maxDaysToSearch Maximum number of days to search forward.
|
|
33
34
|
* @returns The next available timestamp, or undefined if none found within
|
|
34
|
-
* the search window
|
|
35
|
+
* the search window.
|
|
35
36
|
*
|
|
36
37
|
* @example
|
|
37
38
|
* // Schedule: Mon-Fri 09:00-17:00
|
|
@@ -52,4 +53,4 @@ import type { Schedule } from './types.js';
|
|
|
52
53
|
* // Custom search window: only search 30 days ahead
|
|
53
54
|
* getNextAvailableFromSchedule(schedule, timestamp, 30)
|
|
54
55
|
*/
|
|
55
|
-
export declare const getNextAvailableFromSchedule: (schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch
|
|
56
|
+
export declare const getNextAvailableFromSchedule: (schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch: number) => STimestamp | undefined;
|
|
@@ -7,6 +7,7 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
7
7
|
*
|
|
8
8
|
* This function searches forward from the given timestamp to find when the
|
|
9
9
|
* schedule next becomes available. It handles:
|
|
10
|
+
* - Always-available days (`weekly: true` or `rules: true` from an override)
|
|
10
11
|
* - Same-day availability (finding the next time range on the current day)
|
|
11
12
|
* - Cross-midnight spillover (ranges that extend past midnight are detected
|
|
12
13
|
* via isScheduleAvailable)
|
|
@@ -16,8 +17,9 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
16
17
|
* The algorithm works by:
|
|
17
18
|
* 1. Checking if fromTimestamp is already available (including spillover from
|
|
18
19
|
* the previous day's cross-midnight ranges)
|
|
19
|
-
* 2. If not,
|
|
20
|
-
*
|
|
20
|
+
* 2. If not, iterating day-by-day:
|
|
21
|
+
* - If rules are `true`, the entire day is available (returns 00:00)
|
|
22
|
+
* - Otherwise, finding the earliest time range start after fromTimestamp
|
|
21
23
|
* 3. If no ranges found on current day, moving to the next day and repeating
|
|
22
24
|
*
|
|
23
25
|
* Note: Spillover ranges don't need explicit tracking because:
|
|
@@ -27,12 +29,11 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
27
29
|
* - The "next available" time is always a time range start, never a spillover
|
|
28
30
|
* timestamp
|
|
29
31
|
*
|
|
30
|
-
* @param schedule
|
|
31
|
-
* @param fromTimestamp
|
|
32
|
-
* @param maxDaysToSearch
|
|
33
|
-
* 365)
|
|
32
|
+
* @param schedule The schedule to check availability against.
|
|
33
|
+
* @param fromTimestamp The starting timestamp to search from.
|
|
34
|
+
* @param maxDaysToSearch Maximum number of days to search forward.
|
|
34
35
|
* @returns The next available timestamp, or undefined if none found within
|
|
35
|
-
* the search window
|
|
36
|
+
* the search window.
|
|
36
37
|
*
|
|
37
38
|
* @example
|
|
38
39
|
* // Schedule: Mon-Fri 09:00-17:00
|
|
@@ -53,7 +54,7 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
53
54
|
* // Custom search window: only search 30 days ahead
|
|
54
55
|
* getNextAvailableFromSchedule(schedule, timestamp, 30)
|
|
55
56
|
*/
|
|
56
|
-
export const getNextAvailableFromSchedule = (schedule, fromTimestamp, maxDaysToSearch
|
|
57
|
+
export const getNextAvailableFromSchedule = (schedule, fromTimestamp, maxDaysToSearch) => {
|
|
57
58
|
const initialTimestamp = sTimestamp(fromTimestamp);
|
|
58
59
|
// Check if already available at fromTimestamp (handles spillover too)
|
|
59
60
|
if (isScheduleAvailable(schedule, initialTimestamp)) {
|
|
@@ -65,6 +66,11 @@ export const getNextAvailableFromSchedule = (schedule, fromTimestamp, maxDaysToS
|
|
|
65
66
|
for (let day = 0; day < maxDaysToSearch; day++) {
|
|
66
67
|
const weekday = getWeekdayFromDate(currentDate);
|
|
67
68
|
const { rules } = getApplicableRuleForDate(schedule, currentDate.date);
|
|
69
|
+
// If rules are true, the entire day is available. This only happens on
|
|
70
|
+
// day 1+ because on day 0, isScheduleAvailable would have returned true.
|
|
71
|
+
if (rules === true) {
|
|
72
|
+
return getTimestampFromDateAndTime(currentDate, '00:00');
|
|
73
|
+
}
|
|
68
74
|
// Track earliest time
|
|
69
75
|
let earliestTime;
|
|
70
76
|
for (const rule of rules) {
|
|
@@ -6,25 +6,30 @@ import type { Schedule } from './types.js';
|
|
|
6
6
|
*
|
|
7
7
|
* This function searches forward from the given timestamp to find when the
|
|
8
8
|
* schedule next becomes unavailable. It handles:
|
|
9
|
+
* - Always-available schedules (`weekly: true`) by searching for overrides
|
|
9
10
|
* - Same-day unavailability (gaps between time ranges or after the last range)
|
|
10
11
|
* - Cross-midnight ranges (unavailability after a range that crosses midnight)
|
|
11
12
|
* - Date overrides (temporary closures)
|
|
12
13
|
*
|
|
13
14
|
* The algorithm works by:
|
|
14
15
|
* 1. Checking if fromTimestamp is already unavailable
|
|
15
|
-
* 2.
|
|
16
|
+
* 2. If `weekly` is `true`, searching forward day-by-day for an override that
|
|
17
|
+
* closes availability
|
|
18
|
+
* 3. Otherwise, collecting all potential "end of availability" candidates from:
|
|
16
19
|
* - Previous day's cross-midnight spillover (ends today at `to` time)
|
|
17
20
|
* - Current day's regular ranges (end today at `to` time)
|
|
18
21
|
* - Current day's cross-midnight ranges (end tomorrow at `to` time)
|
|
19
|
-
*
|
|
22
|
+
* 4. Sorting candidates and returning the earliest one that's unavailable
|
|
20
23
|
*
|
|
21
|
-
* Note:
|
|
22
|
-
*
|
|
23
|
-
* for cross-midnight ranges).
|
|
24
|
+
* Note: For rule-based schedules, no day-by-day iteration is needed because if
|
|
25
|
+
* available, the current range ends within at most ~48 hours (cross-midnight).
|
|
24
26
|
*
|
|
25
|
-
* @param schedule
|
|
26
|
-
* @param
|
|
27
|
-
* @
|
|
27
|
+
* @param schedule The schedule to check availability against.
|
|
28
|
+
* @param timeZone IANA time zone identifier for timestamp arithmetic.
|
|
29
|
+
* @param fromTimestamp The starting timestamp to search from.
|
|
30
|
+
* @param maxDaysToSearch Maximum number of days to search forward.
|
|
31
|
+
* @returns The next unavailable timestamp, or undefined if no unavailability
|
|
32
|
+
* is found within the search window.
|
|
28
33
|
*
|
|
29
34
|
* @example
|
|
30
35
|
* // Schedule: Mon-Fri 09:00-17:00
|
|
@@ -40,5 +45,10 @@ import type { Schedule } from './types.js';
|
|
|
40
45
|
* // Schedule: Thu-Sat 20:00-02:00 (cross-midnight)
|
|
41
46
|
* // Query: Thursday at 23:00
|
|
42
47
|
* // Returns: Friday at 02:01 (after shift ends)
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Schedule: weekly: true, override closing Dec 25
|
|
51
|
+
* // Query: Dec 20 at 10:00
|
|
52
|
+
* // Returns: Dec 25 at 00:00
|
|
43
53
|
*/
|
|
44
|
-
export declare const getNextUnavailableFromSchedule: (schedule: Schedule, fromTimestamp: STimestamp | string) => STimestamp | undefined;
|
|
54
|
+
export declare const getNextUnavailableFromSchedule: (schedule: Schedule, timeZone: string, fromTimestamp: STimestamp | string, maxDaysToSearch: number) => STimestamp | undefined;
|