///|
/// A minute offset from the beginning of the planning horizon.
pub struct Minute {
value_ : Int
} derive(Eq, Compare, Show, ToJson)
///|
pub fn Minute::new(value : Int) -> Minute {
{ value_: value }
}
///|
pub fn Minute::value(self : Minute) -> Int {
self.value_
}
///|
pub fn Minute::add(self : Minute, delta : Int) -> Minute {
{ value_: self.value() + delta }
}
///|
pub fn Minute::distance_to(self : Minute, other : Minute) -> Int {
other.value() - self.value()
}
///|
pub struct TimeRange {
start : Minute
end : Minute
} derive(Eq, Show, ToJson)
///|
pub fn TimeRange::new(start : Int, end : Int) -> TimeRange {
{ start: Minute::new(start), end: Minute::new(end) }
}
///|
pub fn TimeRange::duration(self : TimeRange) -> Int {
self.start.distance_to(self.end)
}
///|
pub fn TimeRange::is_valid(self : TimeRange) -> Bool {
self.start.value() >= 0 && self.end.value() > self.start.value()
}
///|
pub fn TimeRange::overlaps(self : TimeRange, other : TimeRange) -> Bool {
self.start.value() < other.end.value() &&
other.start.value() < self.end.value()
}
///|
pub fn TimeRange::gap_after(self : TimeRange, previous : TimeRange) -> Int {
previous.end.distance_to(self.start)
}
///|
pub(all) enum SkillLevel {
Beginner
Qualified
Advanced
Expert
} derive(Eq, Compare, Show, ToJson)
///|
pub fn SkillLevel::rank(self : SkillLevel) -> Int {
match self {
Beginner => 1
Qualified => 2
Advanced => 3
Expert => 4
}
}
///|
pub struct Skill {
id : String
name : String
} derive(Eq, Show, ToJson)
///|
pub fn Skill::new(id : String, name : String) -> Skill {
{ id, name }
}
///|
pub struct WorkerSkill {
skill_id : String
level : SkillLevel
} derive(Eq, Show, ToJson)
///|
pub fn WorkerSkill::new(skill_id : String, level : SkillLevel) -> WorkerSkill {
{ skill_id, level }
}
///|
pub(all) enum PreferenceKind {
Prefer
Avoid
Require
Unavailable
} derive(Eq, Show, ToJson)
///|
pub struct ShiftPreference {
shift_id : String
kind : PreferenceKind
weight : Int
} derive(Eq, Show, ToJson)
///|
pub fn ShiftPreference::new(
shift_id : String,
kind : PreferenceKind,
weight : Int,
) -> ShiftPreference {
{ shift_id, kind, weight }
}
///|
pub struct Worker {
id : String
name : String
skills : Array[WorkerSkill]
availability : Array[TimeRange]
preferences : Array[ShiftPreference]
min_minutes : Int
target_minutes : Int
max_minutes : Int
max_consecutive_shifts : Int
min_rest_minutes : Int
} derive(Eq, Show, ToJson)
///|
pub fn Worker::new(id : String, name : String) -> Worker {
{
id,
name,
skills: [],
availability: [],
preferences: [],
min_minutes: 0,
target_minutes: 2400,
max_minutes: 3000,
max_consecutive_shifts: 5,
min_rest_minutes: 660,
}
}
///|
pub fn Worker::with_limits(
self : Worker,
min_minutes : Int,
target_minutes : Int,
max_minutes : Int,
) -> Worker {
{ ..self, min_minutes, target_minutes, max_minutes }
}
///|
pub fn Worker::with_rest_rules(
self : Worker,
max_consecutive_shifts : Int,
min_rest_minutes : Int,
) -> Worker {
{ ..self, max_consecutive_shifts, min_rest_minutes }
}
///|
pub fn Worker::add_skill(self : Worker, skill : WorkerSkill) -> Worker {
let skills = self.skills.copy()
skills.push(skill)
{ ..self, skills, }
}
///|
pub fn Worker::add_availability(self : Worker, range : TimeRange) -> Worker {
let availability = self.availability.copy()
availability.push(range)
{ ..self, availability, }
}
///|
pub fn Worker::add_preference(
self : Worker,
preference : ShiftPreference,
) -> Worker {
let preferences = self.preferences.copy()
preferences.push(preference)
{ ..self, preferences, }
}
///|
pub fn Worker::skill_level(self : Worker, skill_id : String) -> SkillLevel? {
for skill in self.skills {
if skill.skill_id == skill_id {
return Some(skill.level)
}
}
None
}
///|
pub fn Worker::has_skill(
self : Worker,
skill_id : String,
minimum : SkillLevel,
) -> Bool {
match self.skill_level(skill_id) {
Some(level) => level.rank() >= minimum.rank()
None => false
}
}
///|
pub fn Worker::is_available(self : Worker, range : TimeRange) -> Bool {
if self.availability.length() == 0 {
return true
}
for allowed in self.availability {
if allowed.start.value() <= range.start.value() &&
allowed.end.value() >= range.end.value() {
return true
}
}
false
}
///|
pub fn Worker::preference_for(
self : Worker,
shift_id : String,
) -> ShiftPreference? {
for preference in self.preferences {
if preference.shift_id == shift_id {
return Some(preference)
}
}
None
}
///|
pub struct SkillRequirement {
skill_id : String
minimum_level : SkillLevel
count : Int
} derive(Eq, Show, ToJson)
///|
pub fn SkillRequirement::new(
skill_id : String,
minimum_level : SkillLevel,
count : Int,
) -> SkillRequirement {
{ skill_id, minimum_level, count }
}
///|
pub(all) enum ShiftKind {
Morning
Day
Evening
Night
OnCall
Custom(String)
} derive(Eq, Show, ToJson)
///|
pub fn ShiftKind::is_night(self : ShiftKind) -> Bool {
match self {
Night => true
_ => false
}
}
///|
pub struct Shift {
id : String
label : String
range : TimeRange
kind : ShiftKind
required_workers : Int
preferred_workers : Int
requirements : Array[SkillRequirement]
tags : Array[String]
day_index : Int
weekend : Bool
} derive(Eq, Show, ToJson)
///|
pub fn Shift::new(
id : String,
label : String,
start : Int,
end : Int,
required_workers : Int,
) -> Shift {
{
id,
label,
range: TimeRange::new(start, end),
kind: Day,
required_workers,
preferred_workers: required_workers,
requirements: [],
tags: [],
day_index: start / 1440,
weekend: false,
}
}
///|
pub fn Shift::with_kind(self : Shift, kind : ShiftKind) -> Shift {
{ ..self, kind, }
}
///|
pub fn Shift::with_preferred_workers(self : Shift, count : Int) -> Shift {
{ ..self, preferred_workers: count }
}
///|
pub fn Shift::with_weekend(self : Shift, weekend : Bool) -> Shift {
{ ..self, weekend, }
}
///|
pub fn Shift::add_requirement(
self : Shift,
requirement : SkillRequirement,
) -> Shift {
let requirements = self.requirements.copy()
requirements.push(requirement)
{ ..self, requirements, }
}
///|
pub fn Shift::add_tag(self : Shift, tag : String) -> Shift {
let tags = self.tags.copy()
tags.push(tag)
{ ..self, tags, }
}
///|
pub struct Assignment {
worker_id : String
shift_id : String
locked : Bool
} derive(Eq, Show, ToJson)
///|
pub fn Assignment::new(worker_id : String, shift_id : String) -> Assignment {
{ worker_id, shift_id, locked: false }
}
///|
pub fn Assignment::locked(worker_id : String, shift_id : String) -> Assignment {
{ worker_id, shift_id, locked: true }
}
///|
pub struct Schedule {
assignments : Array[Assignment]
} derive(Eq, Show, ToJson)
///|
pub fn Schedule::empty() -> Schedule {
{ assignments: [] }
}
///|
pub fn Schedule::add(self : Schedule, assignment : Assignment) -> Schedule {
let assignments = self.assignments.copy()
assignments.push(assignment)
{ assignments, }
}
///|
pub fn Schedule::remove(
self : Schedule,
worker_id : String,
shift_id : String,
) -> Schedule {
let assignments : Array[Assignment] = []
for assignment in self.assignments {
if assignment.worker_id != worker_id || assignment.shift_id != shift_id {
assignments.push(assignment)
}
}
{ assignments, }
}
///|
pub fn Schedule::contains(
self : Schedule,
worker_id : String,
shift_id : String,
) -> Bool {
for assignment in self.assignments {
if assignment.worker_id == worker_id && assignment.shift_id == shift_id {
return true
}
}
false
}
///|
pub fn Schedule::workers_for(
self : Schedule,
shift_id : String,
) -> Array[String] {
let result : Array[String] = []
for assignment in self.assignments {
if assignment.shift_id == shift_id {
result.push(assignment.worker_id)
}
}
result
}
///|
pub fn Schedule::shifts_for(
self : Schedule,
worker_id : String,
) -> Array[String] {
let result : Array[String] = []
for assignment in self.assignments {
if assignment.worker_id == worker_id {
result.push(assignment.shift_id)
}
}
result
}
///|
pub struct FairnessWeights {
workload : Int
night : Int
weekend : Int
preference : Int
understaffing : Int
overtime : Int
} derive(Eq, Show, ToJson)
///|
pub fn FairnessWeights::balanced() -> FairnessWeights {
{
workload: 10,
night: 8,
weekend: 6,
preference: 4,
understaffing: 1000,
overtime: 200,
}
}
///|
pub struct PlanningProblem {
name : String
workers : Array[Worker]
skills : Array[Skill]
shifts : Array[Shift]
locked_assignments : Array[Assignment]
weights : FairnessWeights
seed : Int
} derive(Eq, Show, ToJson)
///|
pub fn PlanningProblem::new(name : String) -> PlanningProblem {
{
name,
workers: [],
skills: [],
shifts: [],
locked_assignments: [],
weights: FairnessWeights::balanced(),
seed: 1,
}
}
///|
pub fn PlanningProblem::add_worker(
self : PlanningProblem,
worker : Worker,
) -> PlanningProblem {
let workers = self.workers.copy()
workers.push(worker)
{ ..self, workers, }
}
///|
pub fn PlanningProblem::add_skill(
self : PlanningProblem,
skill : Skill,
) -> PlanningProblem {
let skills = self.skills.copy()
skills.push(skill)
{ ..self, skills, }
}
///|
pub fn PlanningProblem::add_shift(
self : PlanningProblem,
shift : Shift,
) -> PlanningProblem {
let shifts = self.shifts.copy()
shifts.push(shift)
{ ..self, shifts, }
}
///|
pub fn PlanningProblem::lock(
self : PlanningProblem,
worker_id : String,
shift_id : String,
) -> PlanningProblem {
let locked_assignments = self.locked_assignments.copy()
locked_assignments.push(Assignment::locked(worker_id, shift_id))
{ ..self, locked_assignments, }
}
///|
pub fn PlanningProblem::worker(self : PlanningProblem, id : String) -> Worker? {
for worker in self.workers {
if worker.id == id {
return Some(worker)
}
}
None
}
///|
pub fn PlanningProblem::shift(self : PlanningProblem, id : String) -> Shift? {
for shift in self.shifts {
if shift.id == id {
return Some(shift)
}
}
None
}
///|
pub(all) enum DiagnosticSeverity {
Info
Warning
Error
} derive(Eq, Show, ToJson)
///|
pub struct Diagnostic {
code : String
severity : DiagnosticSeverity
message : String
worker_id : String?
shift_id : String?
} derive(Eq, Show, ToJson)
///|
pub fn Diagnostic::new(
code : String,
severity : DiagnosticSeverity,
message : String,
) -> Diagnostic {
{ code, severity, message, worker_id: None, shift_id: None }
}
///|
pub fn Diagnostic::for_worker(
self : Diagnostic,
worker_id : String,
) -> Diagnostic {
{ ..self, worker_id: Some(worker_id) }
}
///|
pub fn Diagnostic::for_shift(
self : Diagnostic,
shift_id : String,
) -> Diagnostic {
{ ..self, shift_id: Some(shift_id) }
}
///|
pub struct ScoreBreakdown {
total : Int
understaffing : Int
skill_shortage : Int
workload_imbalance : Int
night_imbalance : Int
weekend_imbalance : Int
preference_cost : Int
overtime_cost : Int
} derive(Eq, Show, ToJson)
///|
pub fn ScoreBreakdown::zero() -> ScoreBreakdown {
{
total: 0,
understaffing: 0,
skill_shortage: 0,
workload_imbalance: 0,
night_imbalance: 0,
weekend_imbalance: 0,
preference_cost: 0,
overtime_cost: 0,
}
}
///|
pub struct SolveStats {
explored_nodes : Int
pruned_nodes : Int
improvements : Int
elapsed_steps : Int
} derive(Eq, Show, ToJson)
///|
pub struct Solution {
schedule : Schedule
score : ScoreBreakdown
diagnostics : Array[Diagnostic]
feasible : Bool
stats : SolveStats
} derive(Eq, Show, ToJson)