dabke 0.78.2 → 0.80.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 (129) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +68 -31
  3. package/dist/client.types.d.ts +58 -0
  4. package/dist/client.types.d.ts.map +1 -1
  5. package/dist/client.types.js.map +1 -1
  6. package/dist/cpsat/model-builder.d.ts +13 -2
  7. package/dist/cpsat/model-builder.d.ts.map +1 -1
  8. package/dist/cpsat/model-builder.js.map +1 -1
  9. package/dist/cpsat/response.d.ts +12 -3
  10. package/dist/cpsat/response.d.ts.map +1 -1
  11. package/dist/cpsat/response.js.map +1 -1
  12. package/dist/cpsat/rules/assign-together.d.ts +7 -0
  13. package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
  14. package/dist/cpsat/rules/assign-together.js +1 -0
  15. package/dist/cpsat/rules/assign-together.js.map +1 -1
  16. package/dist/cpsat/rules/employee-assignment-priority.d.ts +11 -37
  17. package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +1 -1
  18. package/dist/cpsat/rules/employee-assignment-priority.js +12 -104
  19. package/dist/cpsat/rules/employee-assignment-priority.js.map +1 -1
  20. package/dist/cpsat/rules/location-preference.d.ts +12 -10
  21. package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
  22. package/dist/cpsat/rules/location-preference.js +16 -14
  23. package/dist/cpsat/rules/location-preference.js.map +1 -1
  24. package/dist/cpsat/rules/max-consecutive-days.d.ts +12 -13
  25. package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
  26. package/dist/cpsat/rules/max-consecutive-days.js +11 -12
  27. package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
  28. package/dist/cpsat/rules/max-hours-day.d.ts +12 -28
  29. package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
  30. package/dist/cpsat/rules/max-hours-day.js +12 -95
  31. package/dist/cpsat/rules/max-hours-day.js.map +1 -1
  32. package/dist/cpsat/rules/max-hours-week.d.ts +14 -34
  33. package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
  34. package/dist/cpsat/rules/max-hours-week.js +12 -103
  35. package/dist/cpsat/rules/max-hours-week.js.map +1 -1
  36. package/dist/cpsat/rules/max-shifts-day.d.ts +14 -39
  37. package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
  38. package/dist/cpsat/rules/max-shifts-day.js +14 -106
  39. package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
  40. package/dist/cpsat/rules/min-consecutive-days.d.ts +12 -13
  41. package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
  42. package/dist/cpsat/rules/min-consecutive-days.js +11 -12
  43. package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
  44. package/dist/cpsat/rules/min-hours-day.d.ts +12 -13
  45. package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
  46. package/dist/cpsat/rules/min-hours-day.js +11 -12
  47. package/dist/cpsat/rules/min-hours-day.js.map +1 -1
  48. package/dist/cpsat/rules/min-hours-week.d.ts +13 -13
  49. package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
  50. package/dist/cpsat/rules/min-hours-week.js +10 -14
  51. package/dist/cpsat/rules/min-hours-week.js.map +1 -1
  52. package/dist/cpsat/rules/min-rest-between-shifts.d.ts +13 -14
  53. package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
  54. package/dist/cpsat/rules/min-rest-between-shifts.js +11 -12
  55. package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
  56. package/dist/cpsat/rules/resolver.d.ts +2 -2
  57. package/dist/cpsat/rules/resolver.d.ts.map +1 -1
  58. package/dist/cpsat/rules/resolver.js +55 -30
  59. package/dist/cpsat/rules/resolver.js.map +1 -1
  60. package/dist/cpsat/rules/scope.types.d.ts +267 -0
  61. package/dist/cpsat/rules/scope.types.d.ts.map +1 -0
  62. package/dist/cpsat/rules/scope.types.js +325 -0
  63. package/dist/cpsat/rules/scope.types.js.map +1 -0
  64. package/dist/cpsat/rules/time-off.d.ts +21 -25
  65. package/dist/cpsat/rules/time-off.d.ts.map +1 -1
  66. package/dist/cpsat/rules/time-off.js +20 -110
  67. package/dist/cpsat/rules/time-off.js.map +1 -1
  68. package/dist/cpsat/semantic-time.d.ts +2 -0
  69. package/dist/cpsat/semantic-time.d.ts.map +1 -1
  70. package/dist/cpsat/semantic-time.js +2 -4
  71. package/dist/cpsat/semantic-time.js.map +1 -1
  72. package/dist/cpsat/types.d.ts +22 -6
  73. package/dist/cpsat/types.d.ts.map +1 -1
  74. package/dist/cpsat/utils.d.ts +1 -1
  75. package/dist/cpsat/utils.js +1 -1
  76. package/dist/cpsat/validation-reporter.js +1 -1
  77. package/dist/cpsat/validation-reporter.js.map +1 -1
  78. package/dist/datetime.utils.d.ts +14 -14
  79. package/dist/datetime.utils.d.ts.map +1 -1
  80. package/dist/datetime.utils.js +26 -27
  81. package/dist/datetime.utils.js.map +1 -1
  82. package/dist/index.d.ts +4 -3
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/llms.d.ts +1 -1
  87. package/dist/llms.d.ts.map +1 -1
  88. package/dist/llms.js +1 -1
  89. package/dist/llms.js.map +1 -1
  90. package/dist/testing/index.d.ts +1 -1
  91. package/dist/testing/index.js +1 -1
  92. package/dist/testing/solver-container.js +3 -1
  93. package/dist/testing/solver-container.js.map +1 -1
  94. package/dist/types.d.ts +18 -20
  95. package/dist/types.d.ts.map +1 -1
  96. package/llms.txt +516 -263
  97. package/package.json +25 -25
  98. package/src/client.types.ts +58 -0
  99. package/src/cpsat/model-builder.ts +19 -7
  100. package/src/cpsat/response.ts +12 -3
  101. package/src/cpsat/rules/assign-together.ts +7 -0
  102. package/src/cpsat/rules/employee-assignment-priority.ts +28 -128
  103. package/src/cpsat/rules/location-preference.ts +24 -17
  104. package/src/cpsat/rules/max-consecutive-days.ts +19 -15
  105. package/src/cpsat/rules/max-hours-day.ts +29 -119
  106. package/src/cpsat/rules/max-hours-week.ts +42 -135
  107. package/src/cpsat/rules/max-shifts-day.ts +31 -130
  108. package/src/cpsat/rules/min-consecutive-days.ts +19 -15
  109. package/src/cpsat/rules/min-hours-day.ts +19 -15
  110. package/src/cpsat/rules/min-hours-week.ts +28 -26
  111. package/src/cpsat/rules/min-rest-between-shifts.ts +21 -17
  112. package/src/cpsat/rules/resolver.ts +66 -45
  113. package/src/cpsat/rules/scope.types.ts +534 -0
  114. package/src/cpsat/rules/time-off.ts +48 -145
  115. package/src/cpsat/semantic-time.ts +10 -8
  116. package/src/cpsat/types.ts +22 -6
  117. package/src/cpsat/utils.ts +1 -1
  118. package/src/cpsat/validation-reporter.ts +1 -1
  119. package/src/datetime.utils.ts +27 -29
  120. package/src/index.ts +11 -7
  121. package/src/llms.ts +1 -1
  122. package/src/testing/index.ts +1 -1
  123. package/src/testing/solver-container.ts +3 -3
  124. package/src/types.ts +27 -31
  125. package/dist/cpsat/rules/scoping.d.ts +0 -129
  126. package/dist/cpsat/rules/scoping.d.ts.map +0 -1
  127. package/dist/cpsat/rules/scoping.js +0 -190
  128. package/dist/cpsat/rules/scoping.js.map +0 -1
  129. package/src/cpsat/rules/scoping.ts +0 -340
