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.
- package/README.md +142 -41
- package/dist/cleanupExpiredOverridesFromSchedule.d.ts +4 -0
- package/dist/cleanupExpiredOverridesFromSchedule.js +4 -0
- package/dist/constants.d.ts +1 -3
- package/dist/constants.js +1 -3
- package/dist/getAvailableRangesFromSchedule.d.ts +10 -0
- package/dist/getAvailableRangesFromSchedule.js +19 -0
- package/dist/getNextAvailableFromSchedule.d.ts +9 -8
- package/dist/getNextAvailableFromSchedule.js +14 -8
- package/dist/getNextUnavailableFromSchedule.d.ts +19 -9
- package/dist/getNextUnavailableFromSchedule.js +124 -63
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/internal/getApplicableRuleForDate.d.ts +11 -11
- package/dist/internal/getApplicableRuleForDate.js +11 -7
- package/dist/internal/index.d.ts +1 -3
- package/dist/internal/index.js +1 -3
- package/dist/internal/validateNoEmptyWeekdays.js +2 -1
- package/dist/internal/validateNoOverlappingRules.js +5 -4
- package/dist/internal/validateNoOverlappingTimesInRule.js +2 -1
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +4 -2
- package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +71 -54
- package/dist/internal/validateNonEmptyTimes.js +2 -1
- package/dist/internal/validateScDateFormats.js +2 -1
- package/dist/isScheduleAvailable.d.ts +9 -0
- package/dist/isScheduleAvailable.js +19 -2
- package/dist/types.d.ts +12 -18
- package/dist/validateSchedule.d.ts +4 -2
- package/dist/validateSchedule.js +4 -4
- package/package.json +2 -2
- package/dist/internal/isValidTimezone.d.ts +0 -4
- package/dist/internal/isValidTimezone.js +0 -12
- package/dist/internal/validateTimezone.d.ts +0 -5
- package/dist/internal/validateTimezone.js +0 -16
|
@@ -1,31 +1,111 @@
|
|
|
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
|
+
for (const timeRange of rule.times) {
|
|
18
|
+
if (isAfterTime(timeRange.from, timeRange.to)) {
|
|
19
|
+
// Cross-midnight range ends tomorrow
|
|
20
|
+
const tomorrow = addDaysToDate(date, 1);
|
|
21
|
+
const rangeEnd = getTimestampFromDateAndTime(tomorrow, timeRange.to);
|
|
22
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (afterTime && !isSameTimeOrAfter(timeRange.to, afterTime)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const rangeEnd = getTimestampFromDateAndTime(date, timeRange.to);
|
|
29
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return candidates;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Collects candidate timestamps from the previous day's cross-midnight ranges
|
|
37
|
+
* that spill into the current date. Only includes spillover endings at or
|
|
38
|
+
* after afterTime.
|
|
39
|
+
*/
|
|
40
|
+
const collectSpilloverCandidates = (schedule, currentDate, timeZone, afterTime) => {
|
|
41
|
+
const candidates = [];
|
|
42
|
+
const previousDate = addDaysToDate(currentDate, -1);
|
|
43
|
+
const previousWeekday = getWeekdayFromDate(previousDate);
|
|
44
|
+
const previousResult = getApplicableRuleForDate(schedule, previousDate.date);
|
|
45
|
+
if (previousResult.rules === true) {
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
for (const rule of previousResult.rules) {
|
|
49
|
+
if (!doesWeekdaysIncludeWeekday(rule.weekdays, previousWeekday)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
for (const timeRange of rule.times) {
|
|
53
|
+
// Only cross-midnight ranges (from > to) spill into today
|
|
54
|
+
if (!isAfterTime(timeRange.from, timeRange.to)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!isSameTimeOrAfter(timeRange.to, afterTime)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const rangeEnd = getTimestampFromDateAndTime(currentDate, timeRange.to);
|
|
61
|
+
candidates.push(addMinutesToTimestamp(rangeEnd, 1, timeZone));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return candidates;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Sorts timestamps chronologically and returns the first one where the
|
|
68
|
+
* schedule is unavailable, or undefined if all are available.
|
|
69
|
+
*/
|
|
70
|
+
const findFirstUnavailableTimestamp = (schedule, candidates) => {
|
|
71
|
+
candidates.sort((a, b) => (isBeforeTimestamp(a, b) ? -1 : 1));
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
if (!isScheduleAvailable(schedule, candidate)) {
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
4
79
|
/**
|
|
5
80
|
* Finds the next unavailable timestamp in a schedule starting from the
|
|
6
81
|
* specified timestamp.
|
|
7
82
|
*
|
|
8
83
|
* This function searches forward from the given timestamp to find when the
|
|
9
84
|
* schedule next becomes unavailable. It handles:
|
|
85
|
+
* - Always-available schedules (`weekly: true`) by searching for overrides
|
|
10
86
|
* - Same-day unavailability (gaps between time ranges or after the last range)
|
|
11
87
|
* - Cross-midnight ranges (unavailability after a range that crosses midnight)
|
|
12
88
|
* - Date overrides (temporary closures)
|
|
13
89
|
*
|
|
14
90
|
* The algorithm works by:
|
|
15
91
|
* 1. Checking if fromTimestamp is already unavailable
|
|
16
|
-
* 2.
|
|
92
|
+
* 2. If `weekly` is `true`, searching forward day-by-day for an override that
|
|
93
|
+
* closes availability
|
|
94
|
+
* 3. Otherwise, collecting all potential "end of availability" candidates from:
|
|
17
95
|
* - Previous day's cross-midnight spillover (ends today at `to` time)
|
|
18
96
|
* - Current day's regular ranges (end today at `to` time)
|
|
19
97
|
* - Current day's cross-midnight ranges (end tomorrow at `to` time)
|
|
20
|
-
*
|
|
98
|
+
* 4. Sorting candidates and returning the earliest one that's unavailable
|
|
21
99
|
*
|
|
22
|
-
* Note:
|
|
23
|
-
*
|
|
24
|
-
* for cross-midnight ranges).
|
|
100
|
+
* Note: For rule-based schedules, no day-by-day iteration is needed because if
|
|
101
|
+
* available, the current range ends within at most ~48 hours (cross-midnight).
|
|
25
102
|
*
|
|
26
|
-
* @param schedule
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
103
|
+
* @param schedule The schedule to check availability against.
|
|
104
|
+
* @param timeZone IANA time zone identifier for timestamp arithmetic.
|
|
105
|
+
* @param fromTimestamp The starting timestamp to search from.
|
|
106
|
+
* @param maxDaysToSearch Maximum number of days to search forward.
|
|
107
|
+
* @returns The next unavailable timestamp, or undefined if no unavailability
|
|
108
|
+
* is found within the search window.
|
|
29
109
|
*
|
|
30
110
|
* @example
|
|
31
111
|
* // Schedule: Mon-Fri 09:00-17:00
|
|
@@ -41,8 +121,13 @@ import { isScheduleAvailable } from './isScheduleAvailable.js';
|
|
|
41
121
|
* // Schedule: Thu-Sat 20:00-02:00 (cross-midnight)
|
|
42
122
|
* // Query: Thursday at 23:00
|
|
43
123
|
* // Returns: Friday at 02:01 (after shift ends)
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* // Schedule: weekly: true, override closing Dec 25
|
|
127
|
+
* // Query: Dec 20 at 10:00
|
|
128
|
+
* // Returns: Dec 25 at 00:00
|
|
44
129
|
*/
|
|
45
|
-
export const getNextUnavailableFromSchedule = (schedule, fromTimestamp) => {
|
|
130
|
+
export const getNextUnavailableFromSchedule = (schedule, timeZone, fromTimestamp, maxDaysToSearch) => {
|
|
46
131
|
const initialTimestamp = sTimestamp(fromTimestamp);
|
|
47
132
|
// Check if already unavailable at fromTimestamp
|
|
48
133
|
if (!isScheduleAvailable(schedule, initialTimestamp)) {
|
|
@@ -50,63 +135,39 @@ export const getNextUnavailableFromSchedule = (schedule, fromTimestamp) => {
|
|
|
50
135
|
}
|
|
51
136
|
const currentDate = getDateFromTimestamp(initialTimestamp);
|
|
52
137
|
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;
|
|
138
|
+
// When weekly is true (always available), search forward for an override
|
|
139
|
+
// that closes availability.
|
|
140
|
+
const initialResult = getApplicableRuleForDate(schedule, currentDate.date);
|
|
141
|
+
if (initialResult.rules === true) {
|
|
142
|
+
// rules === true implies source === 'weekly', so skip current date
|
|
143
|
+
// and start searching from the next day.
|
|
144
|
+
let searchDate = addDaysToDate(currentDate, 1);
|
|
145
|
+
for (let day = 1; day < maxDaysToSearch; day++) {
|
|
146
|
+
const result = getApplicableRuleForDate(schedule, searchDate.date);
|
|
147
|
+
// If we are here it means that weekly is true, so we need to search for
|
|
148
|
+
// an override that closes availability.
|
|
149
|
+
if (result.source === 'override') {
|
|
150
|
+
const dayStart = getTimestampFromDateAndTime(searchDate, '00:00');
|
|
151
|
+
const nextUnavailable = findFirstUnavailableTimestamp(schedule, [
|
|
152
|
+
dayStart,
|
|
153
|
+
...collectRangeEndCandidates(result.rules, searchDate, timeZone),
|
|
154
|
+
]);
|
|
155
|
+
if (nextUnavailable) {
|
|
156
|
+
return nextUnavailable;
|
|
97
157
|
}
|
|
98
|
-
const rangeEnd = getTimestampFromDateAndTime(currentDate, timeRange.to);
|
|
99
|
-
const afterRangeEnd = addMinutesToTimestamp(rangeEnd, 1, schedule.timezone);
|
|
100
|
-
candidates.push(afterRangeEnd);
|
|
101
158
|
}
|
|
159
|
+
searchDate = addDaysToDate(searchDate, 1);
|
|
102
160
|
}
|
|
161
|
+
return undefined;
|
|
103
162
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
163
|
+
// Collect candidates from previous day's cross-midnight spillover and
|
|
164
|
+
// current day's ranges, then return the earliest unavailable one.
|
|
165
|
+
const { rules } = getApplicableRuleForDate(schedule, currentDate.date);
|
|
166
|
+
if (rules === true) {
|
|
167
|
+
return undefined;
|
|
110
168
|
}
|
|
111
|
-
return
|
|
169
|
+
return findFirstUnavailableTimestamp(schedule, [
|
|
170
|
+
...collectSpilloverCandidates(schedule, currentDate, timeZone, fromTime),
|
|
171
|
+
...collectRangeEndCandidates(rules, currentDate, timeZone, fromTime),
|
|
172
|
+
]);
|
|
112
173
|
};
|
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
|
};
|
package/dist/internal/index.d.ts
CHANGED
|
@@ -4,16 +4,14 @@ export * from './doTimeRangesOverlap.js';
|
|
|
4
4
|
export * from './getApplicableRuleForDate.js';
|
|
5
5
|
export * from './getEffectiveTimesForWeekday.js';
|
|
6
6
|
export * from './isTimeInTimeRange.js';
|
|
7
|
-
export * from './isValidTimezone.js';
|
|
8
7
|
export * from './normalizeScheduleForValidation.js';
|
|
9
8
|
export * from './splitCrossMidnightTimeRange.js';
|
|
10
9
|
export * from './validateNoEmptyWeekdays.js';
|
|
10
|
+
export * from './validateNonEmptyTimes.js';
|
|
11
11
|
export * from './validateNoOverlappingOverrides.js';
|
|
12
12
|
export * from './validateNoOverlappingRules.js';
|
|
13
13
|
export * from './validateNoOverlappingTimesInRule.js';
|
|
14
14
|
export * from './validateNoSpilloverConflictsAtOverrideBoundaries.js';
|
|
15
|
-
export * from './validateNonEmptyTimes.js';
|
|
16
15
|
export * from './validateOverrideDateOrder.js';
|
|
17
16
|
export * from './validateOverrideWeekdaysMatchDates.js';
|
|
18
17
|
export * from './validateScDateFormats.js';
|
|
19
|
-
export * from './validateTimezone.js';
|
package/dist/internal/index.js
CHANGED
|
@@ -5,17 +5,15 @@ 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';
|
|
12
|
+
export * from './validateNonEmptyTimes.js';
|
|
13
13
|
export * from './validateNoOverlappingOverrides.js';
|
|
14
14
|
export * from './validateNoOverlappingRules.js';
|
|
15
15
|
export * from './validateNoOverlappingTimesInRule.js';
|
|
16
16
|
export * from './validateNoSpilloverConflictsAtOverrideBoundaries.js';
|
|
17
|
-
export * from './validateNonEmptyTimes.js';
|
|
18
17
|
export * from './validateOverrideDateOrder.js';
|
|
19
18
|
export * from './validateOverrideWeekdaysMatchDates.js';
|
|
20
19
|
export * from './validateScDateFormats.js';
|
|
21
|
-
export * from './validateTimezone.js';
|
|
@@ -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({
|
|
@@ -14,10 +14,11 @@ import { doRulesOverlap } from './doRulesOverlap.js';
|
|
|
14
14
|
export const validateNoOverlappingRules = (schedule) => {
|
|
15
15
|
const errors = [];
|
|
16
16
|
// Check weekly rules for overlaps
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
17
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
18
|
+
for (let i = 0; i < weeklyRules.length; i++) {
|
|
19
|
+
for (let j = i + 1; j < weeklyRules.length; j++) {
|
|
20
|
+
const rule1 = weeklyRules[i];
|
|
21
|
+
const rule2 = weeklyRules[j];
|
|
21
22
|
if (rule1 && rule2) {
|
|
22
23
|
const overlapWeekday = doRulesOverlap(rule1, rule2);
|
|
23
24
|
if (overlapWeekday !== undefined) {
|
|
@@ -32,7 +32,8 @@ const validateRuleTimes = (rule, location) => {
|
|
|
32
32
|
export const validateNoOverlappingTimesInRule = (schedule) => {
|
|
33
33
|
const errors = [];
|
|
34
34
|
// Validate weekly rules
|
|
35
|
-
schedule.weekly
|
|
35
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
36
|
+
weeklyRules.forEach((rule, ruleIndex) => {
|
|
36
37
|
const ruleErrors = validateRuleTimes(rule, {
|
|
37
38
|
type: RuleLocationType.Weekly,
|
|
38
39
|
ruleIndex,
|
|
@@ -3,9 +3,11 @@ import type { Schedule, ValidationError } from '../types.js';
|
|
|
3
3
|
* Validates that cross-midnight spillover at override boundaries doesn't
|
|
4
4
|
* create overlapping time ranges. Checks:
|
|
5
5
|
* 1. Spillover into override first day (from weekly or previous override)
|
|
6
|
-
* 2. Override last day spillover into next day (weekly
|
|
6
|
+
* 2. Override last day spillover into next day (weekly, weekly: true, or
|
|
7
|
+
* another override)
|
|
7
8
|
*
|
|
8
9
|
* Note: Spillover into empty/closed days is allowed (adds availability).
|
|
9
|
-
*
|
|
10
|
+
* Spillover that overlaps with explicit time ranges OR into weekly: true
|
|
11
|
+
* days (which are fully available) is flagged as an error.
|
|
10
12
|
*/
|
|
11
13
|
export declare const validateNoSpilloverConflictsAtOverrideBoundaries: (schedule: Schedule) => ValidationError[];
|
|
@@ -7,10 +7,12 @@ import { splitCrossMidnightTimeRange } from './splitCrossMidnightTimeRange.js';
|
|
|
7
7
|
* Validates that cross-midnight spillover at override boundaries doesn't
|
|
8
8
|
* create overlapping time ranges. Checks:
|
|
9
9
|
* 1. Spillover into override first day (from weekly or previous override)
|
|
10
|
-
* 2. Override last day spillover into next day (weekly
|
|
10
|
+
* 2. Override last day spillover into next day (weekly, weekly: true, or
|
|
11
|
+
* another override)
|
|
11
12
|
*
|
|
12
13
|
* Note: Spillover into empty/closed days is allowed (adds availability).
|
|
13
|
-
*
|
|
14
|
+
* Spillover that overlaps with explicit time ranges OR into weekly: true
|
|
15
|
+
* days (which are fully available) is flagged as an error.
|
|
14
16
|
*/
|
|
15
17
|
export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
16
18
|
const errors = [];
|
|
@@ -27,59 +29,63 @@ export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
|
27
29
|
// including indefinite ones)
|
|
28
30
|
// Get rules that apply to previous day (weekly or override)
|
|
29
31
|
const previousDayResult = getApplicableRuleForDate(schedule, previousDate);
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const overrideSplitRanges = splitCrossMidnightTimeRange(overrideTimeRange);
|
|
51
|
-
// Check same-day portion of override time range
|
|
52
|
-
const overrideSameDayRange = overrideSplitRanges[0];
|
|
53
|
-
if (overrideSameDayRange &&
|
|
54
|
-
doTimeRangesOverlap(spilloverRange, overrideSameDayRange)) {
|
|
55
|
-
// Use source information from getApplicableRuleForDate
|
|
56
|
-
if (previousDayResult.source.type === 'override') {
|
|
57
|
-
// Previous day is in an override
|
|
58
|
-
errors.push({
|
|
59
|
-
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
60
|
-
overrideIndex,
|
|
61
|
-
date: firstDate.toJSON(),
|
|
62
|
-
overrideRuleIndex,
|
|
63
|
-
sourceOverrideIndex: previousDayResult.source.overrideIndex,
|
|
64
|
-
sourceOverrideRuleIndex: previousDayRuleIndex,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// Previous day is weekly
|
|
69
|
-
errors.push({
|
|
70
|
-
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
71
|
-
overrideIndex,
|
|
72
|
-
date: firstDate.toJSON(),
|
|
73
|
-
overrideRuleIndex,
|
|
74
|
-
sourceWeeklyRuleIndex: previousDayRuleIndex,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
32
|
+
// If previous day is always available (weekly: true), no cross-midnight
|
|
33
|
+
// rules to check
|
|
34
|
+
if (previousDayResult.rules !== true) {
|
|
35
|
+
// Check each rule from previous day for cross-midnight spillover
|
|
36
|
+
previousDayResult.rules.forEach((previousDayRule, previousDayRuleIndex) => {
|
|
37
|
+
// Skip if previous day rule doesn't include previous weekday
|
|
38
|
+
if (!doesWeekdaysIncludeWeekday(previousDayRule.weekdays, previousWeekday)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Check each time range for cross-midnight
|
|
42
|
+
previousDayRule.times.forEach((timeRange) => {
|
|
43
|
+
const splitRanges = splitCrossMidnightTimeRange(timeRange);
|
|
44
|
+
// If cross-midnight, splitRanges[1] is the spillover portion
|
|
45
|
+
if (splitRanges.length === 2 && splitRanges[1]) {
|
|
46
|
+
const spilloverRange = splitRanges[1];
|
|
47
|
+
// Check if spillover conflicts with override first day's times
|
|
48
|
+
override.rules.forEach((overrideRule, overrideRuleIndex) => {
|
|
49
|
+
// Does first day match override rule's weekdays?
|
|
50
|
+
if (!doesWeekdaysIncludeWeekday(overrideRule.weekdays, firstDateWeekday)) {
|
|
51
|
+
return;
|
|
77
52
|
}
|
|
53
|
+
// Check each time range in override rule
|
|
54
|
+
overrideRule.times.forEach((overrideTimeRange) => {
|
|
55
|
+
const overrideSplitRanges = splitCrossMidnightTimeRange(overrideTimeRange);
|
|
56
|
+
// Check same-day portion of override time range
|
|
57
|
+
const overrideSameDayRange = overrideSplitRanges[0];
|
|
58
|
+
if (overrideSameDayRange &&
|
|
59
|
+
doTimeRangesOverlap(spilloverRange, overrideSameDayRange)) {
|
|
60
|
+
// Use source information from getApplicableRuleForDate
|
|
61
|
+
if (previousDayResult.source === 'override') {
|
|
62
|
+
// Previous day is in an override
|
|
63
|
+
errors.push({
|
|
64
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
65
|
+
overrideIndex,
|
|
66
|
+
date: firstDate.toJSON(),
|
|
67
|
+
overrideRuleIndex,
|
|
68
|
+
sourceOverrideIndex: previousDayResult.overrideIndex,
|
|
69
|
+
sourceOverrideRuleIndex: previousDayRuleIndex,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Previous day is weekly
|
|
74
|
+
errors.push({
|
|
75
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
76
|
+
overrideIndex,
|
|
77
|
+
date: firstDate.toJSON(),
|
|
78
|
+
overrideRuleIndex,
|
|
79
|
+
sourceWeeklyRuleIndex: previousDayRuleIndex,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
78
84
|
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
81
87
|
});
|
|
82
|
-
}
|
|
88
|
+
}
|
|
83
89
|
// PART 2: Check spillover FROM override last day into next day
|
|
84
90
|
// Skip for indefinite overrides (no last day)
|
|
85
91
|
if (!override.to) {
|
|
@@ -103,6 +109,17 @@ export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
|
103
109
|
const spilloverRange = splitRanges[1];
|
|
104
110
|
// Get what rule applies to next day
|
|
105
111
|
const nextDayResult = getApplicableRuleForDate(schedule, nextDate);
|
|
112
|
+
// If next day is always available (weekly: true), spillover
|
|
113
|
+
// creates overlapping ranges with the full-day availability.
|
|
114
|
+
if (nextDayResult.rules === true) {
|
|
115
|
+
errors.push({
|
|
116
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
117
|
+
overrideIndex,
|
|
118
|
+
date: lastDate.toJSON(),
|
|
119
|
+
overrideRuleIndex,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
106
123
|
// Check if spillover conflicts with next day's times
|
|
107
124
|
nextDayResult.rules.forEach((nextDayRule, nextDayRuleIndex) => {
|
|
108
125
|
// Does next day match the rule's weekdays?
|
|
@@ -117,14 +134,14 @@ export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
|
117
134
|
if (nextDaySameDayRange &&
|
|
118
135
|
doTimeRangesOverlap(spilloverRange, nextDaySameDayRange)) {
|
|
119
136
|
// Use source information from getApplicableRuleForDate
|
|
120
|
-
if (nextDayResult.source
|
|
137
|
+
if (nextDayResult.source === 'override') {
|
|
121
138
|
// Next day is in an override
|
|
122
139
|
errors.push({
|
|
123
140
|
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
124
141
|
overrideIndex,
|
|
125
142
|
date: lastDate.toJSON(),
|
|
126
143
|
overrideRuleIndex,
|
|
127
|
-
nextDayOverrideIndex: nextDayResult.
|
|
144
|
+
nextDayOverrideIndex: nextDayResult.overrideIndex,
|
|
128
145
|
nextDayOverrideRuleIndex: nextDayRuleIndex,
|
|
129
146
|
});
|
|
130
147
|
}
|
|
@@ -5,7 +5,8 @@ import { RuleLocationType, ValidationIssue } from '../constants.js';
|
|
|
5
5
|
export const validateNonEmptyTimes = (schedule) => {
|
|
6
6
|
const errors = [];
|
|
7
7
|
// Validate weekly rules
|
|
8
|
-
schedule.weekly
|
|
8
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
9
|
+
weeklyRules.forEach((rule, ruleIndex) => {
|
|
9
10
|
if (rule.times.length === 0) {
|
|
10
11
|
errors.push({
|
|
11
12
|
issue: ValidationIssue.EmptyTimes,
|
|
@@ -86,7 +86,8 @@ const validateRule = (rule, fieldPrefix) => {
|
|
|
86
86
|
export const validateScDateFormats = (schedule) => {
|
|
87
87
|
const errors = [];
|
|
88
88
|
// Validate weekly rules
|
|
89
|
-
schedule.weekly
|
|
89
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
90
|
+
weeklyRules.forEach((rule, ruleIndex) => {
|
|
90
91
|
errors.push(...validateRule(rule, `weekly[${String(ruleIndex)}]`));
|
|
91
92
|
});
|
|
92
93
|
// Validate overrides
|