Skip to content

Commit 29505bb

Browse files
committed
Add employee scheduling example
1 parent a7df32a commit 29505bb

File tree

1 file changed

+220
-0
lines changed

1 file changed

+220
-0
lines changed

examples/employee_scheduling.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
use selen::prelude::*;
2+
3+
type Employee = (&'static str, usize, usize, bool); // (name, min_shift, max_shift, is_supervisor)
4+
type Schedule = Vec<Vec<Vec<VarId>>>; // [day][shift][employee]
5+
6+
fn can_work_shift(employee: &Employee, shift: usize) -> bool {
7+
shift >= employee.1 && shift <= employee.2
8+
}
9+
10+
fn create_work_variable(m: &mut Model, can_work: bool) -> VarId {
11+
if can_work {
12+
m.int(0, 1) // Binary: works this shift or not
13+
} else {
14+
m.int(0, 0) // Cannot work - fixed to 0
15+
}
16+
}
17+
18+
fn build_schedule(m: &mut Model, staff: &[Employee], days: usize, shifts: usize) -> Schedule {
19+
let mut schedule = Vec::new();
20+
21+
for _day in 0..days {
22+
let mut day_schedule = Vec::new();
23+
24+
for shift in 0..shifts {
25+
let shift_workers: Vec<VarId> = staff.iter()
26+
.map(|employee| create_work_variable(m, can_work_shift(employee, shift)))
27+
.collect();
28+
day_schedule.push(shift_workers);
29+
}
30+
31+
schedule.push(day_schedule);
32+
}
33+
34+
schedule
35+
}
36+
37+
fn add_staffing_constraints(m: &mut Model, schedule: &Schedule, needed: &[i32]) {
38+
for (_day, day_schedule) in schedule.iter().enumerate() {
39+
for (shift, workers) in day_schedule.iter().enumerate() {
40+
let worker_sum = m.sum(workers);
41+
m.c(worker_sum).eq(int(needed[shift]));
42+
}
43+
}
44+
}
45+
46+
/// Ensures each shift has at least one supervisor working
47+
/// For every shift on every day, at least one supervisor must be scheduled
48+
fn add_supervisor_constraints(m: &mut Model, schedule: &Schedule, staff: &[Employee]) {
49+
// Go through each day in the schedule
50+
for day_schedule in schedule {
51+
// Go through each shift in that day (morning, afternoon, night)
52+
for shift_workers in day_schedule {
53+
// Find all supervisor variables for this specific shift
54+
// We look at each employee and check if they're a supervisor
55+
let supervisors: Vec<VarId> = staff.iter()
56+
.enumerate() // Get employee index and their data
57+
.filter_map(|(emp_id, (_, _, _, is_supervisor))| {
58+
if *is_supervisor {
59+
// This employee is a supervisor, so include their work variable for this shift
60+
Some(shift_workers[emp_id])
61+
} else {
62+
// Regular employee, skip them
63+
None
64+
}
65+
})
66+
.collect();
67+
68+
// Only add constraint if we have supervisors available for this shift
69+
if !supervisors.is_empty() {
70+
// Constraint: sum of supervisor work variables >= 1
71+
// This means at least one supervisor must work this shift
72+
let supervisor_sum = m.sum(&supervisors);
73+
m.c(supervisor_sum).ge(int(1));
74+
}
75+
}
76+
}
77+
}
78+
79+
fn add_workload_constraints(m: &mut Model, schedule: &Schedule, staff: &[Employee]) {
80+
for (emp_id, _) in staff.iter().enumerate() {
81+
for day_schedule in schedule {
82+
let daily_shifts: Vec<VarId> = day_schedule.iter()
83+
.map(|shift_workers| shift_workers[emp_id])
84+
.collect();
85+
let daily_sum = m.sum(&daily_shifts);
86+
m.c(daily_sum).le(int(2));
87+
}
88+
}
89+
}
90+
91+
fn main() {
92+
println!("Employee Scheduling System");
93+
println!("==========================");
94+
95+
let mut m = Model::default();
96+
97+
let staff: [Employee; 9] = [
98+
("Alice", 0, 2, true), // Any shift, supervisor
99+
("Bob", 0, 1, false), // Morning/afternoon only
100+
("Carol", 1, 2, true), // Afternoon/night, supervisor
101+
("David", 0, 2, false), // Any shift
102+
("Eve", 2, 2, false), // Night only
103+
("Frank", 0, 1, true), // Morning/afternoon, supervisor
104+
("Grace", 0, 2, false), // Any shift
105+
("Henry", 1, 2, true), // Afternoon/night, supervisor
106+
("Ivy", 0, 1, false), // Morning/afternoon only
107+
];
108+
109+
let days = 2;
110+
let shifts = 3; // 0=Morning, 1=Afternoon, 2=Night
111+
let needed = [3, 4, 3]; // Staff needed per shift
112+
113+
// Build the scheduling variables
114+
let schedule = build_schedule(&mut m, &staff, days, shifts);
115+
116+
// Add all constraints using helper functions
117+
add_staffing_constraints(&mut m, &schedule, &needed);
118+
add_supervisor_constraints(&mut m, &schedule, &staff);
119+
add_workload_constraints(&mut m, &schedule, &staff);
120+
121+
println!("Solving...");
122+
123+
match m.solve() {
124+
Ok(solution) => {
125+
println!("Schedule found!\n");
126+
127+
// Calculate column widths based on name lengths
128+
let mut col_widths = Vec::new();
129+
for (name, _, _, is_supervisor) in &staff {
130+
let display_name = if *is_supervisor {
131+
format!("{}(S)", name)
132+
} else {
133+
name.to_string()
134+
};
135+
col_widths.push(display_name.len().max(3)); // minimum 3 chars for X/-
136+
}
137+
138+
// Print header
139+
println!("Employee Schedule Table:");
140+
print!("┌───────────");
141+
for &width in &col_widths {
142+
print!("┬{}", "─".repeat(width + 2));
143+
}
144+
println!("┐");
145+
146+
print!("│ Shift ");
147+
for (i, (name, _, _, is_supervisor)) in staff.iter().enumerate() {
148+
let display = if *is_supervisor {
149+
format!("{}(S)", name)
150+
} else {
151+
name.to_string()
152+
};
153+
print!("│ {:<width$} ", display, width = col_widths[i]);
154+
}
155+
println!("│");
156+
157+
print!("├───────────");
158+
for &width in &col_widths {
159+
print!("┼{}", "─".repeat(width + 2));
160+
}
161+
println!("┤");
162+
163+
// Print each shift as a row
164+
let shift_names = ["Morning", "Afternoon", "Night"];
165+
for day in 0..days {
166+
for shift in 0..shifts {
167+
let shift_label = if days == 1 {
168+
format!("{:<9}", shift_names[shift])
169+
} else {
170+
format!("{} D{}", &shift_names[shift][..3], day + 1)
171+
};
172+
print!("│ {:<9} ", shift_label);
173+
174+
for emp_id in 0..staff.len() {
175+
let works = solution.get::<i32>(schedule[day][shift][emp_id]) == 1;
176+
let symbol = if works { "X" } else { "-" };
177+
print!("│ {:<width$} ", symbol, width = col_widths[emp_id]);
178+
}
179+
println!("│");
180+
}
181+
182+
// No separator between days to avoid empty rows
183+
}
184+
185+
print!("└───────────");
186+
for &width in &col_widths {
187+
print!("┴{}", "─".repeat(width + 2));
188+
}
189+
println!("┘");
190+
191+
// Show daily schedule breakdown
192+
println!("\nDaily Breakdown:");
193+
let shift_names = ["Morning", "Afternoon", "Night"];
194+
195+
for day in 0..days {
196+
println!("\nDay {}:", day + 1);
197+
for shift in 0..shifts {
198+
let workers: Vec<_> = staff.iter()
199+
.enumerate()
200+
.filter_map(|(emp_id, (name, _, _, is_sup))| {
201+
if solution.get::<i32>(schedule[day][shift][emp_id]) == 1 {
202+
let role_marker = if *is_sup { "(S)" } else { "" };
203+
Some(format!("{}{}", name, role_marker))
204+
} else {
205+
None
206+
}
207+
})
208+
.collect();
209+
210+
println!(" {:<10}: {} (needed: {})",
211+
shift_names[shift],
212+
if workers.is_empty() { "No one assigned".to_string() } else { workers.join(", ") },
213+
needed[shift]
214+
);
215+
}
216+
}
217+
}
218+
Err(_) => println!("No solution found!"),
219+
}
220+
}

0 commit comments

Comments
 (0)