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.
- package/CHANGELOG.md +34 -0
- package/README.md +68 -31
- package/dist/client.types.d.ts +58 -0
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +13 -2
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +12 -3
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rules/assign-together.d.ts +7 -0
- package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
- package/dist/cpsat/rules/assign-together.js +1 -0
- package/dist/cpsat/rules/assign-together.js.map +1 -1
- package/dist/cpsat/rules/employee-assignment-priority.d.ts +11 -37
- package/dist/cpsat/rules/employee-assignment-priority.d.ts.map +1 -1
- package/dist/cpsat/rules/employee-assignment-priority.js +12 -104
- package/dist/cpsat/rules/employee-assignment-priority.js.map +1 -1
- package/dist/cpsat/rules/location-preference.d.ts +12 -10
- package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
- package/dist/cpsat/rules/location-preference.js +16 -14
- package/dist/cpsat/rules/location-preference.js.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.d.ts +12 -13
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +11 -12
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-hours-day.d.ts +12 -28
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +12 -95
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts +14 -34
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +12 -103
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts +14 -39
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +14 -106
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts +12 -13
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +11 -12
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-hours-day.d.ts +12 -13
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +11 -12
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts +13 -13
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +10 -14
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts +13 -14
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +11 -12
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/resolver.d.ts +2 -2
- package/dist/cpsat/rules/resolver.d.ts.map +1 -1
- package/dist/cpsat/rules/resolver.js +55 -30
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +267 -0
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -0
- package/dist/cpsat/rules/scope.types.js +325 -0
- package/dist/cpsat/rules/scope.types.js.map +1 -0
- package/dist/cpsat/rules/time-off.d.ts +21 -25
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +20 -110
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +2 -0
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +2 -4
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +22 -6
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/utils.d.ts +1 -1
- package/dist/cpsat/utils.js +1 -1
- package/dist/cpsat/validation-reporter.js +1 -1
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/datetime.utils.d.ts +14 -14
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +26 -27
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/llms.d.ts +1 -1
- package/dist/llms.d.ts.map +1 -1
- package/dist/llms.js +1 -1
- package/dist/llms.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/solver-container.js +3 -1
- package/dist/testing/solver-container.js.map +1 -1
- package/dist/types.d.ts +18 -20
- package/dist/types.d.ts.map +1 -1
- package/llms.txt +516 -263
- package/package.json +25 -25
- package/src/client.types.ts +58 -0
- package/src/cpsat/model-builder.ts +19 -7
- package/src/cpsat/response.ts +12 -3
- package/src/cpsat/rules/assign-together.ts +7 -0
- package/src/cpsat/rules/employee-assignment-priority.ts +28 -128
- package/src/cpsat/rules/location-preference.ts +24 -17
- package/src/cpsat/rules/max-consecutive-days.ts +19 -15
- package/src/cpsat/rules/max-hours-day.ts +29 -119
- package/src/cpsat/rules/max-hours-week.ts +42 -135
- package/src/cpsat/rules/max-shifts-day.ts +31 -130
- package/src/cpsat/rules/min-consecutive-days.ts +19 -15
- package/src/cpsat/rules/min-hours-day.ts +19 -15
- package/src/cpsat/rules/min-hours-week.ts +28 -26
- package/src/cpsat/rules/min-rest-between-shifts.ts +21 -17
- package/src/cpsat/rules/resolver.ts +66 -45
- package/src/cpsat/rules/scope.types.ts +534 -0
- package/src/cpsat/rules/time-off.ts +48 -145
- package/src/cpsat/semantic-time.ts +10 -8
- package/src/cpsat/types.ts +22 -6
- package/src/cpsat/utils.ts +1 -1
- package/src/cpsat/validation-reporter.ts +1 -1
- package/src/datetime.utils.ts +27 -29
- package/src/index.ts +11 -7
- package/src/llms.ts +1 -1
- package/src/testing/index.ts +1 -1
- package/src/testing/solver-container.ts +3 -3
- package/src/types.ts +27 -31
- package/dist/cpsat/rules/scoping.d.ts +0 -129
- package/dist/cpsat/rules/scoping.d.ts.map +0 -1
- package/dist/cpsat/rules/scoping.js +0 -190
- package/dist/cpsat/rules/scoping.js.map +0 -1
- 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 {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
58
|
-
const
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
64
|
-
const
|
|
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,
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
|
69
|
-
const
|
|
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 {
|
|
4
|
+
import { entityScope, parseEntityScope, resolveEmployeesFromScope } from "./scope.types.js";
|
|
5
5
|
|
|
6
|
-
const MinConsecutiveDaysSchema =
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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[] = [];
|