scschedule 2.0.2 → 2.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 +494 -10
- package/dist/cleanupExpiredOverridesFromSchedule.d.ts +7 -0
- package/dist/cleanupExpiredOverridesFromSchedule.js +29 -0
- package/dist/constants.d.ts +69 -0
- package/dist/constants.js +71 -0
- package/dist/getAvailableRangesFromSchedule.d.ts +7 -0
- package/dist/getAvailableRangesFromSchedule.js +38 -0
- package/dist/getNextAvailableFromSchedule.d.ts +55 -0
- package/dist/getNextAvailableFromSchedule.js +92 -0
- package/dist/getNextUnavailableFromSchedule.d.ts +44 -0
- package/dist/getNextUnavailableFromSchedule.js +112 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11 -2
- package/dist/internal/doOverridesOverlap.d.ts +5 -0
- package/dist/internal/doOverridesOverlap.js +14 -0
- package/dist/internal/doRulesOverlap.d.ts +15 -0
- package/dist/internal/doRulesOverlap.js +35 -0
- package/dist/internal/doTimeRangesOverlap.d.ts +16 -0
- package/dist/internal/doTimeRangesOverlap.js +49 -0
- package/dist/internal/getApplicableRuleForDate.d.ts +24 -0
- package/dist/internal/getApplicableRuleForDate.js +82 -0
- package/dist/internal/getEffectiveTimesForWeekday.d.ts +12 -0
- package/dist/internal/getEffectiveTimesForWeekday.js +42 -0
- package/dist/internal/index.d.ts +19 -0
- package/dist/internal/index.js +21 -0
- package/dist/internal/isTimeInTimeRange.d.ts +7 -0
- package/dist/internal/isTimeInTimeRange.js +19 -0
- package/dist/internal/isValidTimezone.d.ts +4 -0
- package/dist/internal/isValidTimezone.js +12 -0
- package/dist/internal/normalizeScheduleForValidation.d.ts +13 -0
- package/dist/internal/normalizeScheduleForValidation.js +36 -0
- package/dist/internal/splitCrossMidnightTimeRange.d.ts +5 -0
- package/dist/internal/splitCrossMidnightTimeRange.js +22 -0
- package/dist/internal/validateNoEmptyWeekdays.d.ts +7 -0
- package/dist/internal/validateNoEmptyWeekdays.js +50 -0
- package/dist/internal/validateNoOverlappingOverrides.d.ts +5 -0
- package/dist/internal/validateNoOverlappingOverrides.js +57 -0
- package/dist/internal/validateNoOverlappingRules.d.ts +13 -0
- package/dist/internal/validateNoOverlappingRules.js +56 -0
- package/dist/internal/validateNoOverlappingTimesInRule.d.ts +5 -0
- package/dist/internal/validateNoOverlappingTimesInRule.js +54 -0
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +11 -0
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +149 -0
- package/dist/internal/validateNonEmptyTimes.d.ts +5 -0
- package/dist/internal/validateNonEmptyTimes.js +35 -0
- package/dist/internal/validateOverrideDateOrder.d.ts +6 -0
- package/dist/internal/validateOverrideDateOrder.js +27 -0
- package/dist/internal/validateOverrideWeekdaysMatchDates.d.ts +10 -0
- package/dist/internal/validateOverrideWeekdaysMatchDates.js +63 -0
- package/dist/internal/validateScDateFormats.d.ts +5 -0
- package/dist/internal/validateScDateFormats.js +109 -0
- package/dist/internal/validateTimezone.d.ts +5 -0
- package/dist/internal/validateTimezone.js +16 -0
- package/dist/isScheduleAvailable.d.ts +6 -0
- package/dist/isScheduleAvailable.js +37 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.js +1 -0
- package/dist/validateSchedule.d.ts +15 -0
- package/dist/validateSchedule.js +57 -0
- package/package.json +4 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
# scschedule
|
|
2
2
|
|
|
3
|
-
**Schedule management
|
|
3
|
+
**Schedule management library for time-based availability patterns**
|
|
4
4
|
|
|
5
5
|
[](https://github.com/ericvera/scdate/blob/master/LICENSE)
|
|
6
6
|
[](https://npmjs.org/package/scschedule)
|
|
7
7
|
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
scschedule is a TypeScript library for managing time-based availability patterns. Built on top of [scdate](https://npmjs.org/package/scdate) for consistent date/time handling, it provides a powerful yet simple way to define and query when entities are available or unavailable using recurring patterns and date-specific overrides.
|
|
11
|
+
|
|
12
|
+
**Use cases**: Business hours, resource availability, service windows, time-restricted access control, scheduling systems, and any application that needs to determine availability based on time patterns.
|
|
13
|
+
|
|
8
14
|
## Features
|
|
9
15
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
16
|
+
- **Recurring patterns**: Define weekly schedules with different hours for different days
|
|
17
|
+
- **Override system**: Add exceptions for holidays, special events, or schedule changes
|
|
18
|
+
- **Cross-midnight support**: Handle time ranges that span midnight (e.g., 22:00-02:00)
|
|
19
|
+
- **Timezone aware**: All operations use schedule's timezone for calculations
|
|
20
|
+
- **DST handling**: Properly handles daylight saving time transitions
|
|
21
|
+
- **Immutable**: All operations return new instances
|
|
22
|
+
- **Type-safe validation**: Discriminated union errors with detailed information
|
|
23
|
+
- **Tree-shakeable**: Each function in its own file for optimal bundling
|
|
15
24
|
|
|
16
25
|
## Installation
|
|
17
26
|
|
|
18
27
|
```bash
|
|
19
|
-
npm install scschedule
|
|
28
|
+
npm install scschedule scdate
|
|
20
29
|
# or
|
|
21
|
-
yarn add scschedule
|
|
30
|
+
yarn add scschedule scdate
|
|
22
31
|
```
|
|
23
32
|
|
|
24
33
|
## Requirements
|
|
@@ -26,9 +35,478 @@ yarn add scschedule
|
|
|
26
35
|
- Node.js >= 22
|
|
27
36
|
- TypeScript >= 5.0 (for TypeScript users)
|
|
28
37
|
|
|
29
|
-
##
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import {
|
|
42
|
+
Schedule,
|
|
43
|
+
isScheduleAvailable,
|
|
44
|
+
getNextAvailableFromSchedule,
|
|
45
|
+
validateSchedule,
|
|
46
|
+
} from 'scschedule'
|
|
47
|
+
import { sDate, sTime, sWeekdays, getTimestampNow } from 'scdate'
|
|
48
|
+
|
|
49
|
+
// Define a restaurant schedule: Tuesday-Saturday, 11:00-22:00
|
|
50
|
+
const restaurant: Schedule = {
|
|
51
|
+
timezone: 'America/Puerto_Rico',
|
|
52
|
+
weekly: [
|
|
53
|
+
{
|
|
54
|
+
weekdays: sWeekdays('-MTWTFS'), // Tue-Sat
|
|
55
|
+
times: [{ from: sTime('11:00'), to: sTime('22:00') }],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate the schedule
|
|
61
|
+
const validation = validateSchedule(restaurant)
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
console.error('Invalid schedule:', validation.errors)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if open now
|
|
67
|
+
const now = getTimestampNow('America/Puerto_Rico')
|
|
68
|
+
const isOpen = isScheduleAvailable(restaurant, now)
|
|
69
|
+
console.log(`Restaurant is ${isOpen ? 'open' : 'closed'}`)
|
|
70
|
+
|
|
71
|
+
// Find next opening time
|
|
72
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
73
|
+
if (nextOpen) {
|
|
74
|
+
console.log(`Next opening: ${nextOpen.timestamp}`)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Core Concepts
|
|
79
|
+
|
|
80
|
+
### Schedule Structure
|
|
81
|
+
|
|
82
|
+
A `Schedule` consists of:
|
|
83
|
+
|
|
84
|
+
- **timezone**: The timezone for all time calculations
|
|
85
|
+
- **weekly**: Base recurring schedule (array of `WeeklyScheduleRule`)
|
|
86
|
+
- **overrides** (optional): Date-specific exceptions (array of `OverrideScheduleRule`)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
interface Schedule {
|
|
90
|
+
timezone: string
|
|
91
|
+
weekly: WeeklyScheduleRule[]
|
|
92
|
+
overrides?: OverrideScheduleRule[]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Weekly Rules
|
|
97
|
+
|
|
98
|
+
Define recurring availability patterns for specific days of the week:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface WeeklyScheduleRule {
|
|
102
|
+
weekdays: SWeekdays // e.g., 'SMTWTFS' or '-MTWTF-'
|
|
103
|
+
times: TimeRange[] // Array of time ranges
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface TimeRange {
|
|
107
|
+
from: STime // e.g., '09:00'
|
|
108
|
+
to: STime // e.g., '17:00'
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Override Rules
|
|
113
|
+
|
|
114
|
+
Override the weekly schedule for specific date ranges:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
interface OverrideScheduleRule {
|
|
118
|
+
from: SDate // Start date (required)
|
|
119
|
+
to?: SDate // End date (optional - if omitted, applies indefinitely)
|
|
120
|
+
rules: WeeklyScheduleRule[] // Empty array = unavailable/closed
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Override semantics**:
|
|
125
|
+
|
|
126
|
+
- **Closed for a day**: `{ from: '2025-12-25', rules: [] }` (Christmas Day)
|
|
127
|
+
- **Closed indefinitely**: `{ from: '2025-12-25', rules: [] }` (no `to` date)
|
|
128
|
+
- **Special hours**: Override with different weekly patterns for the date range
|
|
129
|
+
|
|
130
|
+
### Rule Priority
|
|
131
|
+
|
|
132
|
+
Rules are evaluated in order of priority:
|
|
133
|
+
|
|
134
|
+
1. **Specific override** (with both `from` and `to`) - highest priority, shortest duration wins when multiple could apply
|
|
135
|
+
2. **Indefinite override** (with only `from`, no `to`) - when multiple indefinite overrides could apply, the one with the latest `from` date wins (most recent policy)
|
|
136
|
+
3. **Weekly rules** - lowest priority (base schedule)
|
|
137
|
+
|
|
138
|
+
## API Reference
|
|
139
|
+
|
|
140
|
+
### Validation
|
|
141
|
+
|
|
142
|
+
#### `validateSchedule(schedule: Schedule): ValidationResult`
|
|
143
|
+
|
|
144
|
+
Validates a schedule and returns detailed errors.
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { validateSchedule, ValidationIssue } from 'scschedule'
|
|
148
|
+
|
|
149
|
+
const result = validateSchedule(mySchedule)
|
|
150
|
+
|
|
151
|
+
if (!result.valid) {
|
|
152
|
+
result.errors.forEach((error) => {
|
|
153
|
+
switch (error.issue) {
|
|
154
|
+
case ValidationIssue.InvalidTimezone:
|
|
155
|
+
console.error(`Invalid timezone: ${error.timezone}`)
|
|
156
|
+
break
|
|
157
|
+
case ValidationIssue.OverlappingSpecificOverrides:
|
|
158
|
+
console.error(
|
|
159
|
+
`Overlapping overrides at indexes: ${error.overrideIndexes}`,
|
|
160
|
+
)
|
|
161
|
+
break
|
|
162
|
+
// ... handle other error types
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Validation checks**:
|
|
169
|
+
|
|
170
|
+
- Valid timezone (in `Intl.supportedValuesOf('timeZone')`)
|
|
171
|
+
- No duplicate overrides (identical from/to dates)
|
|
172
|
+
- No overlapping specific overrides (hierarchical nesting allowed)
|
|
173
|
+
- No overlapping time ranges within rules (same weekday)
|
|
174
|
+
- All rules have at least one time range
|
|
175
|
+
- Valid scdate formats (SDate, STime, SWeekdays)
|
|
176
|
+
- No empty weekdays patterns (e.g., '-------' with no days selected)
|
|
177
|
+
- Override weekdays must match at least one date in the override's date range
|
|
178
|
+
|
|
179
|
+
### Schedule Management
|
|
180
|
+
|
|
181
|
+
#### `cleanupExpiredOverridesFromSchedule(schedule: Schedule, beforeDate: SDate | string): Schedule`
|
|
182
|
+
|
|
183
|
+
Removes expired overrides (with a `to` date) that ended before a given date. Indefinite overrides (no `to` date) are never removed. Returns a new Schedule instance.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { cleanupExpiredOverridesFromSchedule } from 'scschedule'
|
|
187
|
+
import { sDate } from 'scdate'
|
|
188
|
+
|
|
189
|
+
const cleaned = cleanupExpiredOverridesFromSchedule(
|
|
190
|
+
mySchedule,
|
|
191
|
+
sDate('2025-01-01'),
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Availability Queries
|
|
196
|
+
|
|
197
|
+
#### `isScheduleAvailable(schedule: Schedule, timestamp: STimestamp | string): boolean`
|
|
198
|
+
|
|
199
|
+
Check if schedule is available at a specific timestamp.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { isScheduleAvailable } from 'scschedule'
|
|
203
|
+
import { getTimestampNow } from 'scdate'
|
|
204
|
+
|
|
205
|
+
const isOpen = isScheduleAvailable(
|
|
206
|
+
restaurant,
|
|
207
|
+
getTimestampNow('America/Puerto_Rico'),
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### `getNextAvailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string, maxDaysToSearch?: number): STimestamp | undefined`
|
|
212
|
+
|
|
213
|
+
Find the next available timestamp from a given time. Searches up to `maxDaysToSearch` days (default: 365).
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { getNextAvailableFromSchedule } from 'scschedule'
|
|
217
|
+
|
|
218
|
+
const nextOpen = getNextAvailableFromSchedule(restaurant, now)
|
|
219
|
+
if (nextOpen) {
|
|
220
|
+
console.log(`Opens at: ${nextOpen.timestamp}`)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### `getNextUnavailableFromSchedule(schedule: Schedule, fromTimestamp: STimestamp | string): STimestamp | undefined`
|
|
225
|
+
|
|
226
|
+
Find the next unavailable timestamp from a given time.
|
|
30
227
|
|
|
31
|
-
|
|
228
|
+
```typescript
|
|
229
|
+
import { getNextUnavailableFromSchedule } from 'scschedule'
|
|
230
|
+
|
|
231
|
+
const nextClosed = getNextUnavailableFromSchedule(restaurant, now)
|
|
232
|
+
if (nextClosed) {
|
|
233
|
+
console.log(`Closes at: ${nextClosed.timestamp}`)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### `getAvailableRangesFromSchedule(schedule: Schedule, startDate: SDate | string, endDate: SDate | string): AvailabilityRange[]`
|
|
238
|
+
|
|
239
|
+
Get all available time ranges within a date range.
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { getAvailableRangesFromSchedule } from 'scschedule'
|
|
243
|
+
import { sDate } from 'scdate'
|
|
244
|
+
|
|
245
|
+
const ranges = getAvailableRangesFromSchedule(
|
|
246
|
+
restaurant,
|
|
247
|
+
sDate('2025-11-11'),
|
|
248
|
+
sDate('2025-11-17'),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
ranges.forEach((range) => {
|
|
252
|
+
console.log(`Available: ${range.from.timestamp} to ${range.to.timestamp}`)
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Usage Examples
|
|
257
|
+
|
|
258
|
+
### Basic Business Hours
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { Schedule } from 'scschedule'
|
|
262
|
+
import { sTime, sWeekdays } from 'scdate'
|
|
263
|
+
|
|
264
|
+
const restaurant: Schedule = {
|
|
265
|
+
timezone: 'America/Puerto_Rico',
|
|
266
|
+
weekly: [
|
|
267
|
+
{
|
|
268
|
+
weekdays: sWeekdays('-MTWTFS'), // Tue-Sat
|
|
269
|
+
times: [{ from: sTime('11:00'), to: sTime('22:00') }],
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Split Schedules (Lunch & Dinner)
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const restaurant: Schedule = {
|
|
279
|
+
timezone: 'America/Puerto_Rico',
|
|
280
|
+
weekly: [
|
|
281
|
+
{
|
|
282
|
+
weekdays: sWeekdays('-MTWTFS'), // Tue-Sat
|
|
283
|
+
times: [
|
|
284
|
+
{ from: sTime('11:00'), to: sTime('14:00') }, // Lunch
|
|
285
|
+
{ from: sTime('17:00'), to: sTime('22:00') }, // Dinner
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Different Hours for Different Days
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
const restaurant: Schedule = {
|
|
296
|
+
timezone: 'America/Puerto_Rico',
|
|
297
|
+
weekly: [
|
|
298
|
+
{
|
|
299
|
+
// Weekdays: longer hours
|
|
300
|
+
weekdays: sWeekdays('-MTWTF-'), // Mon-Fri
|
|
301
|
+
times: [{ from: sTime('10:00'), to: sTime('23:00') }],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
// Weekends: shorter hours
|
|
305
|
+
weekdays: sWeekdays('S-----S'), // Sat-Sun
|
|
306
|
+
times: [{ from: sTime('12:00'), to: sTime('20:00') }],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Holiday Closure
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { sDate } from 'scdate'
|
|
316
|
+
|
|
317
|
+
const withHoliday: Schedule = {
|
|
318
|
+
...restaurant,
|
|
319
|
+
overrides: [
|
|
320
|
+
{
|
|
321
|
+
from: sDate('2025-12-25'), // Christmas Day
|
|
322
|
+
rules: [], // Empty array = closed
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Extended Holiday Hours
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
const withExtendedHours: Schedule = {
|
|
332
|
+
...restaurant,
|
|
333
|
+
overrides: [
|
|
334
|
+
{
|
|
335
|
+
from: sDate('2025-12-01'),
|
|
336
|
+
to: sDate('2025-12-31'),
|
|
337
|
+
rules: [
|
|
338
|
+
{
|
|
339
|
+
// Extended hours for December, weekends only
|
|
340
|
+
weekdays: sWeekdays('S-----S'),
|
|
341
|
+
times: [{ from: sTime('08:00'), to: sTime('23:00') }],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Permanent Schedule Change
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
const newSchedule: Schedule = {
|
|
353
|
+
...restaurant,
|
|
354
|
+
overrides: [
|
|
355
|
+
{
|
|
356
|
+
// New schedule starting Jan 1, 2026 (indefinite)
|
|
357
|
+
from: sDate('2026-01-01'),
|
|
358
|
+
// No 'to' date = applies indefinitely
|
|
359
|
+
rules: [
|
|
360
|
+
{
|
|
361
|
+
weekdays: sWeekdays('SMTWTFS'), // All days
|
|
362
|
+
times: [{ from: sTime('09:00'), to: sTime('21:00') }],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Cross-Midnight Hours (Late Night)
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
const lateNightBar: Schedule = {
|
|
374
|
+
timezone: 'America/Puerto_Rico',
|
|
375
|
+
weekly: [
|
|
376
|
+
{
|
|
377
|
+
weekdays: sWeekdays('----TFS'), // Thu-Sat
|
|
378
|
+
times: [{ from: sTime('20:00'), to: sTime('03:00') }], // 8PM-3AM
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// This means:
|
|
384
|
+
// - Thursday: 20:00-23:59, Friday: 00:00-03:00
|
|
385
|
+
// - Friday: 20:00-23:59, Saturday: 00:00-03:00
|
|
386
|
+
// - Saturday: 20:00-23:59, Sunday: 00:00-03:00
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Multiple Schedules (Layered Availability)
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { isScheduleAvailable } from 'scschedule'
|
|
393
|
+
|
|
394
|
+
const businessHours: Schedule = {
|
|
395
|
+
/* ... */
|
|
396
|
+
}
|
|
397
|
+
const breakfastMenu: Schedule = {
|
|
398
|
+
/* ... */
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Both must be available
|
|
402
|
+
const canOrderBreakfast =
|
|
403
|
+
isScheduleAvailable(businessHours, timestamp) &&
|
|
404
|
+
isScheduleAvailable(breakfastMenu, timestamp)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Validation Error Types
|
|
408
|
+
|
|
409
|
+
The library uses discriminated unions for type-safe error handling:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
type ValidationError =
|
|
413
|
+
| {
|
|
414
|
+
issue: ValidationIssue.InvalidTimezone
|
|
415
|
+
timezone: string
|
|
416
|
+
}
|
|
417
|
+
| {
|
|
418
|
+
issue: ValidationIssue.DuplicateOverrides
|
|
419
|
+
overrideIndexes: [number, number]
|
|
420
|
+
}
|
|
421
|
+
| {
|
|
422
|
+
issue: ValidationIssue.OverlappingSpecificOverrides
|
|
423
|
+
overrideIndexes: [number, number]
|
|
424
|
+
}
|
|
425
|
+
| {
|
|
426
|
+
issue: ValidationIssue.OverlappingTimesInRule
|
|
427
|
+
location:
|
|
428
|
+
| { type: RuleLocationType.Weekly; ruleIndex: number }
|
|
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
|
+
}
|
|
445
|
+
}
|
|
446
|
+
| {
|
|
447
|
+
issue: ValidationIssue.InvalidScDateFormat
|
|
448
|
+
field: string
|
|
449
|
+
value: string
|
|
450
|
+
expectedFormat: string
|
|
451
|
+
}
|
|
452
|
+
| {
|
|
453
|
+
issue: ValidationIssue.EmptyWeekdays
|
|
454
|
+
location:
|
|
455
|
+
| { type: RuleLocationType.Weekly; ruleIndex: number }
|
|
456
|
+
| {
|
|
457
|
+
type: RuleLocationType.Override
|
|
458
|
+
overrideIndex: number
|
|
459
|
+
ruleIndex: number
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
| {
|
|
463
|
+
issue: ValidationIssue.OverrideWeekdaysMismatch
|
|
464
|
+
overrideIndex: number
|
|
465
|
+
ruleIndex: number
|
|
466
|
+
weekdays: string
|
|
467
|
+
dateRange: { from: string; to: string }
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Best Practices
|
|
472
|
+
|
|
473
|
+
1. **Always validate schedules** before using them in production
|
|
474
|
+
2. **Clean up expired overrides** periodically to keep schedules manageable
|
|
475
|
+
3. **Use specific date ranges** for overrides when possible - indefinite overrides are useful for permanent schedule changes
|
|
476
|
+
4. **When using multiple indefinite overrides**, remember that the most recent one (latest `from` date) takes precedence
|
|
477
|
+
5. **Test cross-midnight ranges** thoroughly if your schedule uses them
|
|
478
|
+
6. **Consider timezone** carefully - all times are interpreted in the schedule's timezone
|
|
479
|
+
7. **Handle DST transitions** by testing schedules during spring forward and fall back
|
|
480
|
+
|
|
481
|
+
## Edge Cases
|
|
482
|
+
|
|
483
|
+
### DST Transitions
|
|
484
|
+
|
|
485
|
+
The library handles DST transitions using scdate's timezone functions. Times that fall in "missing hours" (spring forward) are treated as unavailable.
|
|
486
|
+
|
|
487
|
+
### Cross-Midnight Ranges
|
|
488
|
+
|
|
489
|
+
Time ranges that cross midnight (e.g., `22:00-02:00`) are split internally and always spill into the next calendar day, regardless of whether that day is in the weekdays pattern.
|
|
490
|
+
|
|
491
|
+
### Overlapping Overrides
|
|
492
|
+
|
|
493
|
+
Specific overrides (with both `from` and `to`) cannot overlap - this is a validation error. However, indefinite overrides can coexist with each other and with future specific overrides. When multiple indefinite overrides could apply to a date, the one with the latest `from` date is used (most recent policy wins).
|
|
494
|
+
|
|
495
|
+
## TypeScript Support
|
|
496
|
+
|
|
497
|
+
The library is written in TypeScript and provides full type definitions. All types are exported for use in your code:
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
import type {
|
|
501
|
+
Schedule,
|
|
502
|
+
WeeklyScheduleRule,
|
|
503
|
+
OverrideScheduleRule,
|
|
504
|
+
TimeRange,
|
|
505
|
+
AvailabilityRange,
|
|
506
|
+
ValidationError,
|
|
507
|
+
ValidationResult,
|
|
508
|
+
} from 'scschedule'
|
|
509
|
+
```
|
|
32
510
|
|
|
33
511
|
## Dependencies
|
|
34
512
|
|
|
@@ -36,6 +514,12 @@ This package depends on:
|
|
|
36
514
|
|
|
37
515
|
- `scdate`: Core date and time handling library
|
|
38
516
|
|
|
517
|
+
All of scdate's peer dependencies are also required.
|
|
518
|
+
|
|
39
519
|
## License
|
|
40
520
|
|
|
41
521
|
MIT
|
|
522
|
+
|
|
523
|
+
## Contributing
|
|
524
|
+
|
|
525
|
+
Issues and pull requests are welcome on [GitHub](https://github.com/ericvera/scdate).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type SDate } from 'scdate';
|
|
2
|
+
import type { Schedule } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Removes expired overrides from a schedule that ended before the
|
|
5
|
+
* specified date. It does not remove indefinite overrides (no to date).
|
|
6
|
+
*/
|
|
7
|
+
export declare const cleanupExpiredOverridesFromSchedule: (schedule: Schedule, beforeDate: SDate | string) => Schedule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isSameDateOrAfter } from 'scdate';
|
|
2
|
+
/**
|
|
3
|
+
* Removes expired overrides from a schedule that ended before the
|
|
4
|
+
* specified date. It does not remove indefinite overrides (no to date).
|
|
5
|
+
*/
|
|
6
|
+
export const cleanupExpiredOverridesFromSchedule = (schedule, beforeDate) => {
|
|
7
|
+
// If there are no overrides, return the schedule as-is
|
|
8
|
+
if (!schedule.overrides || schedule.overrides.length === 0) {
|
|
9
|
+
return schedule;
|
|
10
|
+
}
|
|
11
|
+
// Filter out overrides that have ended before the given date
|
|
12
|
+
const filteredOverrides = schedule.overrides.filter((override) => {
|
|
13
|
+
// Keep indefinite overrides (no 'to' date)
|
|
14
|
+
if (!override.to) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// Keep overrides that end on or after beforeDate
|
|
18
|
+
return isSameDateOrAfter(override.to, beforeDate);
|
|
19
|
+
});
|
|
20
|
+
// Return new Schedule instance with filtered overrides
|
|
21
|
+
if (filteredOverrides.length === 0) {
|
|
22
|
+
const { overrides, ...rest } = schedule;
|
|
23
|
+
return rest;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...schedule,
|
|
27
|
+
overrides: filteredOverrides,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumeration of all possible validation issues that can occur when validating
|
|
3
|
+
* a schedule. Used in ValidationError to identify the specific type of
|
|
4
|
+
* validation failure.
|
|
5
|
+
*/
|
|
6
|
+
export declare enum ValidationIssue {
|
|
7
|
+
/** The timezone string is not a valid IANA timezone identifier */
|
|
8
|
+
InvalidTimezone = "invalid-timezone",
|
|
9
|
+
/** Two or more specific overrides have identical date ranges */
|
|
10
|
+
DuplicateOverrides = "duplicate-overrides",
|
|
11
|
+
/** Two or more specific overrides have overlapping date ranges */
|
|
12
|
+
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
|
+
/**
|
|
21
|
+
* A field contains an invalid scdate format (SDate, STime, SWeekdays, or
|
|
22
|
+
* STimestamp)
|
|
23
|
+
*/
|
|
24
|
+
InvalidScDateFormat = "invalid-scdate-format",
|
|
25
|
+
/**
|
|
26
|
+
* A rule has a weekdays pattern with no days selected (e.g., '-------')
|
|
27
|
+
*/
|
|
28
|
+
EmptyWeekdays = "empty-weekdays",
|
|
29
|
+
/**
|
|
30
|
+
* An override has weekdays that don't match any actual dates in the
|
|
31
|
+
* override's date range, making it effectively closed (should use empty rules
|
|
32
|
+
* instead)
|
|
33
|
+
*/
|
|
34
|
+
OverrideWeekdaysMismatch = "override-weekdays-mismatch",
|
|
35
|
+
/**
|
|
36
|
+
* Two or more rules in the weekly schedule have overlapping weekdays and
|
|
37
|
+
* time ranges
|
|
38
|
+
*/
|
|
39
|
+
OverlappingRulesInWeekly = "overlapping-rules-in-weekly",
|
|
40
|
+
/**
|
|
41
|
+
* Two or more rules within the same override have overlapping weekdays and
|
|
42
|
+
* time ranges
|
|
43
|
+
*/
|
|
44
|
+
OverlappingRulesInOverride = "overlapping-rules-in-override",
|
|
45
|
+
/**
|
|
46
|
+
* Cross-midnight spillover (from weekly rule or previous override) conflicts
|
|
47
|
+
* with override's first day time ranges
|
|
48
|
+
*/
|
|
49
|
+
SpilloverConflictIntoOverrideFirstDay = "spillover-conflict-into-override-first-day",
|
|
50
|
+
/**
|
|
51
|
+
* Cross-midnight spillover from override's last day conflicts with next
|
|
52
|
+
* day's time ranges (weekly or another override)
|
|
53
|
+
*/
|
|
54
|
+
SpilloverConflictOverrideIntoNext = "spillover-conflict-override-into-next",
|
|
55
|
+
/**
|
|
56
|
+
* An override has a 'to' date that is before the 'from' date
|
|
57
|
+
*/
|
|
58
|
+
InvalidOverrideDateOrder = "invalid-override-date-order"
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Enumeration of rule location types used in validation errors to indicate
|
|
62
|
+
* where a validation issue occurred within a schedule structure.
|
|
63
|
+
*/
|
|
64
|
+
export declare enum RuleLocationType {
|
|
65
|
+
/** The rule is in the weekly schedule section */
|
|
66
|
+
Weekly = "weekly",
|
|
67
|
+
/** The rule is in an override section */
|
|
68
|
+
Override = "override"
|
|
69
|
+
}
|