@@ -1,12 +1,18 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilationRule } from "../model-builder.js";
3
- import type { SchedulingEmployee } from "../types.js";
4
3
  import type { Term } from "../types.js";
5
- import { parseDayString, priorityToPenalty } from "../utils.js";
6
- import { normalizeScope, withScopes, type RuleScope } from "./scoping.js";
7
-
8
- const MaxHoursDaySchema = withScopes(
9
- z.object({
4
+ import { priorityToPenalty } from "../utils.js";
5
+ import {
6
+ entityScope,
7
+ timeScope,
8
+ parseEntityScope,
9
+ parseTimeScope,
10
+ resolveEmployeesFromScope,
11
+ resolveActiveDaysFromScope,
12
+ } from "./scope.types.js";
13
+
14
+ const MaxHoursDaySchema = z
15
+ .object({
10
16
  hours: z.number().min(0),
11
17
  priority: z.union([
12
18
  z.literal("LOW"),
@@ -14,21 +20,25 @@ const MaxHoursDaySchema = withScopes(
14
20
  z.literal("HIGH"),
15
21
  z.literal("MANDATORY"),
16
22
  ]),
17
- }),
18
- {
19
- entities: ["employees", "roles", "skills"],
20
- times: ["dateRange", "specificDates", "dayOfWeek", "recurring"],
21
- },
22
- );
23
+ })
24
+ .and(entityScope(["employees", "roles", "skills"]))
25
+ .and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]));
23
26
 
