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
|
@@ -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) {
|
|
@@ -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,15 +29,17 @@ 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
|
-
|
|
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 for cross-midnight
|
|
42
|
+
const splitRanges = splitCrossMidnightTimeRange(previousDayRule);
|
|
39
43
|
// If cross-midnight, splitRanges[1] is the spillover portion
|
|
40
44
|
if (splitRanges.length === 2 && splitRanges[1]) {
|
|
41
45
|
const spilloverRange = splitRanges[1];
|
|
@@ -45,41 +49,38 @@ export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
|
45
49
|
if (!doesWeekdaysIncludeWeekday(overrideRule.weekdays, firstDateWeekday)) {
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
52
|
+
const overrideSplitRanges = splitCrossMidnightTimeRange(overrideRule);
|
|
53
|
+
// Check same-day portion of override time range
|
|
54
|
+
const overrideSameDayRange = overrideSplitRanges[0];
|
|
55
|
+
if (overrideSameDayRange &&
|
|
56
|
+
doTimeRangesOverlap(spilloverRange, overrideSameDayRange)) {
|
|
57
|
+
// Use source information from getApplicableRuleForDate
|
|
58
|
+
if (previousDayResult.source === 'override') {
|
|
59
|
+
// Previous day is in an override
|
|
60
|
+
errors.push({
|
|
61
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
62
|
+
overrideIndex,
|
|
63
|
+
date: firstDate.toJSON(),
|
|
64
|
+
overrideRuleIndex,
|
|
65
|
+
sourceOverrideIndex: previousDayResult.overrideIndex,
|
|
66
|
+
sourceOverrideRuleIndex: previousDayRuleIndex,
|
|
67
|
+
});
|
|
77
68
|
}
|
|
78
|
-
|
|
69
|
+
else {
|
|
70
|
+
// Previous day is weekly
|
|
71
|
+
errors.push({
|
|
72
|
+
issue: ValidationIssue.SpilloverConflictIntoOverrideFirstDay,
|
|
73
|
+
overrideIndex,
|
|
74
|
+
date: firstDate.toJSON(),
|
|
75
|
+
overrideRuleIndex,
|
|
76
|
+
sourceWeeklyRuleIndex: previousDayRuleIndex,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
|
-
}
|
|
83
|
+
}
|
|
83
84
|
// PART 2: Check spillover FROM override last day into next day
|
|
84
85
|
// Skip for indefinite overrides (no last day)
|
|
85
86
|
if (!override.to) {
|
|
@@ -95,54 +96,60 @@ export const validateNoSpilloverConflictsAtOverrideBoundaries = (schedule) => {
|
|
|
95
96
|
if (!doesWeekdaysIncludeWeekday(overrideRule.weekdays, lastDateWeekday)) {
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
// Check
|
|
99
|
-
overrideRule
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
nextDayRule.times.forEach((nextDayTimeRange) => {
|
|
114
|
-
const nextDaySplitRanges = splitCrossMidnightTimeRange(nextDayTimeRange);
|
|
115
|
-
// Check same-day portion of next day's time range
|
|
116
|
-
const nextDaySameDayRange = nextDaySplitRanges[0];
|
|
117
|
-
if (nextDaySameDayRange &&
|
|
118
|
-
doTimeRangesOverlap(spilloverRange, nextDaySameDayRange)) {
|
|
119
|
-
// Use source information from getApplicableRuleForDate
|
|
120
|
-
if (nextDayResult.source.type === 'override') {
|
|
121
|
-
// Next day is in an override
|
|
122
|
-
errors.push({
|
|
123
|
-
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
124
|
-
overrideIndex,
|
|
125
|
-
date: lastDate.toJSON(),
|
|
126
|
-
overrideRuleIndex,
|
|
127
|
-
nextDayOverrideIndex: nextDayResult.source.overrideIndex,
|
|
128
|
-
nextDayOverrideRuleIndex: nextDayRuleIndex,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
// Next day is weekly
|
|
133
|
-
errors.push({
|
|
134
|
-
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
135
|
-
overrideIndex,
|
|
136
|
-
date: lastDate.toJSON(),
|
|
137
|
-
overrideRuleIndex,
|
|
138
|
-
nextDayWeeklyRuleIndex: nextDayRuleIndex,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
99
|
+
// Check for cross-midnight
|
|
100
|
+
const splitRanges = splitCrossMidnightTimeRange(overrideRule);
|
|
101
|
+
// If cross-midnight, splitRanges[1] is the spillover portion
|
|
102
|
+
if (splitRanges.length === 2 && splitRanges[1]) {
|
|
103
|
+
const spilloverRange = splitRanges[1];
|
|
104
|
+
// Get what rule applies to next day
|
|
105
|
+
const nextDayResult = getApplicableRuleForDate(schedule, nextDate);
|
|
106
|
+
// If next day is always available (weekly: true), spillover
|
|
107
|
+
// creates overlapping ranges with the full-day availability.
|
|
108
|
+
if (nextDayResult.rules === true) {
|
|
109
|
+
errors.push({
|
|
110
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
111
|
+
overrideIndex,
|
|
112
|
+
date: lastDate.toJSON(),
|
|
113
|
+
overrideRuleIndex,
|
|
143
114
|
});
|
|
115
|
+
return;
|
|
144
116
|
}
|
|
145
|
-
|
|
117
|
+
// Check if spillover conflicts with next day's times
|
|
118
|
+
nextDayResult.rules.forEach((nextDayRule, nextDayRuleIndex) => {
|
|
119
|
+
// Does next day match the rule's weekdays?
|
|
120
|
+
if (!doesWeekdaysIncludeWeekday(nextDayRule.weekdays, nextDateWeekday)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const nextDaySplitRanges = splitCrossMidnightTimeRange(nextDayRule);
|
|
124
|
+
// Check same-day portion of next day's time range
|
|
125
|
+
const nextDaySameDayRange = nextDaySplitRanges[0];
|
|
126
|
+
if (nextDaySameDayRange &&
|
|
127
|
+
doTimeRangesOverlap(spilloverRange, nextDaySameDayRange)) {
|
|
128
|
+
// Use source information from getApplicableRuleForDate
|
|
129
|
+
if (nextDayResult.source === 'override') {
|
|
130
|
+
// Next day is in an override
|
|
131
|
+
errors.push({
|
|
132
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
133
|
+
overrideIndex,
|
|
134
|
+
date: lastDate.toJSON(),
|
|
135
|
+
overrideRuleIndex,
|
|
136
|
+
nextDayOverrideIndex: nextDayResult.overrideIndex,
|
|
137
|
+
nextDayOverrideRuleIndex: nextDayRuleIndex,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Next day is weekly
|
|
142
|
+
errors.push({
|
|
143
|
+
issue: ValidationIssue.SpilloverConflictOverrideIntoNext,
|
|
144
|
+
overrideIndex,
|
|
145
|
+
date: lastDate.toJSON(),
|
|
146
|
+
overrideRuleIndex,
|
|
147
|
+
nextDayWeeklyRuleIndex: nextDayRuleIndex,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
146
153
|
});
|
|
147
154
|
});
|
|
148
155
|
return errors;
|
|
@@ -67,7 +67,7 @@ const validateTimeRange = (timeRange, fieldPrefix) => {
|
|
|
67
67
|
return errors;
|
|
68
68
|
};
|
|
69
69
|
/**
|
|
70
|
-
* Validates a rule (weekdays and
|
|
70
|
+
* Validates a rule (weekdays and time range).
|
|
71
71
|
*/
|
|
72
72
|
const validateRule = (rule, fieldPrefix) => {
|
|
73
73
|
const errors = [];
|
|
@@ -75,9 +75,7 @@ const validateRule = (rule, fieldPrefix) => {
|
|
|
75
75
|
if (weekdaysError) {
|
|
76
76
|
errors.push(weekdaysError);
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
errors.push(...validateTimeRange(timeRange, `${fieldPrefix}.times[${String(timeIndex)}]`));
|
|
80
|
-
});
|
|
78
|
+
errors.push(...validateTimeRange(rule, fieldPrefix));
|
|
81
79
|
return errors;
|
|
82
80
|
};
|
|
83
81
|
/**
|
|
@@ -86,7 +84,8 @@ const validateRule = (rule, fieldPrefix) => {
|
|
|
86
84
|
export const validateScDateFormats = (schedule) => {
|
|
87
85
|
const errors = [];
|
|
88
86
|
// Validate weekly rules
|
|
89
|
-
schedule.weekly
|
|
87
|
+
const weeklyRules = schedule.weekly === true ? [] : schedule.weekly;
|
|
88
|
+
weeklyRules.forEach((rule, ruleIndex) => {
|
|
90
89
|
errors.push(...validateRule(rule, `weekly[${String(ruleIndex)}]`));
|
|
91
90
|
});
|
|
92
91
|
// Validate overrides
|
|
@@ -2,5 +2,14 @@ import type { STimestamp } from 'scdate';
|
|
|
2
2
|
import type { Schedule } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Checks if a schedule is available at the specified timestamp.
|
|
5
|
+
*
|
|
6
|
+
* When `weekly` is `true`, the schedule is always available (unless an
|
|
7
|
+
* override applies). Otherwise, checks whether the timestamp falls within any
|
|
8
|
+
* matching time range for the day, including cross-midnight spillover from the
|
|
9
|
+
* previous day.
|
|
10
|
+
*
|
|
11
|
+
* @param schedule The schedule to check availability against.
|
|
12
|
+
* @param timestamp The timestamp to check.
|
|
13
|
+
* @returns True if the schedule is available at the given timestamp.
|
|
5
14
|
*/
|
|
6
15
|
export declare const isScheduleAvailable: (schedule: Schedule, timestamp: STimestamp | string) => boolean;
|
|
@@ -3,6 +3,15 @@ import { getApplicableRuleForDate } from './internal/getApplicableRuleForDate.js
|
|
|
3
3
|
import { isTimeInTimeRange } from './internal/isTimeInTimeRange.js';
|
|
4
4
|
/**
|
|
5
5
|
* Checks if a schedule is available at the specified timestamp.
|
|
6
|
+
*
|
|
7
|
+
* When `weekly` is `true`, the schedule is always available (unless an
|
|
8
|
+
* override applies). Otherwise, checks whether the timestamp falls within any
|
|
9
|
+
* matching time range for the day, including cross-midnight spillover from the
|
|
10
|
+
* previous day.
|
|
11
|
+
*
|
|
12
|
+
* @param schedule The schedule to check availability against.
|
|
13
|
+
* @param timestamp The timestamp to check.
|
|
14
|
+
* @returns True if the schedule is available at the given timestamp.
|
|
6
15
|
*/
|
|
7
16
|
export const isScheduleAvailable = (schedule, timestamp) => {
|
|
8
17
|
const date = getDateFromTimestamp(timestamp);
|
|
@@ -10,14 +19,18 @@ export const isScheduleAvailable = (schedule, timestamp) => {
|
|
|
10
19
|
const weekday = getWeekdayFromDate(date);
|
|
11
20
|
// Get the applicable rules for this date
|
|
12
21
|
const { rules } = getApplicableRuleForDate(schedule, date.date);
|
|
22
|
+
// If weekly is true, always available (unless overridden)
|
|
23
|
+
if (rules === true) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
13
26
|
// Check if any rule's time ranges include this timestamp (same-day check)
|
|
14
27
|
const matchesSameDay = rules.some((rule) => {
|
|
15
28
|
// Check if this weekday is in the rule
|
|
16
29
|
if (!doesWeekdaysIncludeWeekday(rule.weekdays, weekday)) {
|
|
17
30
|
return false;
|
|
18
31
|
}
|
|
19
|
-
// Check if
|
|
20
|
-
return
|
|
32
|
+
// Check if time range matches
|
|
33
|
+
return isTimeInTimeRange(time, rule, true);
|
|
21
34
|
});
|
|
22
35
|
if (matchesSameDay) {
|
|
23
36
|
return true;
|
|
@@ -25,13 +38,17 @@ export const isScheduleAvailable = (schedule, timestamp) => {
|
|
|
25
38
|
// Also check previous day's rules for cross-midnight spillover
|
|
26
39
|
const previousDate = addDaysToDate(date, -1);
|
|
27
40
|
const previousWeekday = getWeekdayFromDate(previousDate);
|
|
28
|
-
const
|
|
29
|
-
|
|
41
|
+
const previousResult = getApplicableRuleForDate(schedule, previousDate.date);
|
|
42
|
+
// If previous day was always available, no cross-midnight rules to check
|
|
43
|
+
if (previousResult.rules === true) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return previousResult.rules.some((rule) => {
|
|
30
47
|
// Check if previous day's weekday is in the rule
|
|
31
48
|
if (!doesWeekdaysIncludeWeekday(rule.weekdays, previousWeekday)) {
|
|
32
49
|
return false;
|
|
33
50
|
}
|
|
34
|
-
// Check if
|
|
35
|
-
return
|
|
51
|
+
// Check if time range matches (next-day portion)
|
|
52
|
+
return isTimeInTimeRange(time, rule, false);
|
|
36
53
|
});
|
|
37
54
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -16,29 +16,18 @@ export type STimestampString = string;
|
|
|
16
16
|
* String in SMTWTFS format representing weekdays.
|
|
17
17
|
*/
|
|
18
18
|
export type SWeekdaysString = string;
|
|
19
|
-
/**
|
|
20
|
-
* Represents a time range within a day. Time ranges can cross midnight. For
|
|
21
|
-
* example, a range from 20:00 to 02:00 represents 8:00 PM to 2:00 AM the next
|
|
22
|
-
* day.
|
|
23
|
-
*/
|
|
24
|
-
export interface TimeRange {
|
|
25
|
-
/** Start time of the range (inclusive) */
|
|
26
|
-
from: STime | STimeString;
|
|
27
|
-
/** End time of the range (inclusive) */
|
|
28
|
-
to: STime | STimeString;
|
|
29
|
-
}
|
|
30
19
|
/**
|
|
31
20
|
* Defines a recurring weekly availability pattern. Specifies which days of the
|
|
32
|
-
* week are available and what time
|
|
21
|
+
* week are available and what time range applies on those days. For split
|
|
22
|
+
* shifts, use multiple rules with the same weekdays.
|
|
33
23
|
*/
|
|
34
24
|
export interface WeeklyScheduleRule {
|
|
35
25
|
/** Days of the week this rule applies to */
|
|
36
26
|
weekdays: SWeekdays | SWeekdaysString;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
times: TimeRange[];
|
|
27
|
+
/** Start time of the range (inclusive). Ranges can cross midnight. */
|
|
28
|
+
from: STime | STimeString;
|
|
29
|
+
/** End time of the range (inclusive). Ranges can cross midnight. */
|
|
30
|
+
to: STime | STimeString;
|
|
42
31
|
}
|
|
43
32
|
/**
|
|
44
33
|
* Defines a date-specific override to the weekly schedule. Overrides apply to
|
|
@@ -60,20 +49,19 @@ export interface OverrideScheduleRule {
|
|
|
60
49
|
rules: WeeklyScheduleRule[];
|
|
61
50
|
}
|
|
62
51
|
/**
|
|
63
|
-
* Represents a complete availability schedule. A schedule consists of
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* Indefinite override (with only from date) 3. Weekly schedule
|
|
52
|
+
* Represents a complete availability schedule. A schedule consists of base
|
|
53
|
+
* weekly recurring patterns and optional date-specific overrides. Priority
|
|
54
|
+
* order for determining availability: 1. Specific override (with both from and
|
|
55
|
+
* to dates) 2. Indefinite override (with only from date) 3. Weekly schedule
|
|
68
56
|
*/
|
|
69
57
|
export interface Schedule {
|
|
70
58
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
59
|
+
* Base recurring weekly schedule patterns.
|
|
60
|
+
* - `true`: available 24/7 (overrides can close windows)
|
|
61
|
+
* - `WeeklyScheduleRule[]`: available during defined time ranges
|
|
62
|
+
* - `[]`: never available (overrides can open windows)
|
|
73
63
|
*/
|
|
74
|
-
|
|
75
|
-
/** Base recurring weekly schedule patterns */
|
|
76
|
-
weekly: WeeklyScheduleRule[];
|
|
64
|
+
weekly: WeeklyScheduleRule[] | true;
|
|
77
65
|
/**
|
|
78
66
|
* Date-specific exceptions to the weekly schedule. Overrides take precedence
|
|
79
67
|
* over weekly rules.
|
|
@@ -95,11 +83,6 @@ export interface AvailabilityRange {
|
|
|
95
83
|
* property to narrow the type and access error-specific fields.
|
|
96
84
|
*/
|
|
97
85
|
export type ValidationError = {
|
|
98
|
-
/** The timezone string is not a valid IANA timezone identifier */
|
|
99
|
-
issue: ValidationIssue.InvalidTimezone;
|
|
100
|
-
/** The invalid timezone string that was provided */
|
|
101
|
-
timezone: string;
|
|
102
|
-
} | {
|
|
103
86
|
/** Two or more specific overrides have identical date ranges */
|
|
104
87
|
issue: ValidationIssue.DuplicateOverrides;
|
|
105
88
|
/** Indexes of the two duplicate overrides */
|
|
@@ -109,35 +92,6 @@ export type ValidationError = {
|
|
|
109
92
|
issue: ValidationIssue.OverlappingSpecificOverrides;
|
|
110
93
|
/** Indexes of the two overlapping overrides */
|
|
111
94
|
overrideIndexes: [number, number];
|
|
112
|
-
} | {
|
|
113
|
-
/** Time ranges within a single rule overlap with each other */
|
|
114
|
-
issue: ValidationIssue.OverlappingTimesInRule;
|
|
115
|
-
/** Location of the rule containing overlapping times */
|
|
116
|
-
location: {
|
|
117
|
-
type: RuleLocationType.Weekly;
|
|
118
|
-
ruleIndex: number;
|
|
119
|
-
} | {
|
|
120
|
-
type: RuleLocationType.Override;
|
|
121
|
-
overrideIndex: number;
|
|
122
|
-
ruleIndex: number;
|
|
123
|
-
};
|
|
124
|
-
/** Indexes of the two overlapping time ranges within the rule */
|
|
125
|
-
timeRangeIndexes: [number, number];
|
|
126
|
-
} | {
|
|
127
|
-
/**
|
|
128
|
-
* A rule has an empty times array
|
|
129
|
-
* (should have at least one time range or be removed)
|
|
130
|
-
*/
|
|
131
|
-
issue: ValidationIssue.EmptyTimes;
|
|
132
|
-
/** Location of the rule with empty times */
|
|
133
|
-
location: {
|
|
134
|
-
type: RuleLocationType.Weekly;
|
|
135
|
-
ruleIndex: number;
|
|
136
|
-
} | {
|
|
137
|
-
type: RuleLocationType.Override;
|
|
138
|
-
overrideIndex: number;
|
|
139
|
-
ruleIndex: number;
|
|
140
|
-
};
|
|
141
95
|
} | {
|
|
142
96
|
/**
|
|
143
97
|
* A field contains an invalid scdate format
|
|
@@ -225,7 +179,7 @@ export type ValidationError = {
|
|
|
225
179
|
} | {
|
|
226
180
|
/**
|
|
227
181
|
* Cross-midnight spillover from override's last day conflicts with next
|
|
228
|
-
* day's time ranges
|
|
182
|
+
* day's time ranges or weekly: true availability
|
|
229
183
|
*/
|
|
230
184
|
issue: ValidationIssue.SpilloverConflictOverrideIntoNext;
|
|
231
185
|
/** Index of the override whose last day causes spillover */
|
|
@@ -235,8 +189,8 @@ export type ValidationError = {
|
|
|
235
189
|
/** The override rule index causing the spillover */
|
|
236
190
|
overrideRuleIndex: number;
|
|
237
191
|
/**
|
|
238
|
-
* The next day's rule that conflicts
|
|
239
|
-
*
|
|
192
|
+
* The next day's rule that conflicts. When all three fields are
|
|
193
|
+
* undefined, the next day is weekly: true (fully available).
|
|
240
194
|
*/
|
|
241
195
|
nextDayWeeklyRuleIndex?: number;
|
|
242
196
|
nextDayOverrideIndex?: number;
|
|
@@ -3,13 +3,15 @@ import type { Schedule, ValidationResult } from './types.js';
|
|
|
3
3
|
* Validates a schedule configuration and returns all validation errors found.
|
|
4
4
|
*
|
|
5
5
|
* Validation is performed in two phases:
|
|
6
|
-
* 1. Structural validation (
|
|
7
|
-
*
|
|
6
|
+
* 1. Structural validation (formats, date order, empty weekdays,
|
|
7
|
+
* weekday-date mismatch) - runs on original schedule
|
|
8
8
|
* 2. Semantic validation (overlaps, conflicts) - runs on normalized schedule
|
|
9
9
|
* after filtering weekdays to actual dates
|
|
10
10
|
*
|
|
11
11
|
* If structural errors are found, validation stops early and returns only
|
|
12
12
|
* those errors. This provides better user experience and avoids crashes from
|
|
13
13
|
* invalid data during normalization.
|
|
14
|
+
*
|
|
15
|
+
* @param schedule The schedule to validate.
|
|
14
16
|
*/
|
|
15
17
|
export declare const validateSchedule: (schedule: Schedule) => ValidationResult;
|
package/dist/validateSchedule.js
CHANGED
|
@@ -2,36 +2,33 @@ import { normalizeScheduleForValidation } from './internal/normalizeScheduleForV
|
|
|
2
2
|
import { validateNoEmptyWeekdays } from './internal/validateNoEmptyWeekdays.js';
|
|
3
3
|
import { validateNoOverlappingOverrides } from './internal/validateNoOverlappingOverrides.js';
|
|
4
4
|
import { validateNoOverlappingRules } from './internal/validateNoOverlappingRules.js';
|
|
5
|
-
import { validateNoOverlappingTimesInRule } from './internal/validateNoOverlappingTimesInRule.js';
|
|
6
5
|
import { validateNoSpilloverConflictsAtOverrideBoundaries } from './internal/validateNoSpilloverConflictsAtOverrideBoundaries.js';
|
|
7
|
-
import { validateNonEmptyTimes } from './internal/validateNonEmptyTimes.js';
|
|
8
6
|
import { validateOverrideDateOrder } from './internal/validateOverrideDateOrder.js';
|
|
9
7
|
import { validateOverrideWeekdaysMatchDates } from './internal/validateOverrideWeekdaysMatchDates.js';
|
|
10
8
|
import { validateScDateFormats } from './internal/validateScDateFormats.js';
|
|
11
|
-
import { validateTimezone } from './internal/validateTimezone.js';
|
|
12
9
|
/**
|
|
13
10
|
* Validates a schedule configuration and returns all validation errors found.
|
|
14
11
|
*
|
|
15
12
|
* Validation is performed in two phases:
|
|
16
|
-
* 1. Structural validation (
|
|
17
|
-
*
|
|
13
|
+
* 1. Structural validation (formats, date order, empty weekdays,
|
|
14
|
+
* weekday-date mismatch) - runs on original schedule
|
|
18
15
|
* 2. Semantic validation (overlaps, conflicts) - runs on normalized schedule
|
|
19
16
|
* after filtering weekdays to actual dates
|
|
20
17
|
*
|
|
21
18
|
* If structural errors are found, validation stops early and returns only
|
|
22
19
|
* those errors. This provides better user experience and avoids crashes from
|
|
23
20
|
* invalid data during normalization.
|
|
21
|
+
*
|
|
22
|
+
* @param schedule The schedule to validate.
|
|
24
23
|
*/
|
|
25
24
|
export const validateSchedule = (schedule) => {
|
|
26
25
|
// Phase 1: Structural validation
|
|
27
26
|
// Note: Order matters - date formats and order must be validated before
|
|
28
27
|
// weekday matching (which calls filterWeekdaysForDates)
|
|
29
28
|
const structuralErrors = [
|
|
30
|
-
...validateTimezone(schedule),
|
|
31
29
|
...validateScDateFormats(schedule),
|
|
32
30
|
...validateOverrideDateOrder(schedule),
|
|
33
31
|
...validateNoEmptyWeekdays(schedule),
|
|
34
|
-
...validateNonEmptyTimes(schedule),
|
|
35
32
|
...validateOverrideWeekdaysMatchDates(schedule),
|
|
36
33
|
];
|
|
37
34
|
// Return immediately if structural errors exist
|
|
@@ -46,7 +43,6 @@ export const validateSchedule = (schedule) => {
|
|
|
46
43
|
// Phase 3: Semantic validation on normalized schedule
|
|
47
44
|
const semanticErrors = [
|
|
48
45
|
...validateNoOverlappingOverrides(normalizedSchedule),
|
|
49
|
-
...validateNoOverlappingTimesInRule(normalizedSchedule),
|
|
50
46
|
...validateNoOverlappingRules(normalizedSchedule),
|
|
51
47
|
...validateNoSpilloverConflictsAtOverrideBoundaries(normalizedSchedule),
|
|
52
48
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scschedule",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "vitest run"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"scdate": "
|
|
23
|
+
"scdate": "3.1.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@eslint/js": "^9.39.2",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if a string is a valid IANA timezone identifier.
|
|
3
|
-
*/
|
|
4
|
-
export const isValidTimezone = (timezone) => {
|
|
5
|
-
try {
|
|
6
|
-
const validTimezones = Intl.supportedValuesOf('timeZone');
|
|
7
|
-
return validTimezones.includes(timezone);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
};
|