///|
pub(all) enum FindingLevel {
Info
Warning
Error
} derive(Debug, Eq)
///|
pub(all) struct PolicyFinding {
rule : String
level : FindingLevel
message : String
person_id : PersonId?
shift_id : ShiftId?
measured : Int
expected : Int
} derive(Debug, Eq)
///|
fn finding(
rule : String,
level : FindingLevel,
message : String,
person_id? : PersonId = "",
shift_id? : ShiftId = "",
measured? : Int = 0,
expected? : Int = 0,
) -> PolicyFinding {
{
rule,
level,
message,
person_id: if person_id == "" {
None
} else {
Some(person_id)
},
shift_id: if shift_id == "" {
None
} else {
Some(shift_id)
},
measured,
expected,
}
}
///|
pub fn check_coverage_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for shift in problem.shifts {
let assigned = assignments_for_shift(assignments, shift.id).length()
if assigned < shift.required_people {
out.push(
finding(
"coverage",
Error,
"shift has uncovered slots",
shift_id=shift.id,
measured=assigned,
expected=shift.required_people,
),
)
}
}
out
}
///|
pub fn check_capacity_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for shift in problem.shifts {
let assigned = assignments_for_shift(assignments, shift.id).length()
if assigned > shift.required_people {
out.push(
finding(
"capacity",
Error,
"shift is assigned above capacity",
shift_id=shift.id,
measured=assigned,
expected=shift.required_people,
),
)
}
}
out
}
///|
pub fn check_maximum_minutes_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for person in problem.people {
let minutes = assigned_minutes(problem, assignments, person.id)
if minutes > person.maximum_minutes {
out.push(
finding(
"maximum-minutes",
Error,
"person exceeds maximum minutes",
person_id=person.id,
measured=minutes,
expected=person.maximum_minutes,
),
)
}
}
out
}
///|
pub fn check_target_minutes_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
tolerance? : Int = 60,
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for person in problem.people {
let minutes = assigned_minutes(problem, assignments, person.id)
if absolute(minutes - person.target_minutes) > tolerance {
out.push(
finding(
"target-minutes",
Warning,
"person is outside target tolerance",
person_id=person.id,
measured=minutes,
expected=person.target_minutes,
),
)
}
}
out
}
///|
pub fn check_availability_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for assignment in assignments {
match
(
find_person(problem, assignment.person_id),
find_shift(problem, assignment.shift_id),
) {
(Some(person), Some(shift)) =>
if !is_available(person, shift) {
out.push(
finding(
"availability",
Error,
"assignment is outside availability",
person_id=person.id,
shift_id=shift.id,
),
)
}
_ => ()
}
}
out
}
///|
pub fn check_skill_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for assignment in assignments {
match
(
find_person(problem, assignment.person_id),
find_shift(problem, assignment.shift_id),
) {
(Some(person), Some(shift)) =>
if !meets_requirements(person, shift) {
out.push(
finding(
"skills",
Error,
"person lacks a required skill",
person_id=person.id,
shift_id=shift.id,
),
)
}
_ => ()
}
}
out
}
///|
pub fn check_overlap_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for i = 0; i < assignments.length(); i = i + 1 {
for j = i + 1; j < assignments.length(); j = j + 1 {
let a = assignments[i]
let b = assignments[j]
if a.person_id == b.person_id {
match
(find_shift(problem, a.shift_id), find_shift(problem, b.shift_id)) {
(Some(sa), Some(sb)) =>
if sa.time.overlaps(sb.time) {
out.push(
finding(
"overlap",
Error,
"person has overlapping shifts",
person_id=a.person_id,
shift_id=a.shift_id,
),
)
}
_ => ()
}
}
}
}
out
}
///|
pub fn check_rest_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for person in problem.people {
let own = assignments_for_person(assignments, person.id)
for i = 0; i < own.length(); i = i + 1 {
for j = i + 1; j < own.length(); j = j + 1 {
match
(
find_shift(problem, own[i].shift_id),
find_shift(problem, own[j].shift_id),
) {
(Some(a), Some(b)) => {
let gap = a.time.gap_to(b.time)
if gap >= 0 && gap < problem.minimum_rest_minutes {
out.push(
finding(
"minimum-rest",
Error,
"rest interval is too short",
person_id=person.id,
shift_id=b.id,
measured=gap,
expected=problem.minimum_rest_minutes,
),
)
}
}
_ => ()
}
}
}
}
out
}
///|
pub fn check_duplicate_slot_policy(
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for i = 0; i < assignments.length(); i = i + 1 {
for j = i + 1; j < assignments.length(); j = j + 1 {
if assignments[i].shift_id == assignments[j].shift_id &&
assignments[i].slot == assignments[j].slot {
out.push(
finding(
"duplicate-slot",
Error,
"multiple people occupy one slot",
shift_id=assignments[i].shift_id,
measured=2,
expected=1,
),
)
}
}
}
out
}
///|
pub fn check_duplicate_person_in_shift_policy(
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for i = 0; i < assignments.length(); i = i + 1 {
for j = i + 1; j < assignments.length(); j = j + 1 {
if assignments[i].shift_id == assignments[j].shift_id &&
assignments[i].person_id == assignments[j].person_id {
out.push(
finding(
"duplicate-person-shift",
Error,
"one person occupies multiple slots in the same shift",
person_id=assignments[i].person_id,
shift_id=assignments[i].shift_id,
),
)
}
}
}
out
}
///|
pub fn check_preference_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
threshold? : Int = -5,
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for assignment in assignments {
match find_person(problem, assignment.person_id) {
Some(person) => {
let score = preference_for(person, assignment.shift_id)
if score <= threshold {
out.push(
finding(
"negative-preference",
Warning,
"assignment conflicts with a strong preference",
person_id=person.id,
shift_id=assignment.shift_id,
measured=score,
expected=threshold + 1,
),
)
}
}
None => ()
}
}
out
}
///|
pub fn check_locked_assignment_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for locked in problem.existing {
if locked.locked {
let mut found = false
for item in assignments {
if same_assignment(locked, item) {
found = true
}
}
if !found {
out.push(
finding(
"locked-assignment",
Error,
"locked assignment was changed",
person_id=locked.person_id,
shift_id=locked.shift_id,
),
)
}
}
}
out
}
///|
pub fn check_priority_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
minimum_priority? : Int = 5,
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for shift in problem.shifts {
if shift.priority >= minimum_priority {
let assigned = assignments_for_shift(assignments, shift.id).length()
if assigned < shift.required_people {
out.push(
finding(
"high-priority-coverage",
Error,
"high-priority shift is not fully covered",
shift_id=shift.id,
measured=assigned,
expected=shift.required_people,
),
)
}
}
}
out
}
///|
pub fn check_empty_skill_policy(
problem : ScheduleProblem,
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for shift in problem.shifts {
for requirement in shift.requirements {
let mut qualified = 0
for person in problem.people {
if skill_level(person, requirement.skill) >= requirement.minimum_level {
qualified = qualified + 1
}
}
if qualified == 0 {
out.push(
finding(
"skill-supply",
Error,
"no person can satisfy skill requirement",
shift_id=shift.id,
measured=0,
expected=1,
),
)
} else if qualified < shift.required_people {
out.push(
finding(
"skill-supply",
Warning,
"qualified person supply is fragile",
shift_id=shift.id,
measured=qualified,
expected=shift.required_people,
),
)
}
}
}
out
}
///|
pub fn check_availability_supply_policy(
problem : ScheduleProblem,
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for shift in problem.shifts {
let mut available = 0
for person in problem.people {
if is_available(person, shift) && meets_requirements(person, shift) {
available = available + 1
}
}
if available < shift.required_people {
out.push(
finding(
"availability-supply",
Error,
"not enough eligible people are available",
shift_id=shift.id,
measured=available,
expected=shift.required_people,
),
)
} else if available == shift.required_people {
out.push(
finding(
"availability-supply",
Warning,
"shift has no backup candidate",
shift_id=shift.id,
measured=available,
expected=shift.required_people + 1,
),
)
}
}
out
}
///|
pub fn check_fairness_gap_policy(
problem : ScheduleProblem,
assignments : Array[Assignment],
maximum_gap? : Int = 480,
) -> Array[PolicyFinding] {
let gap = maximum_workload_gap(problem, assignments)
if gap > maximum_gap {
[
finding(
"fairness-gap",
Warning,
"workload gap exceeds configured limit",
measured=gap,
expected=maximum_gap,
),
]
} else {
[]
}
}
///|
pub fn run_all_policies(
problem : ScheduleProblem,
assignments : Array[Assignment],
) -> Array[PolicyFinding] {
let out : Array[PolicyFinding] = []
for item in check_coverage_policy(problem, assignments) {
out.push(item)
}
for item in check_capacity_policy(problem, assignments) {
out.push(item)
}
for item in check_maximum_minutes_policy(problem, assignments) {
out.push(item)
}
for item in check_target_minutes_policy(problem, assignments) {
out.push(item)
}
for item in check_availability_policy(problem, assignments) {
out.push(item)
}
for item in check_skill_policy(problem, assignments) {
out.push(item)
}
for item in check_overlap_policy(problem, assignments) {
out.push(item)
}
for item in check_rest_policy(problem, assignments) {
out.push(item)
}
for item in check_duplicate_slot_policy(assignments) {
out.push(item)
}
for item in check_duplicate_person_in_shift_policy(assignments) {
out.push(item)
}
for item in check_preference_policy(problem, assignments) {
out.push(item)
}
for item in check_locked_assignment_policy(problem, assignments) {
out.push(item)
}
for item in check_priority_policy(problem, assignments) {
out.push(item)
}
for item in check_empty_skill_policy(problem) {
out.push(item)
}
for item in check_availability_supply_policy(problem) {
out.push(item)
}
for item in check_fairness_gap_policy(problem, assignments) {
out.push(item)
}
out
}
///|
pub fn count_policy_errors(findings : Array[PolicyFinding]) -> Int {
let mut count = 0
for item in findings {
if item.level == Error {
count = count + 1
}
}
count
}
///|
pub fn count_policy_warnings(findings : Array[PolicyFinding]) -> Int {
let mut count = 0
for item in findings {
if item.level == Warning {
count = count + 1
}
}
count
}