27
+ /**
28
+ * Configuration for {@link createMaxHoursDayRule}.
29
+ *
30
+ * - `hours` (required): maximum hours allowed per day
31
+ * - `priority` (required): how strictly the solver enforces this rule
32
+ *
33
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
34
+ * Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
35
+ */
24
36
  export type MaxHoursDayConfig = z.infer<typeof MaxHoursDaySchema>;
25
37
 
26
38
  /**
27
39
  * Limits how many hours a person can work in a single day.
28
40
  *
29
- * Supports entity scoping (people, roles, skills) and time scoping
30
- * (date ranges, specific dates, days of week, recurring periods).
31
- *
41
+ * @param config - See {@link MaxHoursDayConfig}
32
42
  * @example Limit everyone to 8 hours per day
33
43
  * ```ts
34
44
  * createMaxHoursDayRule({
@@ -49,14 +59,15 @@ export type MaxHoursDayConfig = z.infer<typeof MaxHoursDaySchema>;
49
59
  */
50
60
  export function createMaxHoursDayRule(config: MaxHoursDayConfig): CompilationRule {
51
61
  const parsed = MaxHoursDaySchema.parse(config);
62
+ const entityScopeValue = parseEntityScope(parsed);
63
+ const timeScopeValue = parseTimeScope(parsed);
52
64
  const { hours, priority } = parsed;
53
65
  const maxMinutes = hours * 60;
54
66
 
55
67
  return {
56
68
  compile(b) {
57
- const scope = normalizeScope(parsed, b.employees);
58
- const targetEmployees = resolveEmployees(scope, b.employees);
59
- const activeDays = resolveActiveDays(scope, b.days);
69
+ const targetEmployees = resolveEmployeesFromScope(entityScopeValue, b.employees);
70
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
60
71
 
61
72
  if (targetEmployees.length === 0 || activeDays.length === 0) return;
62
73
 
@@ -84,104 +95,3 @@ export function createMaxHoursDayRule(config: MaxHoursDayConfig): CompilationRul
84
95
  },
85
96
  };
86
97
  }
