scschedule 2.1.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -74
- package/dist/cleanupExpiredOverridesFromSchedule.d.ts +4 -0
- package/dist/cleanupExpiredOverridesFromSchedule.js +4 -0
- package/dist/constants.d.ts +1 -10
- package/dist/constants.js +1 -10
- package/dist/getAvailableRangesFromSchedule.d.ts +10 -0
- package/dist/getAvailableRangesFromSchedule.js +29 -13
- package/dist/getNextAvailableFromSchedule.d.ts +9 -8
- package/dist/getNextAvailableFromSchedule.js +21 -17
- package/dist/getNextUnavailableFromSchedule.d.ts +19 -9
- package/dist/getNextUnavailableFromSchedule.js +120 -63
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/internal/doTimeRangesOverlap.d.ts +1 -1
- package/dist/internal/getApplicableRuleForDate.d.ts +11 -11
- package/dist/internal/getApplicableRuleForDate.js +11 -7
- package/dist/internal/getEffectiveTimesForWeekday.d.ts +2 -1
- package/dist/internal/getEffectiveTimesForWeekday.js +9 -14
- package/dist/internal/index.d.ts +1 -4
- package/dist/internal/index.js +0 -4
- package/dist/internal/isTimeInTimeRange.d.ts +2 -1
- package/dist/internal/splitCrossMidnightTimeRange.d.ts +1 -1
- package/dist/internal/splitCrossMidnightTimeRange.js +3 -2
- package/dist/internal/types.d.ts +13 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/validateNoEmptyWeekdays.js +2 -1
- package/dist/internal/validateNoOverlappingRules.js +5 -4
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +4 -2
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +95 -88
- package/dist/internal/validateScDateFormats.js +4 -5
- package/dist/isScheduleAvailable.d.ts +9 -0
- package/dist/isScheduleAvailable.js +23 -6
- package/dist/types.d.ts +18 -64
- package/dist/validateSchedule.d.ts +4 -2
- package/dist/validateSchedule.js +4 -8
- package/package.json +2 -2
- package/dist/internal/isValidTimezone.d.ts +0 -4
- package/dist/internal/isValidTimezone.js +0 -12
- package/dist/internal/validateNoOverlappingTimesInRule.d.ts +0 -5
- package/dist/internal/validateNoOverlappingTimesInRule.js +0 -54
- package/dist/internal/validateNonEmptyTimes.d.ts +0 -5
- package/dist/internal/validateNonEmptyTimes.js +0 -35
- 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,13 +46,13 @@ 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'), //
|
|
55
|
-
|
|
53
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
54
|
+
from: sTime('11:00'),
|
|
55
|
+
to: sTime('22:00'),
|
|
56
56
|
},
|
|
57
57
|
],
|
|
58
58
|
}
|
|
@@ -68,8 +68,8 @@ const now = getTimestampNow('America/Puerto_Rico')
|
|
|
68
68
|
const isOpen = isScheduleAvailable(restaurant, now)
|
|
69
69
|
console.log(`Restaurant is ${isOpen ? 'open' : 'closed'}`)
|
|
70
70
|
|
|
71
|
-
// Find next opening time
|
|
72
|
-
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
71
|
+
// Find next opening time (search up to 30 days ahead)
|
|
72
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now, 30)
|
|
73
73
|
if (nextOpen) {
|
|
74
74
|
console.log(`Next opening: ${nextOpen.timestamp}`)
|
|
75
75
|
}
|
|
@@ -81,14 +81,12 @@ if (nextOpen) {
|
|
|
81
81
|
|
|
82
82
|
A `Schedule` consists of:
|
|
83
83
|
|
|
84
|
-
- **
|
|
85
|
-
- **weekly**: Base recurring schedule (array of `WeeklyScheduleRule`)
|
|
84
|
+
- **weekly**: Base recurring schedule — `true` (available 24/7), an array of `WeeklyScheduleRule` (time-based), or `[]` (never available; overrides can open windows)
|
|
86
85
|
- **overrides** (optional): Date-specific exceptions (array of `OverrideScheduleRule`)
|
|
87
86
|
|
|
88
87
|
```typescript
|
|
89
88
|
interface Schedule {
|
|
90
|
-
|
|
91
|
-
weekly: WeeklyScheduleRule[]
|
|
89
|
+
weekly: WeeklyScheduleRule[] | true
|
|
92
90
|
overrides?: OverrideScheduleRule[]
|
|
93
91
|
}
|
|
94
92
|
```
|
|
@@ -100,10 +98,6 @@ Define recurring availability patterns for specific days of the week:
|
|
|
100
98
|
```typescript
|
|
101
99
|
interface WeeklyScheduleRule {
|
|
102
100
|
weekdays: SWeekdays // e.g., 'SMTWTFS' or '-MTWTF-'
|
|
103
|
-
times: TimeRange[] // Array of time ranges
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
interface TimeRange {
|
|
107
101
|
from: STime // e.g., '09:00'
|
|
108
102
|
to: STime // e.g., '17:00'
|
|
109
103
|
}
|
|
@@ -139,6 +133,17 @@ Rules are evaluated in order of priority:
|
|
|
139
133
|
|
|
140
134
|
### Validation
|
|
141
135
|
|
|
136
|
+
#### `isValidTimeZone(timeZone: string): boolean`
|
|
137
|
+
|
|
138
|
+
Re-exported from `scdate`. Checks if a string is a valid IANA time zone identifier (using `Intl.supportedValuesOf('timeZone')`).
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { isValidTimeZone } from 'scschedule' // or from 'scdate'
|
|
142
|
+
|
|
143
|
+
isValidTimeZone('America/New_York') // true
|
|
144
|
+
isValidTimeZone('Invalid/Timezone') // false
|
|
145
|
+
```
|
|
146
|
+
|
|
142
147
|
#### `validateSchedule(schedule: Schedule): ValidationResult`
|
|
143
148
|
|
|
144
149
|
Validates a schedule and returns detailed errors.
|
|
@@ -151,9 +156,6 @@ const result = validateSchedule(mySchedule)
|
|
|
151
156
|
if (!result.valid) {
|
|
152
157
|
result.errors.forEach((error) => {
|
|
153
158
|
switch (error.issue) {
|
|
154
|
-
case ValidationIssue.InvalidTimezone:
|
|
155
|
-
console.error(`Invalid timezone: ${error.timezone}`)
|
|
156
|
-
break
|
|
157
159
|
case ValidationIssue.OverlappingSpecificOverrides:
|
|
158
160
|
console.error(
|
|
159
161
|
`Overlapping overrides at indexes: ${error.overrideIndexes}`,
|
|
@@ -167,13 +169,16 @@ if (!result.valid) {
|
|
|
167
169
|
|
|
168
170
|
**Validation checks**:
|
|
169
171
|
|
|
170
|
-
- Valid
|
|
172
|
+
- Valid scdate formats (SDate, STime, SWeekdays)
|
|
173
|
+
- Override `to` date must not be before `from` date
|
|
171
174
|
- No duplicate overrides (identical from/to dates)
|
|
172
175
|
- No overlapping specific overrides (hierarchical nesting allowed)
|
|
173
|
-
- No overlapping time ranges within
|
|
176
|
+
- No overlapping time ranges within a single rule (same weekday)
|
|
177
|
+
- No overlapping rules within the weekly schedule (shared weekdays with overlapping times)
|
|
178
|
+
- No overlapping rules within the same override (shared weekdays with overlapping times)
|
|
179
|
+
- No cross-midnight spillover conflicts at override boundaries
|
|
174
180
|
- All rules have at least one time range
|
|
175
|
-
-
|
|
176
|
-
- No empty weekdays patterns (e.g., '-------' with no days selected)
|
|
181
|
+
- No empty weekdays patterns (e.g., `'-------'` with no days selected)
|
|
177
182
|
- Override weekdays must match at least one date in the override's date range
|
|
178
183
|
|
|
179
184
|
### Schedule Management
|
|
@@ -208,27 +213,32 @@ const isOpen = isScheduleAvailable(
|
|
|
208
213
|
)
|
|
209
214
|
```
|
|
210
215
|
|
|
211
|
-
#### `getNextAvailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch
|
|
216
|
+
#### `getNextAvailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch: number): STimestamp | undefined`
|
|
212
217
|
|
|
213
|
-
Find the next available timestamp from a given time. Searches up to `maxDaysToSearch` days
|
|
218
|
+
Find the next available timestamp from a given time. Searches up to `maxDaysToSearch` days forward.
|
|
214
219
|
|
|
215
220
|
```typescript
|
|
216
221
|
import { getNextAvailableFromSchedule } from 'scschedule'
|
|
217
222
|
|
|
218
|
-
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
223
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now, 30)
|
|
219
224
|
if (nextOpen) {
|
|
220
225
|
console.log(`Opens at: ${nextOpen.timestamp}`)
|
|
221
226
|
}
|
|
222
227
|
```
|
|
223
228
|
|
|
224
|
-
#### `getNextUnavailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string): STimestamp | undefined`
|
|
229
|
+
#### `getNextUnavailableFromSchedule(schedule: Schedule, timeZone: string, fromTimestamp: STimestamp | string, maxDaysToSearch: number): STimestamp | undefined`
|
|
225
230
|
|
|
226
|
-
Find the next unavailable timestamp from a given time.
|
|
231
|
+
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
232
|
|
|
228
233
|
```typescript
|
|
229
234
|
import { getNextUnavailableFromSchedule } from 'scschedule'
|
|
230
235
|
|
|
231
|
-
const nextClosed = getNextUnavailableFromSchedule(
|
|
236
|
+
const nextClosed = getNextUnavailableFromSchedule(
|
|
237
|
+
restaurant,
|
|
238
|
+
'America/Puerto_Rico',
|
|
239
|
+
now,
|
|
240
|
+
30,
|
|
241
|
+
)
|
|
232
242
|
if (nextClosed) {
|
|
233
243
|
console.log(`Closes at: ${nextClosed.timestamp}`)
|
|
234
244
|
}
|
|
@@ -262,11 +272,11 @@ import { Schedule } from 'scschedule'
|
|
|
262
272
|
import { sTime, sWeekdays } from 'scdate'
|
|
263
273
|
|
|
264
274
|
const restaurant: Schedule = {
|
|
265
|
-
timezone: 'America/Puerto_Rico',
|
|
266
275
|
weekly: [
|
|
267
276
|
{
|
|
268
|
-
weekdays: sWeekdays('-MTWTFS'), //
|
|
269
|
-
|
|
277
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
278
|
+
from: sTime('11:00'),
|
|
279
|
+
to: sTime('22:00'),
|
|
270
280
|
},
|
|
271
281
|
],
|
|
272
282
|
}
|
|
@@ -276,14 +286,16 @@ const restaurant: Schedule = {
|
|
|
276
286
|
|
|
277
287
|
```typescript
|
|
278
288
|
const restaurant: Schedule = {
|
|
279
|
-
timezone: 'America/Puerto_Rico',
|
|
280
289
|
weekly: [
|
|
281
290
|
{
|
|
282
|
-
weekdays: sWeekdays('-MTWTFS'), //
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
291
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
292
|
+
from: sTime('11:00'),
|
|
293
|
+
to: sTime('14:00'), // Lunch
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
weekdays: sWeekdays('-MTWTFS'), // Mon-Sat
|
|
297
|
+
from: sTime('17:00'),
|
|
298
|
+
to: sTime('22:00'), // Dinner
|
|
287
299
|
},
|
|
288
300
|
],
|
|
289
301
|
}
|
|
@@ -293,17 +305,18 @@ const restaurant: Schedule = {
|
|
|
293
305
|
|
|
294
306
|
```typescript
|
|
295
307
|
const restaurant: Schedule = {
|
|
296
|
-
timezone: 'America/Puerto_Rico',
|
|
297
308
|
weekly: [
|
|
298
309
|
{
|
|
299
310
|
// Weekdays: longer hours
|
|
300
311
|
weekdays: sWeekdays('-MTWTF-'), // Mon-Fri
|
|
301
|
-
|
|
312
|
+
from: sTime('10:00'),
|
|
313
|
+
to: sTime('23:00'),
|
|
302
314
|
},
|
|
303
315
|
{
|
|
304
316
|
// Weekends: shorter hours
|
|
305
317
|
weekdays: sWeekdays('S-----S'), // Sat-Sun
|
|
306
|
-
|
|
318
|
+
from: sTime('12:00'),
|
|
319
|
+
to: sTime('20:00'),
|
|
307
320
|
},
|
|
308
321
|
],
|
|
309
322
|
}
|
|
@@ -338,7 +351,8 @@ const withExtendedHours: Schedule = {
|
|
|
338
351
|
{
|
|
339
352
|
// Extended hours for December, weekends only
|
|
340
353
|
weekdays: sWeekdays('S-----S'),
|
|
341
|
-
|
|
354
|
+
from: sTime('08:00'),
|
|
355
|
+
to: sTime('23:00'),
|
|
342
356
|
},
|
|
343
357
|
],
|
|
344
358
|
},
|
|
@@ -359,7 +373,8 @@ const newSchedule: Schedule = {
|
|
|
359
373
|
rules: [
|
|
360
374
|
{
|
|
361
375
|
weekdays: sWeekdays('SMTWTFS'), // All days
|
|
362
|
-
|
|
376
|
+
from: sTime('09:00'),
|
|
377
|
+
to: sTime('21:00'),
|
|
363
378
|
},
|
|
364
379
|
],
|
|
365
380
|
},
|
|
@@ -371,11 +386,11 @@ const newSchedule: Schedule = {
|
|
|
371
386
|
|
|
372
387
|
```typescript
|
|
373
388
|
const lateNightBar: Schedule = {
|
|
374
|
-
timezone: 'America/Puerto_Rico',
|
|
375
389
|
weekly: [
|
|
376
390
|
{
|
|
377
391
|
weekdays: sWeekdays('----TFS'), // Thu-Sat
|
|
378
|
-
|
|
392
|
+
from: sTime('20:00'),
|
|
393
|
+
to: sTime('03:00'), // 8PM-3AM
|
|
379
394
|
},
|
|
380
395
|
],
|
|
381
396
|
}
|
|
@@ -386,16 +401,67 @@ const lateNightBar: Schedule = {
|
|
|
386
401
|
// - Saturday: 20:00-23:59, Sunday: 00:00-03:00
|
|
387
402
|
```
|
|
388
403
|
|
|
404
|
+
### Always Available (`weekly: true`)
|
|
405
|
+
|
|
406
|
+
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).
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
// Menu item available 24/7 (restaurant hours handle the filtering)
|
|
410
|
+
const menuItem: Schedule = {
|
|
411
|
+
weekly: true,
|
|
412
|
+
overrides: [
|
|
413
|
+
{
|
|
414
|
+
// Except Christmas Day
|
|
415
|
+
from: sDate('2025-12-25'),
|
|
416
|
+
to: sDate('2025-12-25'),
|
|
417
|
+
rules: [],
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Closed by Default (`weekly: []`)
|
|
424
|
+
|
|
425
|
+
Use `weekly: []` when an entity is unavailable by default and only opens during specific override periods.
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Pop-up shop: closed by default, open only during specific events
|
|
429
|
+
const popUpShop: Schedule = {
|
|
430
|
+
weekly: [],
|
|
431
|
+
overrides: [
|
|
432
|
+
{
|
|
433
|
+
from: sDate('2025-12-20'),
|
|
434
|
+
to: sDate('2025-12-24'),
|
|
435
|
+
rules: [
|
|
436
|
+
{
|
|
437
|
+
weekdays: sWeekdays('SMTWTFS'),
|
|
438
|
+
from: sTime('10:00'),
|
|
439
|
+
to: sTime('18:00'),
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
389
447
|
### Multiple Schedules (Layered Availability)
|
|
390
448
|
|
|
391
449
|
```typescript
|
|
392
450
|
import { isScheduleAvailable } from 'scschedule'
|
|
393
451
|
|
|
394
452
|
const businessHours: Schedule = {
|
|
395
|
-
|
|
453
|
+
weekly: [
|
|
454
|
+
{
|
|
455
|
+
weekdays: sWeekdays('-MTWTFS'),
|
|
456
|
+
from: sTime('11:00'),
|
|
457
|
+
to: sTime('22:00'),
|
|
458
|
+
},
|
|
459
|
+
],
|
|
396
460
|
}
|
|
461
|
+
|
|
462
|
+
// Menu available 24/7 — restaurant hours do the filtering
|
|
397
463
|
const breakfastMenu: Schedule = {
|
|
398
|
-
|
|
464
|
+
weekly: true,
|
|
399
465
|
}
|
|
400
466
|
|
|
401
467
|
// Both must be available
|
|
@@ -411,8 +477,16 @@ The library uses discriminated unions for type-safe error handling:
|
|
|
411
477
|
```typescript
|
|
412
478
|
type ValidationError =
|
|
413
479
|
| {
|
|
414
|
-
issue: ValidationIssue.
|
|
415
|
-
|
|
480
|
+
issue: ValidationIssue.InvalidScDateFormat
|
|
481
|
+
field: string
|
|
482
|
+
value: string
|
|
483
|
+
expectedFormat: string
|
|
484
|
+
}
|
|
485
|
+
| {
|
|
486
|
+
issue: ValidationIssue.InvalidOverrideDateOrder
|
|
487
|
+
overrideIndex: number
|
|
488
|
+
from: string
|
|
489
|
+
to: string
|
|
416
490
|
}
|
|
417
491
|
| {
|
|
418
492
|
issue: ValidationIssue.DuplicateOverrides
|
|
@@ -423,31 +497,15 @@ type ValidationError =
|
|
|
423
497
|
overrideIndexes: [number, number]
|
|
424
498
|
}
|
|
425
499
|
| {
|
|
426
|
-
issue: ValidationIssue.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
| {
|
|
430
|
-
type: RuleLocationType.Override
|
|
431
|
-
overrideIndex: number
|
|
432
|
-
ruleIndex: number
|
|
433
|
-
}
|
|
434
|
-
timeRangeIndexes: [number, number]
|
|
435
|
-
}
|
|
436
|
-
| {
|
|
437
|
-
issue: ValidationIssue.EmptyTimes
|
|
438
|
-
location:
|
|
439
|
-
| { type: RuleLocationType.Weekly; ruleIndex: number }
|
|
440
|
-
| {
|
|
441
|
-
type: RuleLocationType.Override
|
|
442
|
-
overrideIndex: number
|
|
443
|
-
ruleIndex: number
|
|
444
|
-
}
|
|
500
|
+
issue: ValidationIssue.OverlappingRulesInWeekly
|
|
501
|
+
ruleIndexes: [number, number]
|
|
502
|
+
weekday: Weekday
|
|
445
503
|
}
|
|
446
504
|
| {
|
|
447
|
-
issue: ValidationIssue.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
505
|
+
issue: ValidationIssue.OverlappingRulesInOverride
|
|
506
|
+
overrideIndex: number
|
|
507
|
+
ruleIndexes: [number, number]
|
|
508
|
+
weekday: Weekday
|
|
451
509
|
}
|
|
452
510
|
| {
|
|
453
511
|
issue: ValidationIssue.EmptyWeekdays
|
|
@@ -466,6 +524,24 @@ type ValidationError =
|
|
|
466
524
|
weekdays: string
|
|
467
525
|
dateRange: { from: string; to: string }
|
|
468
526
|
}
|
|
527
|
+
| {
|
|
528
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay
|
|
529
|
+
overrideIndex: number
|
|
530
|
+
date: string
|
|
531
|
+
overrideRuleIndex: number
|
|
532
|
+
sourceWeeklyRuleIndex?: number
|
|
533
|
+
sourceOverrideIndex?: number
|
|
534
|
+
sourceOverrideRuleIndex?: number
|
|
535
|
+
}
|
|
536
|
+
| {
|
|
537
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext
|
|
538
|
+
overrideIndex: number
|
|
539
|
+
date: string
|
|
540
|
+
overrideRuleIndex: number
|
|
541
|
+
nextDayWeeklyRuleIndex?: number
|
|
542
|
+
nextDayOverrideIndex?: number
|
|
543
|
+
nextDayOverrideRuleIndex?: number
|
|
544
|
+
}
|
|
469
545
|
```
|
|
470
546
|
|
|
471
547
|
## Best Practices
|
|
@@ -475,14 +551,14 @@ type ValidationError =
|
|
|
475
551
|
3. **Use specific date ranges** for overrides when possible - indefinite overrides are useful for permanent schedule changes
|
|
476
552
|
4. **When using multiple indefinite overrides**, remember that the most recent one (latest `from` date) takes precedence
|
|
477
553
|
5. **Test cross-midnight ranges** thoroughly if your schedule uses them
|
|
478
|
-
6. **
|
|
554
|
+
6. **Validate time zones** separately using `isValidTimeZone()` before passing them to functions that require one
|
|
479
555
|
7. **Handle DST transitions** by testing schedules during spring forward and fall back
|
|
480
556
|
|
|
481
557
|
## Edge Cases
|
|
482
558
|
|
|
483
559
|
### DST Transitions
|
|
484
560
|
|
|
485
|
-
The library handles DST transitions using scdate's
|
|
561
|
+
The library handles DST transitions using scdate's time zone functions. Times that fall in "missing hours" (spring forward) are treated as unavailable.
|
|
486
562
|
|
|
487
563
|
### Cross-Midnight Ranges
|
|
488
564
|
|
|
@@ -501,11 +577,22 @@ import type {
|
|
|
501
577
|
Schedule,
|
|
502
578
|
WeeklyScheduleRule,
|
|
503
579
|
OverrideScheduleRule,
|
|
504
|
-
TimeRange,
|
|
505
580
|
AvailabilityRange,
|
|
506
581
|
ValidationError,
|
|
507
582
|
ValidationResult,
|
|
583
|
+
SDateString,
|
|
584
|
+
STimeString,
|
|
585
|
+
STimestampString,
|
|
586
|
+
SWeekdaysString,
|
|
508
587
|
} from 'scschedule'
|
|
588
|
+
|
|
589
|
+
import { ValidationIssue, RuleLocationType } from 'scschedule'
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Note: The `Weekday` enum (used in some `ValidationError` variants) is exported from `scdate`, not `scschedule`:
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
import { Weekday } from 'scdate'
|
|
509
596
|
```
|
|
510
597
|
|
|
511
598
|
## 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,19 +4,10 @@
|
|
|
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 */
|
|
12
10
|
OverlappingSpecificOverrides = "overlapping-specific-overrides",
|
|
13
|
-
/** Time ranges within a single rule overlap with each other */
|
|
14
|
-
OverlappingTimesInRule = "overlapping-times-in-rule",
|
|
15
|
-
/**
|
|
16
|
-
* A rule has an empty times array (should have at least one time range or be
|
|
17
|
-
* removed)
|
|
18
|
-
*/
|
|
19
|
-
EmptyTimes = "empty-times",
|
|
20
11
|
/**
|
|
21
12
|
* A field contains an invalid scdate format (SDate, STime, SWeekdays, or
|
|
22
13
|
* STimestamp)
|
|
@@ -49,7 +40,7 @@ export declare enum ValidationIssue {
|
|
|
49
40
|
SpilloverConflictIntoOverrideFirstDay = "spillover-conflict-into-override-first-day",
|
|
50
41
|
/**
|
|
51
42
|
* Cross-midnight spillover from override's last day conflicts with next
|
|
52
|
-
* day's time ranges (weekly or another override)
|
|
43
|
+
* day's time ranges (weekly rules, weekly: true, or another override)
|
|
53
44
|
*/
|
|
54
45
|
SpilloverConflictOverrideIntoNext = "spillover-conflict-override-into-next",
|
|
55
46
|
/**
|
package/dist/constants.js
CHANGED
|
@@ -5,19 +5,10 @@
|
|
|
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 */
|
|
13
11
|
ValidationIssue["OverlappingSpecificOverrides"] = "overlapping-specific-overrides";
|
|
14
|
-
/** Time ranges within a single rule overlap with each other */
|
|
15
|
-
ValidationIssue["OverlappingTimesInRule"] = "overlapping-times-in-rule";
|
|
16
|
-
/**
|
|
17
|
-
* A rule has an empty times array (should have at least one time range or be
|
|
18
|
-
* removed)
|
|
19
|
-
*/
|
|
20
|
-
ValidationIssue["EmptyTimes"] = "empty-times";
|
|
21
12
|
/**
|
|
22
13
|
* A field contains an invalid scdate format (SDate, STime, SWeekdays, or
|
|
23
14
|
* STimestamp)
|
|
@@ -50,7 +41,7 @@ export var ValidationIssue;
|
|
|
50
41
|
ValidationIssue["SpilloverConflictIntoOverrideFirstDay"] = "spillover-conflict-into-override-first-day";
|
|
51
42
|
/**
|
|
52
43
|
* Cross-midnight spillover from override's last day conflicts with next
|
|
53
|
-
* day's time ranges (weekly or another override)
|
|
44
|
+
* day's time ranges (weekly rules, weekly: true, or another override)
|
|
54
45
|
*/
|
|
55
46
|
ValidationIssue["SpilloverConflictOverrideIntoNext"] = "spillover-conflict-override-into-next";
|
|
56
47
|
/**
|
|
@@ -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,25 +21,31 @@ 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
|
|
17
36
|
if (!doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
18
37
|
continue;
|
|
19
38
|
}
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
to: rangeEnd,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
39
|
+
// Handle cross-midnight ranges (from > to means it crosses midnight)
|
|
40
|
+
const isCrossMidnight = isAfterTime(rule.from, rule.to);
|
|
41
|
+
const rangeStart = getTimestampFromDateAndTime(currentDate, rule.from);
|
|
42
|
+
const rangeEnd = isCrossMidnight
|
|
43
|
+
? getTimestampFromDateAndTime(addDaysToDate(currentDate, 1), rule.to)
|
|
44
|
+
: getTimestampFromDateAndTime(currentDate, rule.to);
|
|
45
|
+
ranges.push({
|
|
46
|
+
from: rangeStart,
|
|
47
|
+
to: rangeEnd,
|
|
48
|
+
});
|
|
33
49
|
}
|
|
34
50
|
// Move to the next day
|
|
35
51
|
currentDate = addDaysToDate(currentDate, 1);
|
|
@@ -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;
|