scschedule 2.1.1 → 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.
Files changed (34) hide show
  1. package/README.md +142 -41
  2. package/dist/cleanupExpiredOverridesFromSchedule.d.ts +4 -0
  3. package/dist/cleanupExpiredOverridesFromSchedule.js +4 -0
  4. package/dist/constants.d.ts +1 -3
  5. package/dist/constants.js +1 -3
  6. package/dist/getAvailableRangesFromSchedule.d.ts +10 -0
  7. package/dist/getAvailableRangesFromSchedule.js +19 -0
  8. package/dist/getNextAvailableFromSchedule.d.ts +9 -8
  9. package/dist/getNextAvailableFromSchedule.js +14 -8
  10. package/dist/getNextUnavailableFromSchedule.d.ts +19 -9
  11. package/dist/getNextUnavailableFromSchedule.js +124 -63
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.js +4 -2
  14. package/dist/internal/getApplicableRuleForDate.d.ts +11 -11
  15. package/dist/internal/getApplicableRuleForDate.js +11 -7
  16. package/dist/internal/index.d.ts +1 -3
  17. package/dist/internal/index.js +1 -3
  18. package/dist/internal/validateNoEmptyWeekdays.js +2 -1
  19. package/dist/internal/validateNoOverlappingRules.js +5 -4
  20. package/dist/internal/validateNoOverlappingTimesInRule.js +2 -1
  21. package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +4 -2
  22. package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +71 -54
  23. package/dist/internal/validateNonEmptyTimes.js +2 -1
  24. package/dist/internal/validateScDateFormats.js +2 -1
  25. package/dist/isScheduleAvailable.d.ts +9 -0
  26. package/dist/isScheduleAvailable.js +19 -2
  27. package/dist/types.d.ts +12 -18
  28. package/dist/validateSchedule.d.ts +4 -2
  29. package/dist/validateSchedule.js +4 -4
  30. package/package.json +2 -2
  31. package/dist/internal/isValidTimezone.d.ts +0 -4
  32. package/dist/internal/isValidTimezone.js +0 -12
  33. package/dist/internal/validateTimezone.d.ts +0 -5
  34. 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
- - **Timezone aware**: All operations use schedule's timezone for calculations
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: Tuesday-Saturday, 11:00-22:00
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'), // Tue-Sat
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
- - **timezone**: The timezone for all time calculations
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
- timezone: string
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 timezone (in `Intl.supportedValuesOf('timeZone')`)
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 rules (same weekday)
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
- - Valid scdate formats (SDate, STime, SWeekdays)
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?: number): STimestamp | undefined`
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 (default: 365).
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(restaurant, now)
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'), // Tue-Sat
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'), // Tue-Sat
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.InvalidTimezone
415
- timezone: string
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. **Consider timezone** carefully - all times are interpreted in the schedule's timezone
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 timezone functions. Times that fall in "missing hours" (spring forward) are treated as unavailable.
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
@@ -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, finding the earliest time range start on the current day that
19
- * occurs after fromTimestamp
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 - The schedule to check availability against
30
- * @param fromTimestamp - The starting timestamp to search from
31
- * @param maxDaysToSearch - Maximum number of days to search forward (default:
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?: number) => STimestamp | undefined;
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, finding the earliest time range start on the current day that
20
- * occurs after fromTimestamp
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 - The schedule to check availability against
31
- * @param fromTimestamp - The starting timestamp to search from
32
- * @param maxDaysToSearch - Maximum number of days to search forward (default:
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 = 365) => {
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. Collecting all potential "end of availability" candidates from:
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
- * 3. Sorting candidates and returning the earliest one that's unavailable
22
+ * 4. Sorting candidates and returning the earliest one that's unavailable
20
23
  *
21
- * Note: No day-by-day iteration is needed because if we're available, we must
22
- * be in some range, and that range ends within at most 24 hours (or ~48 hours
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 - The schedule to check availability against
26
- * @param fromTimestamp - The starting timestamp to search from
27
- * @returns The next unavailable timestamp, or undefined if always available
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;