87
-
88
- function resolveEmployees(scope: RuleScope, employees: SchedulingEmployee[]): SchedulingEmployee[] {
89
- const entity = scope.entity;
90
- switch (entity.type) {
91
- case "employees":
92
- return employees.filter((e) => entity.employeeIds.includes(e.id));
93
- case "roles":
94
- return employees.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)));
95
- case "skills":
96
- return employees.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)));
97
- case "global":
98
- default:
99
- return employees;
100
- }
101
- }
102
-
103
- type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
104
-
105
- function getDayOfWeekName(dayIndex: number): DayName {
106
- const names: Record<number, DayName> = {
107
- 0: "sunday",
108
- 1: "monday",
109
- 2: "tuesday",
110
- 3: "wednesday",
111
- 4: "thursday",
112
- 5: "friday",
113
- 6: "saturday",
114
- };
115
- return names[dayIndex % 7] ?? "sunday";
116
- }
117
-
118
- function resolveActiveDays(scope: RuleScope, allDays: string[]): string[] {
119
- const timeScope = scope.time;
120
-
121
- if (!timeScope) {
122
- return allDays;
123
- }
124
-
125
- switch (timeScope.type) {
126
- case "always":
127
- return allDays;
128
-
129
- case "dateRange": {
130
- const start = timeScope.start;
131
- const end = timeScope.end;
132
- return allDays.filter((day) => day >= start && day <= end);
133
- }
134
-
135
- case "specificDates":
136
- return allDays.filter((day) => timeScope.dates.includes(day));
137
-
138
- case "dayOfWeek": {
139
- const targetDays = new Set(timeScope.days);
140
- return allDays.filter((day) => {
141
- const date = parseDayString(day);
142
- const dayName = getDayOfWeekName(date.getUTCDay());
143
- return targetDays.has(dayName);
144
- });
145
- }
146
-
147
- case "recurring": {
148
- return allDays.filter((day) => {
149
- const date = parseDayString(day);
150
- const month = date.getUTCMonth() + 1;
151
- const dayOfMonth = date.getUTCDate();
152
-
153
- return timeScope.periods.some((period) =>
154
- isDateInRecurringPeriod(month, dayOfMonth, period),
155
- );
156
- });
157
- }
158
-
159
- default:
160
- return allDays;
161
- }
162
- }
163
-
164
- function isDateInRecurringPeriod(
165
- month: number,
166
- dayOfMonth: number,
167
- period: {
168
- startMonth: number;
169
- startDay: number;
170
- endMonth: number;
171
- endDay: number;
172
- },
173
- ): boolean {
174
- const { startMonth, startDay, endMonth, endDay } = period;
175
-
176
- if (startMonth <= endMonth) {
177
- if (month < startMonth || month > endMonth) return false;
178
- if (month === startMonth && dayOfMonth < startDay) return false;
179
- if (month === endMonth && dayOfMonth > endDay) return false;
180
- return true;
181
- } else {
182
- if (month > endMonth && month < startMonth) return false;
183
- if (month === startMonth && dayOfMonth < startDay) return false;
184
- if (month === endMonth && dayOfMonth > endDay) return false;
185
- return true;
186
- }
187
- }
@@ -1,43 +1,51 @@
1
1
  import * as z from "zod";
2
2
  import { DayOfWeekSchema } from "../../types.js";
3
3
  import type { CompilationRule } from "../model-builder.js";
4
- import type { SchedulingEmployee, Term } from "../types.js";
5
- import { parseDayString, priorityToPenalty, splitIntoWeeks } from "../utils.js";
6
- import { normalizeScope, withScopes, type RuleScope } from "./scoping.js";
7
-
8
- const MaxHoursWeekSchema = withScopes(
9
- z.object({
10
- hours: z.number().min(0),
11
- priority: z.union([
12
- z.literal("LOW"),
13
- z.literal("MEDIUM"),
14
- z.literal("HIGH"),
15
- z.literal("MANDATORY"),
16
- ]),
17
- // Optional override; defaults to ModelBuilder.weekStartsOn
18
- weekStartsOn: DayOfWeekSchema.optional(),
19
- }),
20
- {
21
- entities: ["employees", "roles", "skills"],
22
- times: ["dateRange", "specificDates", "dayOfWeek", "recurring"],
23
- },
4
+ import type { Term } from "../types.js";
5
+ import { priorityToPenalty, splitIntoWeeks } from "../utils.js";
6
+ import {
7
+ entityScope,
8
+ timeScope,
9
+ parseEntityScope,
10
+ parseTimeScope,
11
+ resolveEmployeesFromScope,
12
+ resolveActiveDaysFromScope,
13
+ } from "./scope.types.js";
14
+
15
+ const MaxHoursWeekBase = z.object({
16
+ hours: z.number().min(0),
17
+ priority: z.union([
18
+ z.literal("LOW"),
19
+ z.literal("MEDIUM"),
20
+ z.literal("HIGH"),
21
+ z.literal("MANDATORY"),
22
+ ]),
23
+ weekStartsOn: DayOfWeekSchema.optional(),
24
+ });
25
+
26
+ const MaxHoursWeekSchema = MaxHoursWeekBase.and(entityScope(["employees", "roles", "skills"])).and(
27
+ timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]),
24
28
  );
25
29
 
30
+ /**
31
+ * Configuration for {@link createMaxHoursWeekRule}.
32
+ *
33
+ * - `hours` (required): maximum hours allowed per scheduling week
34
+ * - `priority` (required): how strictly the solver enforces this rule
35
+ * - `weekStartsOn` (optional): which day starts the week; defaults to {@link ModelBuilder.weekStartsOn}
36
+ *
37
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
38
+ * Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
39
+ */
26
40
  export type MaxHoursWeekConfig = z.infer<typeof MaxHoursWeekSchema>;
