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.
Files changed (44) hide show
  1. package/README.md +161 -74
  2. package/dist/cleanupExpiredOverridesFromSchedule.d.ts +4 -0
  3. package/dist/cleanupExpiredOverridesFromSchedule.js +4 -0
  4. package/dist/constants.d.ts +1 -10
  5. package/dist/constants.js +1 -10
  6. package/dist/getAvailableRangesFromSchedule.d.ts +10 -0
  7. package/dist/getAvailableRangesFromSchedule.js +29 -13
  8. package/dist/getNextAvailableFromSchedule.d.ts +9 -8
  9. package/dist/getNextAvailableFromSchedule.js +21 -17
  10. package/dist/getNextUnavailableFromSchedule.d.ts +19 -9
  11. package/dist/getNextUnavailableFromSchedule.js +120 -63
  12. package/dist/index.d.ts +3 -2
  13. package/dist/index.js +4 -2
  14. package/dist/internal/doTimeRangesOverlap.d.ts +1 -1
  15. package/dist/internal/getApplicableRuleForDate.d.ts +11 -11
  16. package/dist/internal/getApplicableRuleForDate.js +11 -7
  17. package/dist/internal/getEffectiveTimesForWeekday.d.ts +2 -1
  18. package/dist/internal/getEffectiveTimesForWeekday.js +9 -14
  19. package/dist/internal/index.d.ts +1 -4
  20. package/dist/internal/index.js +0 -4
  21. package/dist/internal/isTimeInTimeRange.d.ts +2 -1
  22. package/dist/internal/splitCrossMidnightTimeRange.d.ts +1 -1
  23. package/dist/internal/splitCrossMidnightTimeRange.js +3 -2
  24. package/dist/internal/types.d.ts +13 -0
  25. package/dist/internal/types.js +1 -0
  26. package/dist/internal/validateNoEmptyWeekdays.js +2 -1
  27. package/dist/internal/validateNoOverlappingRules.js +5 -4
  28. package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.d.ts +4 -2
  29. package/dist/internal/validateNoSpilloverConflictsAtOverrideBoundaries.js +95 -88
  30. package/dist/internal/validateScDateFormats.js +4 -5
  31. package/dist/isScheduleAvailable.d.ts +9 -0
  32. package/dist/isScheduleAvailable.js +23 -6
  33. package/dist/types.d.ts +18 -64
  34. package/dist/validateSchedule.d.ts +4 -2
  35. package/dist/validateSchedule.js +4 -8
  36. package/package.json +2 -2
  37. package/dist/internal/isValidTimezone.d.ts +0 -4
  38. package/dist/internal/isValidTimezone.js +0 -12
  39. package/dist/internal/validateNoOverlappingTimesInRule.d.ts +0 -5
  40. package/dist/internal/validateNoOverlappingTimesInRule.js +0 -54
  41. package/dist/internal/validateNonEmptyTimes.d.ts +0 -5
  42. package/dist/internal/validateNonEmptyTimes.js +0 -35
  43. package/dist/internal/validateTimezone.d.ts +0 -5
  44. 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
- for (let i = 0; i < schedule.weekly.length; i++) {
18
- for (let j = i + 1; j < schedule.weekly.length; j++) {
19
- const rule1 = schedule.weekly[i];
20
- const rule2 = schedule.weekly[j];
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 or another override)
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
- * Only spillover that OVERLAPS with explicit time ranges is flagged as error.
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 or another override)
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
- * Only spillover that OVERLAPS with explicit time ranges is flagged as error.
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
- // Check each rule from previous day for cross-midnight spillover
31
- previousDayResult.rules.forEach((previousDayRule, previousDayRuleIndex) => {
32
- // Skip if previous day rule doesn't include previous weekday
33
- if (!doesWeekdaysIncludeWeekday(previousDayRule.weekdays, previousWeekday)) {
34
- return;
35
- }
36
- // Check each time range for cross-midnight
37
- previousDayRule.times.forEach((timeRange) => {
38
- const splitRanges = splitCrossMidnightTimeRange(timeRange);
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
- // Check each time range in override rule
49
- overrideRule.times.forEach((overrideTimeRange) => {
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
- }
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 each time range for cross-midnight
99
- overrideRule.times.forEach((timeRange) => {
100
- const splitRanges = splitCrossMidnightTimeRange(timeRange);
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
- // Check if spillover conflicts with next day's times
107
- nextDayResult.rules.forEach((nextDayRule, nextDayRuleIndex) => {
108
- // Does next day match the rule's weekdays?
109
- if (!doesWeekdaysIncludeWeekday(nextDayRule.weekdays, nextDateWeekday)) {
110
- return;
111
- }
112
- // Check each time range in next day's rule
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 all time ranges).
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
- rule.times.forEach((timeRange, timeIndex) => {
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.forEach((rule, ruleIndex) => {
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 any time range matches
20
- return rule.times.some((timeRange) => isTimeInTimeRange(time, timeRange, true));
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 { rules: previousRules } = getApplicableRuleForDate(schedule, previousDate.date);
29
- return previousRules.some((rule) => {
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 any time range matches (next-day portion)
35
- return rule.times.some((timeRange) => isTimeInTimeRange(time, timeRange, false));
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 ranges apply on those days.
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
- * Time ranges when available on the specified weekdays. Empty array means
39
- * unavailable on these days.
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 a
64
- * timezone for all date/time operations, base weekly recurring patterns, and
65
- * optional date-specific overrides. Priority order for determining
66
- * availability: 1. Specific override (with both from and to dates) 2.
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
- * IANA timezone identifier (e.g., 'America/New_York', 'Europe/London'). Must
72
- * be in `Intl.supportedValuesOf('timeZone')`.
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
- timezone: string;
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 (weekly or another override). If
239
- * nextDayOverrideIndex is undefined, it's a weekly rule.
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 (timezone, formats, date order, empty weekdays,
7
- * non-empty times, weekday-date mismatch) - runs on original schedule
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;
@@ -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 (timezone, formats, date order, empty weekdays,
17
- * non-empty times, weekday-date mismatch) - runs on original schedule
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": "2.1.1",
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": "2.1.1"
23
+ "scdate": "3.1.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@eslint/js": "^9.39.2",
@@ -1,4 +0,0 @@
1
- /**
2
- * Checks if a string is a valid IANA timezone identifier.
3
- */
4
- export declare const isValidTimezone: (timezone: string) => boolean;
@@ -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
- };
@@ -1,5 +0,0 @@
1
- import type { Schedule, ValidationError } from '../types.js';
2
- /**
3
- * Validates that time ranges within rules do not overlap with each other.
4
- */
5
- export declare const validateNoOverlappingTimesInRule: (schedule: Schedule) => ValidationError[];