///|
fn consecutive_slot_seen_before(shifts : Array[Shift], index : Int) -> Bool {
for previous = 0; previous < index; previous = previous + 1 {
if shifts[previous].slot == shifts[index].slot {
return true
}
}
false
}
///|
fn add_consecutive_work_constraints(
model : RosterModel,
workers : Array[Worker],
shifts : Array[Shift],
maximum_consecutive_slots : Int,
) -> Result[Unit, RosterError] {
for start_index = 0
start_index < shifts.length()
start_index = start_index + 1 {
if consecutive_slot_seen_before(shifts, start_index) {
continue
}
let start_slot = shifts[start_index].slot
let window_variables : Array[Var] = []
let window_slots : Array[Int] = []
for shift_index = 0
shift_index < shifts.length()
shift_index = shift_index + 1 {
let slot = shifts[shift_index].slot
if slot >= start_slot && slot - start_slot <= maximum_consecutive_slots {
window_variables.push(model.variables[shift_index])
if !int_member(window_slots, slot) {
window_slots.push(slot)
}
}
}
if window_slots.length() > maximum_consecutive_slots {
for worker_index = 0
worker_index < workers.length()
worker_index = worker_index + 1 {
match
model.problem.add_named_constraint(
"worker-\{workers[worker_index].id}:consecutive-from-\{start_slot}:max-\{maximum_consecutive_slots}",
CountAtMost(
window_variables.copy(),
worker_index,
maximum_consecutive_slots,
),
) {
Ok(_) => ()
Err(error) => return Err(InvalidModel(error))
}
}
}
}
Ok(())
}
///|
/// Build a roster while limiting consecutive occupied slot values.
///
/// Slot values are treated as equally sized time units. For example, a limit
/// of two permits work in slots 9 and 10, but not 9, 10, and 11. Missing slot
/// values break a sequence. Standard workload and rest policy rules still
/// apply.
pub fn build_roster_with_consecutive_limit(
workers : Array[Worker],
shifts : Array[Shift],
policy : RosterPolicy,
maximum_consecutive_slots : Int,
) -> Result[Roster, RosterError] {
if maximum_consecutive_slots <= 0 {
return Err(InvalidConsecutiveWorkLimit(maximum_consecutive_slots))
}
match validate_roster_policy(policy) {
Err(error) => return Err(error)
Ok(_) => ()
}
match build_roster_model(workers, shifts) {
Err(error) => Err(error)
Ok(model) => {
match apply_roster_policy(model, workers, shifts, policy) {
Err(error) => return Err(error)
Ok(_) => ()
}
match
add_consecutive_work_constraints(
model, workers, shifts, maximum_consecutive_slots,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
match model.problem.solve() {
Unsatisfied(stats) => Err(Unsatisfiable(stats))
Satisfied(solution, stats) =>
Ok(
roster_from_solution(
workers,
shifts,
model.variables,
solution,
stats,
),
)
}
}
}
}