27
41
 
28
42
  /**
29
43
  * Caps total hours a person can work within each scheduling week.
30
44
  *
31
- * Supports entity scoping (people, roles, skills) and time scoping
32
- * (date ranges, specific dates, days of week, recurring periods).
33
- * Time scoping filters which days within each week count toward the limit.
34
- *
45
+ * @param config - See {@link MaxHoursWeekConfig}
35
46
  * @example Limit everyone to 40 hours per week
36
47
  * ```ts
37
- * createMaxHoursWeekRule({
38
- * hours: 40,
39
- * priority: "HIGH",
40
- * });
48
+ * createMaxHoursWeekRule({ hours: 40, priority: "HIGH" });
41
49
  * ```
42
50
  *
43
51
  * @example Students limited to 20 hours during term time
@@ -47,7 +55,6 @@ export type MaxHoursWeekConfig = z.infer<typeof MaxHoursWeekSchema>;
47
55
  * hours: 20,
48
56
  * recurringPeriods: [
49
57
  * { name: "fall-term", startMonth: 9, startDay: 1, endMonth: 12, endDay: 15 },
50
- * { name: "spring-term", startMonth: 1, startDay: 15, endMonth: 5, endDay: 31 },
51
58
  * ],
52
59
  * priority: "MANDATORY",
53
60
  * });
@@ -55,18 +62,19 @@ export type MaxHoursWeekConfig = z.infer<typeof MaxHoursWeekSchema>;
55
62
  */
56
63
  export function createMaxHoursWeekRule(config: MaxHoursWeekConfig): CompilationRule {
57
64
  const parsed = MaxHoursWeekSchema.parse(config);
58
- const { hours, priority } = parsed;
65
+ const entityScopeValue = parseEntityScope(parsed);
66
+ const timeScopeValue = parseTimeScope(parsed);
67
+ const { hours, priority, weekStartsOn } = parsed;
59
68
  const maxMinutes = hours * 60;
60
69
 
61
70
  return {
62
71
  compile(b) {
63
- const scope = normalizeScope(parsed, b.employees);
64
- const targetEmployees = resolveEmployees(scope, b.employees);
65
- const activeDays = resolveActiveDays(scope, b.days);
72
+ const targetEmployees = resolveEmployeesFromScope(entityScopeValue, b.employees);
73
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
66
74
 
67
75
  if (targetEmployees.length === 0 || activeDays.length === 0) return;
68
76
 
69
- const weeks = splitIntoWeeks(activeDays, parsed.weekStartsOn ?? b.weekStartsOn);
77
+ const weeks = splitIntoWeeks(activeDays, weekStartsOn ?? b.weekStartsOn);
70
78
 
71
79
  for (const emp of targetEmployees) {
72
80
  for (const weekDays of weeks) {
@@ -94,104 +102,3 @@ export function createMaxHoursWeekRule(config: MaxHoursWeekConfig): CompilationR
94
102
  },
95
103
  };
96
104
  }
97
-
98
- function resolveEmployees(scope: RuleScope, employees: SchedulingEmployee[]): SchedulingEmployee[] {
99
- const entity = scope.entity;
100
- switch (entity.type) {
101
- case "employees":
102
- return employees.filter((e) => entity.employeeIds.includes(e.id));
103
- case "roles":
104
- return employees.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)));
105
- case "skills":
106
- return employees.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)));
107
- case "global":
108
- default:
109
- return employees;
110
- }
111
- }
112
-
113
- type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
114
-
115
- function getDayOfWeekName(dayIndex: number): DayName {
116
- const names: Record<number, DayName> = {
117
- 0: "sunday",
118
- 1: "monday",
119
- 2: "tuesday",
120
- 3: "wednesday",
121
- 4: "thursday",
122
- 5: "friday",
123
- 6: "saturday",
124
- };
125
- return names[dayIndex % 7] ?? "sunday";
126
- }
127
-
128
- function resolveActiveDays(scope: RuleScope, allDays: string[]): string[] {
129
- const timeScope = scope.time;
130
-
131
- if (!timeScope) {
132
- return allDays;
133
- }
134
-
135
- switch (timeScope.type) {
136
- case "always":
137
- return allDays;
138
-
139
- case "dateRange": {
140
- const start = timeScope.start;
141
- const end = timeScope.end;
142
- return allDays.filter((day) => day >= start && day <= end);
143
- }
144
-
145
- case "specificDates":
146
- return allDays.filter((day) => timeScope.dates.includes(day));
147
-
148
- case "dayOfWeek": {
149
- const targetDays = new Set(timeScope.days);
150
- return allDays.filter((day) => {
151
- const date = parseDayString(day);
152
- const dayName = getDayOfWeekName(date.getUTCDay());
153
- return targetDays.has(dayName);
154
- });
155
- }
156
-
157
- case "recurring": {
158
- return allDays.filter((day) => {
159
- const date = parseDayString(day);
160
- const month = date.getUTCMonth() + 1;
161
- const dayOfMonth = date.getUTCDate();
162
-
163
- return timeScope.periods.some((period) =>
164
- isDateInRecurringPeriod(month, dayOfMonth, period),
165
- );
166
- });
167
- }
168
-
169
- default:
170
- return allDays;
171
- }
172
- }
173
-
174
- function isDateInRecurringPeriod(
175
- month: number,
176
- dayOfMonth: number,
177
- period: {
178
- startMonth: number;
179
- startDay: number;
180
- endMonth: number;
181
- endDay: number;
182
- },
183
- ): boolean {
184
- const { startMonth, startDay, endMonth, endDay } = period;
185
-
186
- if (startMonth <= endMonth) {
187
- if (month < startMonth || month > endMonth) return false;
188
- if (month === startMonth && dayOfMonth < startDay) return false;
189
- if (month === endMonth && dayOfMonth > endDay) return false;
190
- return true;
191
- } else {
192
- if (month > endMonth && month < startMonth) return false;
193
- if (month === startMonth && dayOfMonth < startDay) return false;
194
- if (month === endMonth && dayOfMonth > endDay) return false;
195
- return true;
196
- }
197
- }
@@ -1,12 +1,18 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilationRule } from "../model-builder.js";
3
- import type { SchedulingEmployee } from "../types.js";
4
3
  import type { Term } from "../types.js";
