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
|
@@ -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,21 +66,24 @@ 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) {
|
|
71
77
|
if (!doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
72
78
|
continue;
|
|
73
79
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
earliestTime = timeRange.from;
|
|
82
|
-
}
|
|
80
|
+
// Day 0: only consider ranges starting after fromTimestamp's time
|
|
81
|
+
// Day 1+: consider all ranges (any start time qualifies)
|
|
82
|
+
if (day === 0 && !isAfterTime(rule.from, fromTime)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!earliestTime || isBeforeTime(rule.from, earliestTime)) {
|
|
86
|
+
earliestTime = rule.from;
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
if (earliestTime) {
|
|
@@ -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;
|
|
@@ -1,31 +1,107 @@
|
|
|
1
1
|
import { addDaysToDate, addMinutesToTimestamp, doesWeekdaysIncludeWeekday, getDateFromTimestamp, getTimeFromTimestamp, getTimestampFromDateAndTime, getWeekdayFromDate, isAfterTime, isBeforeTimestamp, isSameTimeOrAfter, sTimestamp, } from 'scdate';
|
|
2
2
|
import { getApplicableRuleForDate } from './internal/getApplicableRuleForDate.js';
|
|
3
3
|
import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
4
|
+
/**
|
|
5
|
+
* Collects "range end + 1 minute" candidate timestamps for a given date's
|
|
6
|
+
* rules. For cross-midnight ranges, the candidate falls on the next day.
|
|
7
|
+
* When afterTime is provided, regular (non-cross-midnight) ranges ending
|
|
8
|
+
* before afterTime are excluded.
|
|
9
|
+
*/
|
|
10
|
+
const collectRangeEndCandidates = (rules, date, timeZone, afterTime) => {
|
|
11
|
+
const candidates = [];
|
|
12
|
+
const weekday = getWeekdayFromDate(date);
|
|
13
|
+
for (const rule of rules) {
|
|
14
|
+
if (!doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (isAfterTime(rule.from, rule.to)) {
|
|
18
|
+
// Cross-midnight range ends tomorrow
|
|
19
|
+
const tomorrow = addDaysToDate(date, 1);
|
|
20
|
+
const rangeEnd = getTimestampFromDateAndTime(tomorrow, rule.to);
|
|
21
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (afterTime && !isSameTimeOrAfter(rule.to, afterTime)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const rangeEnd = getTimestampFromDateAndTime(date, rule.to);
|
|
28
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return candidates;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Collects candidate timestamps from the previous day's cross-midnight ranges
|
|
35
|
+
* that spill into the current date. Only includes spillover endings at or
|
|
36
|
+
* after afterTime.
|
|
37
|
+
*/
|
|
38
|
+
const collectSpilloverCandidates = (schedule, currentDate, timeZone, afterTime) => {
|
|
39
|
+
const candidates = [];
|
|
40
|
+
const previousDate = addDaysToDate(currentDate, -1);
|
|
41
|
+
const previousWeekday = getWeekdayFromDate(previousDate);
|
|
42
|
+
const previousResult = getApplicableRuleForDate(schedule, previousDate.date);
|
|
43
|
+
if (previousResult.rules === true) {
|
|
44
|
+
return candidates;
|
|
45
|
+
}
|
|
46
|
+
for (const rule of previousResult.rules) {
|
|
47
|
+
if (!doesWeekdaysIncludeWeekday(rule.weekdays, previousWeekday)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Only cross-midnight ranges (from > to) spill into today
|
|
51
|
+
if (!isAfterTime(rule.from, rule.to)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!isSameTimeOrAfter(rule.to, afterTime)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const rangeEnd = getTimestampFromDateAndTime(currentDate, rule.to);
|
|
58
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
59
|
+
}
|
|
60
|
+
return candidates;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Sorts timestamps chronologically and returns the first one where the
|
|
64
|
+
* schedule is unavailable, or undefined if all are available.
|
|
65
|
+
*/
|
|
66
|
+
const findFirstUnavailableTimestamp = (schedule, candidates) => {
|
|
67
|
+
candidates.sort((a, b) => (isBeforeTimestamp(a, b) ? -1 : 1));
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (!isScheduleAvailable(schedule, candidate)) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
};
|
|
4
75
|
/**
|
|
5
76
|
* Finds the next unavailable timestamp in a schedule starting from the
|
|
6
77
|
* specified timestamp.
|
|
7
78
|
*
|
|
8
79
|
* This function searches forward from the given timestamp to find when the
|
|
9
80
|
* schedule next becomes unavailable. It handles:
|
|
81
|
+
* - Always-available schedules (`weekly: true`) by searching for overrides
|
|
10
82
|
* - Same-day unavailability (gaps between time ranges or after the last range)
|
|
11
83
|
* - Cross-midnight ranges (unavailability after a range that crosses midnight)
|
|
12
84
|
* - Date overrides (temporary closures)
|
|
13
85
|
*
|
|
14
86
|
* The algorithm works by:
|
|
15
87
|
* 1. Checking if fromTimestamp is already unavailable
|
|
16
|
-
* 2.
|
|
88
|
+
* 2. If `weekly` is `true`, searching forward day-by-day for an override that
|
|
89
|
+
* closes availability
|
|
90
|
+
* 3. Otherwise, collecting all potential "end of availability" candidates from:
|
|
17
91
|
* - Previous day's cross-midnight spillover (ends today at `to` time)
|
|
18
92
|
* - Current day's regular ranges (end today at `to` time)
|
|
19
93
|
* - Current day's cross-midnight ranges (end tomorrow at `to` time)
|
|
20
|
-
*
|
|
94
|
+
* 4. Sorting candidates and returning the earliest one that's unavailable
|
|
21
95
|
*
|
|
22
|
-
* Note:
|
|
23
|
-
*
|
|
24
|
-
* for cross-midnight ranges).
|
|
96
|
+
* Note: For rule-based schedules, no day-by-day iteration is needed because if
|
|
97
|
+
* available, the current range ends within at most ~48 hours (cross-midnight).
|
|
25
98
|
*
|
|
26
|
-
* @param schedule
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
99
|
+
* @param schedule The schedule to check availability against.
|
|
100
|
+
* @param timeZone IANA time zone identifier for timestamp arithmetic.
|
|
101
|
+
* @param fromTimestamp The starting timestamp to search from.
|
|
102
|
+
* @param maxDaysToSearch Maximum number of days to search forward.
|
|
103
|
+
* @returns The next unavailable timestamp, or undefined if no unavailability
|
|
104
|
+
* is found within the search window.
|
|
29
105
|
*
|
|
30
106
|
* @example
|
|
31
107
|
* // Schedule: Mon-Fri 09:00-17:00
|
|
@@ -41,8 +117,13 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
41
117
|
* // Schedule: Thu-Sat 20:00-02:00 (cross-midnight)
|
|
42
118
|
* // Query: Thursday at 23:00
|
|
43
119
|
* // Returns: Friday at 02:01 (after shift ends)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // Schedule: weekly: true, override closing Dec 25
|
|
123
|
+
* // Query: Dec 20 at 10:00
|
|
124
|
+
* // Returns: Dec 25 at 00:00
|
|
44
125
|
*/
|
|
45
|
-
export const getNextUnavailableFromSchedule = (schedule, fromTimestamp) => {
|
|
126
|
+
export const getNextUnavailableFromSchedule = (schedule, timeZone, fromTimestamp, maxDaysToSearch) => {
|
|
46
127
|
const initialTimestamp = sTimestamp(fromTimestamp);
|
|
47
128
|
// Check if already unavailable at fromTimestamp
|
|
48
129
|
if (!isScheduleAvailable(schedule, initialTimestamp)) {
|
|
@@ -50,63 +131,39 @@ export const getNextUnavailableFromSchedule = (schedule, fromTimestamp) => {
|
|
|
50
131
|
}
|
|
51
132
|
const currentDate = getDateFromTimestamp(initialTimestamp);
|
|
52
133
|
const fromTime = getTimeFromTimestamp(initialTimestamp);
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
const rangeEnd = getTimestampFromDateAndTime(currentDate, timeRange.to);
|
|
74
|
-
const afterRangeEnd = addMinutesToTimestamp(rangeEnd, 1, schedule.timezone);
|
|
75
|
-
candidates.push(afterRangeEnd);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
// 2. Check current day's ranges
|
|
79
|
-
const weekday = getWeekdayFromDate(currentDate);
|
|
80
|
-
const { rules } = getApplicableRuleForDate(schedule, currentDate.date);
|
|
81
|
-
for (const rule of rules) {
|
|
82
|
-
if (!doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
for (const timeRange of rule.times) {
|
|
86
|
-
// Cross-midnight range (from > to) ends TOMORROW
|
|
87
|
-
if (isAfterTime(timeRange.from, timeRange.to)) {
|
|
88
|
-
const tomorrow = addDaysToDate(currentDate, 1);
|
|
89
|
-
const rangeEnd = getTimestampFromDateAndTime(tomorrow, timeRange.to);
|
|
90
|
-
const afterRangeEnd = addMinutesToTimestamp(rangeEnd, 1, schedule.timezone);
|
|
91
|
-
candidates.push(afterRangeEnd);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// Regular range ends today - only consider if end >= current time
|
|
95
|
-
if (!isSameTimeOrAfter(timeRange.to, fromTime)) {
|
|
96
|
-
continue;
|
|
134
|
+
// When weekly is true (always available), search forward for an override
|
|
135
|
+
// that closes availability.
|
|
136
|
+
const initialResult = getApplicableRuleForDate(schedule, currentDate.date);
|
|
137
|
+
if (initialResult.rules === true) {
|
|
138
|
+
// rules === true implies source === 'weekly', so skip current date
|
|
139
|
+
// and start searching from the next day.
|
|
140
|
+
let searchDate = addDaysToDate(currentDate, 1);
|
|
141
|
+
for (let day = 1; day < maxDaysToSearch; day++) {
|
|
142
|
+
const result = getApplicableRuleForDate(schedule, searchDate.date);
|
|
143
|
+
// If we are here it means that weekly is true, so we need to search for
|
|
144
|
+
// an override that closes availability.
|
|
145
|
+
if (result.source === 'override') {
|
|
146
|
+
const dayStart = getTimestampFromDateAndTime(searchDate, '00:00');
|
|
147
|
+
const nextUnavailable = findFirstUnavailableTimestamp(schedule, [
|
|
148
|
+
dayStart,
|
|
149
|
+
...collectRangeEndCandidates(result.rules, searchDate, timeZone),
|
|
150
|
+
]);
|
|
151
|
+
if (nextUnavailable) {
|
|
152
|
+
return nextUnavailable;
|
|
97
153
|
}
|
|
98
|
-
const rangeEnd = getTimestampFromDateAndTime(currentDate, timeRange.to);
|
|
99
|
-
const afterRangeEnd = addMinutesToTimestamp(rangeEnd, 1, schedule.timezone);
|
|
100
|
-
candidates.push(afterRangeEnd);
|
|
101
154
|
}
|
|
155
|
+
searchDate = addDaysToDate(searchDate, 1);
|
|
102
156
|
}
|
|
157
|
+
return undefined;
|
|
103
158
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
159
|
+
// Collect candidates from previous day's cross-midnight spillover and
|
|
160
|
+
// current day's ranges, then return the earliest unavailable one.
|
|
161
|
+
const { rules } = getApplicableRuleForDate(schedule, currentDate.date);
|
|
162
|
+
if (rules === true) {
|
|
163
|
+
return undefined;
|
|
110
164
|
}
|
|
111
|
-
return
|
|
165
|
+
return findFirstUnavailableTimestamp(schedule, [
|
|
166
|
+
...collectSpilloverCandidates(schedule, currentDate, timeZone, fromTime),
|
|
167
|
+
...collectRangeEndCandidates(rules, currentDate, timeZone, fromTime),
|
|
168
|
+
]);
|
|
112
169
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
export { isValidTimeZone } from 'scdate';
|
|
1
2
|
export type { SDate, STime, STimestamp, SWeekdays } from 'scdate';
|
|
2
3
|
export * from './constants.js';
|
|
3
4
|
export type * from './types.js';
|
|
4
5
|
export * from './validateSchedule.js';
|
|
5
6
|
export * from './cleanupExpiredOverridesFromSchedule.js';
|
|
6
|
-
export * from './
|
|
7
|
+
export * from './getAvailableRangesFromSchedule.js';
|
|
7
8
|
export * from './getNextAvailableFromSchedule.js';
|
|
8
9
|
export * from './getNextUnavailableFromSchedule.js';
|
|
9
|
-
export * from './
|
|
10
|
+
export * from './isScheduleAvailable.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Re-export scdate types and utilities for convenience
|
|
2
|
+
export { isValidTimeZone } from 'scdate';
|
|
1
3
|
// Export constants
|
|
2
4
|
export * from './constants.js';
|
|
3
5
|
// Export validation functions
|
|
@@ -5,7 +7,7 @@ export * from './validateSchedule.js';
|
|
|
5
7
|
// Export schedule management functions
|
|
6
8
|
export * from './cleanupExpiredOverridesFromSchedule.js';
|
|
7
9
|
// Export availability query functions
|
|
8
|
-
export * from './
|
|
10
|
+
export * from './getAvailableRangesFromSchedule.js';
|
|
9
11
|
export * from './getNextAvailableFromSchedule.js';
|
|
10
12
|
export * from './getNextUnavailableFromSchedule.js';
|
|
11
|
-
export * from './
|
|
13
|
+
export * from './isScheduleAvailable.js';
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { SDate } from 'scdate';
|
|
2
2
|
import type { Schedule, SDateString, WeeklyScheduleRule } from '../types.js';
|
|
3
|
-
export type
|
|
4
|
-
|
|
3
|
+
export type ApplicableRule = {
|
|
4
|
+
source: 'weekly';
|
|
5
|
+
rules: WeeklyScheduleRule[] | true;
|
|
5
6
|
} | {
|
|
6
|
-
|
|
7
|
+
source: 'override';
|
|
7
8
|
overrideIndex: number;
|
|
8
|
-
};
|
|
9
|
-
export interface RuleWithSource {
|
|
10
9
|
rules: WeeklyScheduleRule[];
|
|
11
|
-
|
|
12
|
-
}
|
|
10
|
+
};
|
|
13
11
|
/**
|
|
14
12
|
* Determines which rules apply for a given date based on overrides or weekly
|
|
15
|
-
* schedule
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* schedule. Returns a discriminated union indicating the source (`'weekly'` or
|
|
14
|
+
* `'override'`) and the applicable rules. When the source is `'weekly'` and
|
|
15
|
+
* the schedule has `weekly: true`, the returned `rules` will be `true`.
|
|
16
|
+
*
|
|
17
|
+
* When multiple overrides apply, selects the most specific (shortest duration).
|
|
18
18
|
*
|
|
19
19
|
* Priority:
|
|
20
20
|
* 1. Specific overrides (with 'to' date) - shortest duration wins
|
|
21
21
|
* 2. Indefinite overrides (no 'to' date) - latest 'from' date wins
|
|
22
22
|
* 3. Weekly schedule (fallback)
|
|
23
23
|
*/
|
|
24
|
-
export declare const getApplicableRuleForDate: (schedule: Schedule, date: SDate | SDateString) =>
|
|
24
|
+
export declare const getApplicableRuleForDate: (schedule: Schedule, date: SDate | SDateString) => ApplicableRule;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getDaysBetweenDates, isSameDateOrAfter, isSameDateOrBefore, } from 'scdate';
|
|
2
2
|
/**
|
|
3
3
|
* Determines which rules apply for a given date based on overrides or weekly
|
|
4
|
-
* schedule
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* schedule. Returns a discriminated union indicating the source (`'weekly'` or
|
|
5
|
+
* `'override'`) and the applicable rules. When the source is `'weekly'` and
|
|
6
|
+
* the schedule has `weekly: true`, the returned `rules` will be `true`.
|
|
7
|
+
*
|
|
8
|
+
* When multiple overrides apply, selects the most specific (shortest duration).
|
|
7
9
|
*
|
|
8
10
|
* Priority:
|
|
9
11
|
* 1. Specific overrides (with 'to' date) - shortest duration wins
|
|
@@ -15,8 +17,8 @@ export const getApplicableRuleForDate = (schedule, date) => {
|
|
|
15
17
|
// schedule)
|
|
16
18
|
if (!schedule.overrides || schedule.overrides.length === 0) {
|
|
17
19
|
return {
|
|
20
|
+
source: 'weekly',
|
|
18
21
|
rules: schedule.weekly,
|
|
19
|
-
source: { type: 'weekly' },
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
// - Check the case where there are overrides and collect all specific
|
|
@@ -48,8 +50,9 @@ export const getApplicableRuleForDate = (schedule, date) => {
|
|
|
48
50
|
return currentDuration < shortestDuration ? current : shortest;
|
|
49
51
|
});
|
|
50
52
|
return {
|
|
53
|
+
source: 'override',
|
|
54
|
+
overrideIndex: mostSpecific.index,
|
|
51
55
|
rules: mostSpecific.override.rules,
|
|
52
|
-
source: { type: 'override', overrideIndex: mostSpecific.index },
|
|
53
56
|
};
|
|
54
57
|
}
|
|
55
58
|
// - At this point, we know there are no applicable overrides with from and
|
|
@@ -70,13 +73,14 @@ export const getApplicableRuleForDate = (schedule, date) => {
|
|
|
70
73
|
: latest;
|
|
71
74
|
});
|
|
72
75
|
return {
|
|
76
|
+
source: 'override',
|
|
77
|
+
overrideIndex: mostRecent.index,
|
|
73
78
|
rules: mostRecent.override.rules,
|
|
74
|
-
source: { type: 'override', overrideIndex: mostRecent.index },
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
// - If there are no applicable indefinite overrides, return the weekly rules
|
|
78
82
|
return {
|
|
83
|
+
source: 'weekly',
|
|
79
84
|
rules: schedule.weekly,
|
|
80
|
-
source: { type: 'weekly' },
|
|
81
85
|
};
|
|
82
86
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Weekday } from 'scdate';
|
|
2
|
-
import type {
|
|
2
|
+
import type { WeeklyScheduleRule } from '../types.js';
|
|
3
|
+
import type { TimeRange } from './types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Gets the effective time ranges that apply to a specific weekday for a given
|
|
5
6
|
* rule, accounting for both direct ranges and cross-midnight spillover from
|
|
@@ -14,13 +14,10 @@ export const getEffectiveTimesForWeekday = (rule, weekday) => {
|
|
|
14
14
|
const previousWeekday = getPreviousWeekday(weekday);
|
|
15
15
|
// Check if this weekday is directly included in the rule's weekdays
|
|
16
16
|
if (doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
effectiveTimes.push(splitRanges[0]);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
17
|
+
const splitRanges = splitCrossMidnightTimeRange(rule);
|
|
18
|
+
if (splitRanges.length > 0 && splitRanges[0]) {
|
|
19
|
+
effectiveTimes.push(splitRanges[0]);
|
|
20
|
+
}
|
|
24
21
|
}
|
|
25
22
|
// Check if previous weekday has cross-midnight ranges that spill into this
|
|
26
23
|
// weekday.
|
|
@@ -30,13 +27,11 @@ export const getEffectiveTimesForWeekday = (rule, weekday) => {
|
|
|
30
27
|
// only those that occur in the date range. This ensures spillover is only
|
|
31
28
|
// included from days that actually exist in the override period.
|
|
32
29
|
if (doesWeekdaysIncludeWeekday(rule.weekdays, previousWeekday)) {
|
|
33
|
-
rule
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
});
|
|
30
|
+
const splitRanges = splitCrossMidnightTimeRange(rule);
|
|
31
|
+
// If there are 2 ranges, the second one is the spillover to next day
|
|
32
|
+
if (splitRanges.length === 2 && splitRanges[1]) {
|
|
33
|
+
effectiveTimes.push(splitRanges[1]);
|
|
34
|
+
}
|
|
40
35
|
}
|
|
41
36
|
return effectiveTimes;
|
|
42
37
|
};
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
+
export type * from './types.js';
|
|
1
2
|
export * from './doOverridesOverlap.js';
|
|
2
3
|
export * from './doRulesOverlap.js';
|
|
3
4
|
export * from './doTimeRangesOverlap.js';
|
|
4
5
|
export * from './getApplicableRuleForDate.js';
|
|
5
6
|
export * from './getEffectiveTimesForWeekday.js';
|
|
6
7
|
export * from './isTimeInTimeRange.js';
|
|
7
|
-
export * from './isValidTimezone.js';
|
|
8
8
|
export * from './normalizeScheduleForValidation.js';
|
|
9
9
|
export * from './splitCrossMidnightTimeRange.js';
|
|
10
10
|
export * from './validateNoEmptyWeekdays.js';
|
|
11
11
|
export * from './validateNoOverlappingOverrides.js';
|
|
12
12
|
export * from './validateNoOverlappingRules.js';
|
|
13
|
-
export * from './validateNoOverlappingTimesInRule.js';
|
|
14
13
|
export * from './validateNoSpilloverConflictsAtOverrideBoundaries.js';
|
|
15
|
-
export * from './validateNonEmptyTimes.js';
|
|
16
14
|
export * from './validateOverrideDateOrder.js';
|
|
17
15
|
export * from './validateOverrideWeekdaysMatchDates.js';
|
|
18
16
|
export * from './validateScDateFormats.js';
|
|
19
|
-
export * from './validateTimezone.js';
|
package/dist/internal/index.js
CHANGED
|
@@ -5,17 +5,13 @@ export * from './doTimeRangesOverlap.js';
|
|
|
5
5
|
export * from './getApplicableRuleForDate.js';
|
|
6
6
|
export * from './getEffectiveTimesForWeekday.js';
|
|
7
7
|
export * from './isTimeInTimeRange.js';
|
|
8
|
-
export * from './isValidTimezone.js';
|
|
9
8
|
export * from './normalizeScheduleForValidation.js';
|
|
10
9
|
export * from './splitCrossMidnightTimeRange.js';
|
|
11
10
|
// Internal validation helpers
|
|
12
11
|
export * from './validateNoEmptyWeekdays.js';
|
|
13
12
|
export * from './validateNoOverlappingOverrides.js';
|
|
14
13
|
export * from './validateNoOverlappingRules.js';
|
|
15
|
-
export * from './validateNoOverlappingTimesInRule.js';
|
|
16
14
|
export * from './validateNoSpilloverConflictsAtOverrideBoundaries.js';
|
|
17
|
-
export * from './validateNonEmptyTimes.js';
|
|
18
15
|
export * from './validateOverrideDateOrder.js';
|
|
19
16
|
export * from './validateOverrideWeekdaysMatchDates.js';
|
|
20
17
|
export * from './validateScDateFormats.js';
|
|
21
|
-
export * from './validateTimezone.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type STime } from 'scdate';
|
|
2
|
-
import type { STimeString
|
|
2
|
+
import type { STimeString } from '../types.js';
|
|
3
|
+
import type { TimeRange } from './types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Checks if a time falls within a time range, with support for
|
|
5
6
|
* cross-midnight ranges (next-day portion).
|
|
@@ -5,8 +5,9 @@ import { isAfterTime, sTime } from 'scdate';
|
|
|
5
5
|
export const splitCrossMidnightTimeRange = (timeRange) => {
|
|
6
6
|
// Check if range crosses midnight
|
|
7
7
|
if (isAfterTime(timeRange.to, timeRange.from)) {
|
|
8
|
-
// Same day range
|
|
9
|
-
|
|
8
|
+
// Same day range — construct a new object to strip extra fields (e.g.,
|
|
9
|
+
// weekdays) when callers pass a WeeklyScheduleRule as a TimeRange.
|
|
10
|
+
return [{ from: timeRange.from, to: timeRange.to }];
|
|
10
11
|
}
|
|
11
12
|
// Cross-midnight range: split into two periods
|
|
12
13
|
return [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { STime } from 'scdate';
|
|
2
|
+
import type { STimeString } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents a time range within a day. Time ranges can cross midnight. For
|
|
5
|
+
* example, a range from 20:00 to 02:00 represents 8:00 PM to 2:00 AM the next
|
|
6
|
+
* day.
|
|
7
|
+
*/
|
|
8
|
+
export interface TimeRange {
|
|
9
|
+
/** Start time of the range (inclusive) */
|
|
10
|
+
from: STime | STimeString;
|
|
11
|
+
/** End time of the range (inclusive) */
|
|
12
|
+
to: STime | STimeString;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -8,7 +8,8 @@ import { RuleLocationType, ValidationIssue } from '../constants.js';
|
|
|
8
8
|
export const validateNoEmptyWeekdays = (schedule) => {
|
|
9
9
|
const errors = [];
|
|
10
10
|
// Check weekly rules
|
|
11
|
-
schedule.weekly
|
|
11
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
12
|
+
weeklyRules.forEach((rule, ruleIndex) => {
|
|
12
13
|
try {
|
|
13
14
|
if (isWeekdaysEmpty(rule.weekdays)) {
|
|
14
15
|
errors.push({
|