///|
fn absolute(value : Int) -> Int {
if value < 0 {
-value
} else {
value
}
}
///|
fn average(values : Array[Int]) -> Int {
if values.length() == 0 {
return 0
}
let mut sum = 0
for value in values {
sum = sum + value
}
sum / values.length()
}
///|
fn deviation_cost(values : Array[Int]) -> Int {
let center = average(values)
let mut cost = 0
for value in values {
cost = cost + absolute(value - center)
}
cost
}
///|
fn workload_values(
problem : PlanningProblem,
schedule : Schedule,
) -> Array[Int] {
let values : Array[Int] = []
for worker in problem.workers {
values.push(worker_assigned_minutes(problem, schedule, worker.id))
}
values
}
///|
fn night_values(problem : PlanningProblem, schedule : Schedule) -> Array[Int] {
let values : Array[Int] = []
for worker in problem.workers {
values.push(worker_night_count(problem, schedule, worker.id))
}
values
}
///|
fn weekend_values(problem : PlanningProblem, schedule : Schedule) -> Array[Int] {
let values : Array[Int] = []
for worker in problem.workers {
values.push(worker_weekend_count(problem, schedule, worker.id))
}
values
}
///|
fn preference_cost(problem : PlanningProblem, schedule : Schedule) -> Int {
let mut cost = 0
for assignment in schedule.assignments {
match problem.worker(assignment.worker_id) {
Some(worker) =>
match worker.preference_for(assignment.shift_id) {
Some(preference) =>
match preference.kind {
Prefer => cost = cost - preference.weight
Avoid => cost = cost + preference.weight
Require => cost = cost - preference.weight * 2
Unavailable => cost = cost + preference.weight * 100
}
None => ()
}
None => ()
}
}
for worker in problem.workers {
for preference in worker.preferences {
if preference.kind == Require &&
!schedule.contains(worker.id, preference.shift_id) {
cost = cost + preference.weight * 100
}
}
}
cost
}
///|
fn overtime_cost(problem : PlanningProblem, schedule : Schedule) -> Int {
let mut cost = 0
for worker in problem.workers {
let minutes = worker_assigned_minutes(problem, schedule, worker.id)
if minutes > worker.target_minutes {
cost = cost + minutes - worker.target_minutes
}
if minutes < worker.min_minutes {
cost = cost + worker.min_minutes - minutes
}
}
cost
}
///|
fn coverage_cost(report : ConstraintReport, kind : ViolationKind) -> Int {
let mut cost = 0
for violation in report.violations {
if violation.kind == kind {
cost = cost + violation.magnitude
}
}
cost
}
///|
pub fn score_schedule(
problem : PlanningProblem,
schedule : Schedule,
) -> ScoreBreakdown {
let report = evaluate_constraints(problem, schedule)
let understaffing = coverage_cost(report, Understaffed) *
problem.weights.understaffing
let skill_shortage = coverage_cost(report, SkillShortage) *
problem.weights.understaffing
let workload_imbalance = deviation_cost(workload_values(problem, schedule)) *
problem.weights.workload
let night_imbalance = deviation_cost(night_values(problem, schedule)) *
problem.weights.night
let weekend_imbalance = deviation_cost(weekend_values(problem, schedule)) *
problem.weights.weekend
let preference_cost = preference_cost(problem, schedule) *
problem.weights.preference
let overtime_cost = overtime_cost(problem, schedule) *
problem.weights.overtime
let total = understaffing +
skill_shortage +
workload_imbalance +
night_imbalance +
weekend_imbalance +
preference_cost +
overtime_cost
{
total,
understaffing,
skill_shortage,
workload_imbalance,
night_imbalance,
weekend_imbalance,
preference_cost,
overtime_cost,
}
}
///|
pub fn assignment_delta(
problem : PlanningProblem,
schedule : Schedule,
worker_id : String,
shift_id : String,
) -> Int {
let before = score_schedule(problem, schedule).total
let after = score_schedule(
problem,
schedule.add(Assignment::new(worker_id, shift_id)),
).total
after - before
}
///|
pub struct WorkerSummary {
worker_id : String
assigned_minutes : Int
target_minutes : Int
night_shifts : Int
weekend_shifts : Int
preference_cost : Int
} derive(Eq, Show, ToJson)
///|
pub fn summarize_worker(
problem : PlanningProblem,
schedule : Schedule,
worker_id : String,
) -> WorkerSummary? {
match problem.worker(worker_id) {
None => None
Some(worker) => {
let single = Schedule::empty()
for assignment in schedule.assignments {
if assignment.worker_id == worker_id {
single.assignments.push(assignment)
}
}
Some({
worker_id,
assigned_minutes: worker_assigned_minutes(problem, schedule, worker_id),
target_minutes: worker.target_minutes,
night_shifts: worker_night_count(problem, schedule, worker_id),
weekend_shifts: worker_weekend_count(problem, schedule, worker_id),
preference_cost: preference_cost(problem, single),
})
}
}
}
///|
pub fn workload_range(problem : PlanningProblem, schedule : Schedule) -> Int {
if problem.workers.length() == 0 {
return 0
}
let values = workload_values(problem, schedule)
let mut minimum = values[0]
let mut maximum = values[0]
for value in values {
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
}
maximum - minimum
}
///|
pub fn coverage_ratio_per_mille(
problem : PlanningProblem,
schedule : Schedule,
) -> Int {
let mut required = 0
let mut covered = 0
for shift in problem.shifts {
required = required + shift.required_workers
let count = assignment_count_for_shift(schedule, shift.id)
if count < shift.required_workers {
covered = covered + count
} else {
covered = covered + shift.required_workers
}
}
if required == 0 {
1000
} else {
covered * 1000 / required
}
}