5
- import { parseDayString, priorityToPenalty } from "../utils.js";
6
- import { normalizeScope, withScopes, type RuleScope } from "./scoping.js";
7
-
8
- const MaxShiftsDaySchema = withScopes(
9
- z.object({
4
+ import { priorityToPenalty } from "../utils.js";
5
+ import {
6
+ entityScope,
7
+ timeScope,
8
+ parseEntityScope,
9
+ parseTimeScope,
10
+ resolveEmployeesFromScope,
11
+ resolveActiveDaysFromScope,
12
+ } from "./scope.types.js";
13
+
14
+ const MaxShiftsDaySchema = z
15
+ .object({
10
16
  shifts: z.number().int().min(1),
11
17
  priority: z.union([
12
18
  z.literal("LOW"),
@@ -14,25 +20,29 @@ const MaxShiftsDaySchema = withScopes(
14
20
  z.literal("HIGH"),
15
21
  z.literal("MANDATORY"),
16
22
  ]),
17
- }),
18
- {
19
- entities: ["employees", "roles", "skills"],
20
- times: ["dateRange", "specificDates", "dayOfWeek", "recurring"],
21
- },
22
- );
23
+ })
24
+ .and(entityScope(["employees", "roles", "skills"]))
25
+ .and(timeScope(["dateRange", "specificDates", "dayOfWeek", "recurring"]));
23
26
 
27
+ /**
28
+ * Configuration for {@link createMaxShiftsDayRule}.
29
+ *
30
+ * - `shifts` (required): maximum number of shifts per day (at least 1)
31
+ * - `priority` (required): how strictly the solver enforces this rule
32
+ *
33
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
34
+ * Time scoping (at most one, optional): `dateRange`, `specificDates`, `dayOfWeek`, `recurringPeriods`
35
+ */
24
36
  export type MaxShiftsDayConfig = z.infer<typeof MaxShiftsDaySchema>;
25
37
 
26
38
  /**
27
39
  * Limits how many shifts a person can work in a single day.
28
40
  *
29
- * This rule controls the maximum number of distinct shift assignments per day,
41
+ * Controls the maximum number of distinct shift assignments per day,
30
42
  * regardless of shift duration. For limiting total hours worked, use `max-hours-day`.
31
43
  *
32
- * Supports entity scoping (people, roles, skills) and time scoping
33
- * (date ranges, specific dates, days of week, recurring periods).
34
- *
35
- * @example Limit to one shift per day (common for most schedules)
44
+ * @param config - See {@link MaxShiftsDayConfig}
45
+ * @example Limit to one shift per day
36
46
  * ```ts
37
47
  * createMaxShiftsDayRule({
38
48
  * shifts: 1,
@@ -40,15 +50,6 @@ export type MaxShiftsDayConfig = z.infer<typeof MaxShiftsDaySchema>;
40
50
  * });
41
51
  * ```
42
52
  *
43
- * @example Allow up to two shifts per day for part-time workers
44
- * ```ts
45
- * createMaxShiftsDayRule({
46
- * roleIds: ["part-time"],
47
- * shifts: 2,
48
- * priority: "HIGH",
49
- * });
50
- * ```
51
- *
52
53
  * @example Students can work 2 shifts on weekends only
53
54
  * ```ts
54
55
  * createMaxShiftsDayRule({
@@ -61,13 +62,14 @@ export type MaxShiftsDayConfig = z.infer<typeof MaxShiftsDaySchema>;
61
62
  */
62
63
  export function createMaxShiftsDayRule(config: MaxShiftsDayConfig): CompilationRule {
63
64
  const parsed = MaxShiftsDaySchema.parse(config);
65
+ const entityScopeValue = parseEntityScope(parsed);
66
+ const timeScopeValue = parseTimeScope(parsed);
64
67
  const { shifts, priority } = parsed;
65
68
 
66
69
  return {
67
70
  compile(b) {
68
- const scope = normalizeScope(parsed, b.employees);
69
- const targetEmployees = resolveEmployees(scope, b.employees);
70
- const activeDays = resolveActiveDays(scope, b.days);
71
+ const targetEmployees = resolveEmployeesFromScope(entityScopeValue, b.employees);
72
+ const activeDays = resolveActiveDaysFromScope(timeScopeValue, b.days);
71
73
 
72
74
  if (targetEmployees.length === 0 || activeDays.length === 0) return;
73
75
 
@@ -95,104 +97,3 @@ export function createMaxShiftsDayRule(config: MaxShiftsDayConfig): CompilationR
95
97
  },
96
98
  };
97
99
  }
98
-
99
- function resolveEmployees(scope: RuleScope, employees: SchedulingEmployee[]): SchedulingEmployee[] {
100
- const entity = scope.entity;
101
- switch (entity.type) {
102
- case "employees":
103
- return employees.filter((e) => entity.employeeIds.includes(e.id));
104
- case "roles":
105
- return employees.filter((e) => e.roleIds.some((r) => entity.roleIds.includes(r)));
106
- case "skills":
107
- return employees.filter((e) => e.skillIds?.some((s) => entity.skillIds.includes(s)));
108
- case "global":
109
- default:
110
- return employees;
111
- }
112
- }
113
-
114
- type DayName = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";
115
-
116
- function getDayOfWeekName(dayIndex: number): DayName {
117
- const names: Record<number, DayName> = {
118
- 0: "sunday",
119
- 1: "monday",
120
- 2: "tuesday",
121
- 3: "wednesday",
122
- 4: "thursday",
123
- 5: "friday",
124
- 6: "saturday",
125
- };
126
- return names[dayIndex % 7] ?? "sunday";
127
- }
128
-
129
- function resolveActiveDays(scope: RuleScope, allDays: string[]): string[] {
130
- const timeScope = scope.time;
131
-
132
- if (!timeScope) {
133
- return allDays;
134
- }
135
-
136
- switch (timeScope.type) {
137
- case "always":
138
- return allDays;
139
-
140
- case "dateRange": {
141
- const start = timeScope.start;
142
- const end = timeScope.end;
143
- return allDays.filter((day) => day >= start && day <= end);
144
- }
145
-
146
- case "specificDates":
147
- return allDays.filter((day) => timeScope.dates.includes(day));
148
-
149
- case "dayOfWeek": {
150
- const targetDays = new Set(timeScope.days);
151
- return allDays.filter((day) => {
152
- const date = parseDayString(day);
153
- const dayName = getDayOfWeekName(date.getUTCDay());
154
- return targetDays.has(dayName);
155
- });
156
- }
157
-
158
- case "recurring": {
159
- return allDays.filter((day) => {
160
- const date = parseDayString(day);
161
- const month = date.getUTCMonth() + 1;
162
- const dayOfMonth = date.getUTCDate();
163
-
164
- return timeScope.periods.some((period) =>
165
- isDateInRecurringPeriod(month, dayOfMonth, period),
166
- );
167
- });
168
- }
169
-
170
- default:
171
- return allDays;
172
- }
173
- }
174
-
175
- function isDateInRecurringPeriod(
176
- month: number,
177
- dayOfMonth: number,
178
- period: {
179
- startMonth: number;
180
- startDay: number;
181
- endMonth: number;
182
- endDay: number;
183
- },
184
- ): boolean {
185
- const { startMonth, startDay, endMonth, endDay } = period;
186
-
187
- if (startMonth <= endMonth) {
188
- if (month < startMonth || month > endMonth) return false;
189
- if (month === startMonth && dayOfMonth < startDay) return false;
190
- if (month === endMonth && dayOfMonth > endDay) return false;
191
- return true;
192
- } else {
193
- if (month > endMonth && month < startMonth) return false;
194
- if (month === startMonth && dayOfMonth < startDay) return false;
195
- if (month === endMonth && dayOfMonth > endDay) return false;
196
- return true;
197
- }
198
- }
@@ -1,10 +1,10 @@
1
1
  import * as z from "zod";
2
2
  import type { CompilationRule } from "../model-builder.js";
3
3
  import { priorityToPenalty } from "../utils.js";
4
- import { withScopes } from "./scoping.js";
4
+ import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
5
5
 
6
- const MinConsecutiveDaysSchema = withScopes(
7
- z.object({
6
+ const MinConsecutiveDaysSchema = z
7
+ .object({
8
8
  days: z.number().min(0),
9
9
  priority: z.union([
10
10
  z.literal("LOW"),
@@ -12,35 +12,39 @@ const MinConsecutiveDaysSchema = withScopes(
12
12
  z.literal("HIGH"),
13
13
  z.literal("MANDATORY"),
14
14
  ]),
15
- }),
16
- { entities: ["employees", "roles", "skills"], times: [] },
17
- );
15
+ })
16
+ .and(entityScope(["employees", "roles", "skills"]));
18
17
 
18
+ /**
19
+ * Configuration for {@link createMinConsecutiveDaysRule}.
20
+ *
21
+ * - `days` (required): minimum consecutive days required once a person starts working
22
+ * - `priority` (required): how strictly the solver enforces this rule
23
+ *
24
+ * Entity scoping (at most one): `employeeIds`, `roleIds`, `skillIds`
25
+ */
19
26
  export type MinConsecutiveDaysConfig = z.infer<typeof MinConsecutiveDaysSchema>;
20
27
 
21
28
  /**
22
29
  * Requires that once a person starts working, they continue for a minimum
23
30
  * number of consecutive days.
24
31
  *
32
+ * @param config - See {@link MinConsecutiveDaysConfig}
25
33
  * @example
26
34
  * ```ts
27
- * const rule = createMinConsecutiveDaysRule({
28
- * days: 3,
29
- * priority: "MANDATORY",
30
- * });
31
- * builder = new ModelBuilder({ ...config, rules: [rule] });
35
+ * createMinConsecutiveDaysRule({ days: 3, priority: "MANDATORY" });
32
36
  * ```
33
37
  */
34
38
  export function createMinConsecutiveDaysRule(config: MinConsecutiveDaysConfig): CompilationRule {
35
- const { days, priority, employeeIds } = MinConsecutiveDaysSchema.parse(config);
39
+ const parsed = MinConsecutiveDaysSchema.parse(config);
40
+ const scope = parseEntityScope(parsed);
41
+ const { days, priority } = parsed;
36
42
 
37
43
  return {
38
44
  compile(b) {
39
45
  if (days <= 1) return;
40
46
 
41
- const employees = employeeIds
42
- ? b.employees.filter((e) => employeeIds.includes(e.id))
43
- : b.employees;
47
+ const employees = resolveEmployeesFromScope(scope, b.employees);
44
48
 
45
49
  for (const emp of employees) {
46
50
  const worksByDay: string[] = [];