///|
pub struct ShiftSummary {
shift_id : String
label : String
assigned : Int
required : Int
qualified_requirements : Int
total_requirements : Int
covered : Bool
} derive(Eq, Show, ToJson)
///|
pub struct ScheduleSummary {
problem_name : String
worker_count : Int
shift_count : Int
assignment_count : Int
coverage_per_mille : Int
workload_spread : Int
night_spread : Int
weekend_spread : Int
feasible : Bool
score : ScoreBreakdown
workers : Array[WorkerSummary]
shifts : Array[ShiftSummary]
} derive(Eq, Show, ToJson)
///|
fn count_requirement_coverage(
problem : PlanningProblem,
schedule : Schedule,
shift : Shift,
) -> Int {
let mut covered = 0
for requirement in shift.requirements {
let mut qualified = 0
for worker_id in schedule.workers_for(shift.id) {
match problem.worker(worker_id) {
Some(worker) =>
if worker.has_skill(requirement.skill_id, requirement.minimum_level) {
qualified = qualified + 1
}
None => ()
}
}
if qualified >= requirement.count {
covered = covered + 1
}
}
covered
}
///|
pub fn summarize_shift(
problem : PlanningProblem,
schedule : Schedule,
shift_id : String,
) -> ShiftSummary? {
match problem.shift(shift_id) {
None => None
Some(shift) => {
let assigned = assignment_count_for_shift(schedule, shift.id)
let qualified = count_requirement_coverage(problem, schedule, shift)
Some({
shift_id,
label: shift.label,
assigned,
required: shift.required_workers,
qualified_requirements: qualified,
total_requirements: shift.requirements.length(),
covered: assigned >= shift.required_workers &&
qualified == shift.requirements.length(),
})
}
}
}
///|
fn spread(values : Array[Int]) -> Int {
if values.length() == 0 {
return 0
}
let mut low = values[0]
let mut high = values[0]
for value in values {
if value < low {
low = value
}
if value > high {
high = value
}
}
high - low
}
///|
pub fn summarize_schedule(
problem : PlanningProblem,
solution : Solution,
) -> ScheduleSummary {
let workers : Array[WorkerSummary] = []
let nights : Array[Int] = []
let weekends : Array[Int] = []
for worker in problem.workers {
match summarize_worker(problem, solution.schedule, worker.id) {
Some(summary) => {
workers.push(summary)
nights.push(summary.night_shifts)
weekends.push(summary.weekend_shifts)
}
None => ()
}
}
let shifts : Array[ShiftSummary] = []
for shift in problem.shifts {
match summarize_shift(problem, solution.schedule, shift.id) {
Some(summary) => shifts.push(summary)
None => ()
}
}
{
problem_name: problem.name,
worker_count: problem.workers.length(),
shift_count: problem.shifts.length(),
assignment_count: solution.schedule.assignments.length(),
coverage_per_mille: coverage_ratio_per_mille(problem, solution.schedule),
workload_spread: workload_range(problem, solution.schedule),
night_spread: spread(nights),
weekend_spread: spread(weekends),
feasible: solution.feasible,
score: solution.score,
workers,
shifts,
}
}
///|
fn csv_escape(value : String) -> String {
let mut needs_quotes = false
for ch in value {
if ch == ',' || ch == '"' || ch == '\n' || ch == '\r' {
needs_quotes = true
}
}
if !needs_quotes {
return value
}
let buffer = StringBuilder::new()
buffer.write_char('"')
for ch in value {
if ch == '"' {
buffer.write_string("\"\"")
} else {
buffer.write_char(ch)
}
}
buffer.write_char('"')
buffer.to_string()
}
///|
pub fn schedule_to_csv(
problem : PlanningProblem,
solution : Solution,
) -> String {
let output = StringBuilder::new()
output.write_string(
"shift_id,shift_label,worker_id,worker_name,start_minute,end_minute,kind\n",
)
for shift in problem.shifts {
for worker_id in solution.schedule.workers_for(shift.id) {
let worker_name = match problem.worker(worker_id) {
Some(worker) => worker.name
None => "unknown"
}
output.write_string(csv_escape(shift.id))
output.write_char(',')
output.write_string(csv_escape(shift.label))
output.write_char(',')
output.write_string(csv_escape(worker_id))
output.write_char(',')
output.write_string(csv_escape(worker_name))
output.write_char(',')
output.write_string(shift.range.start.value().to_string())
output.write_char(',')
output.write_string(shift.range.end.value().to_string())
output.write_char(',')
output.write_string(csv_escape(shift.kind.to_string()))
output.write_char('\n')
}
}
output.to_string()
}
///|
pub fn workload_to_csv(
problem : PlanningProblem,
solution : Solution,
) -> String {
let output = StringBuilder::new()
output.write_string(
"worker_id,worker_name,assigned_minutes,target_minutes,night_shifts,weekend_shifts\n",
)
for worker in problem.workers {
match summarize_worker(problem, solution.schedule, worker.id) {
None => ()
Some(summary) => {
output.write_string(csv_escape(worker.id))
output.write_char(',')
output.write_string(csv_escape(worker.name))
output.write_char(',')
output.write_string(summary.assigned_minutes.to_string())
output.write_char(',')
output.write_string(summary.target_minutes.to_string())
output.write_char(',')
output.write_string(summary.night_shifts.to_string())
output.write_char(',')
output.write_string(summary.weekend_shifts.to_string())
output.write_char('\n')
}
}
}
output.to_string()
}
///|
fn render_status(value : Bool) -> String {
if value {
"yes"
} else {
"no"
}
}
///|
pub fn render_text_report(
problem : PlanningProblem,
solution : Solution,
) -> String {
let summary = summarize_schedule(problem, solution)
let output = StringBuilder::new()
output.write_string("RosterWeave schedule report\n")
output.write_string("=========================\n")
output.write_string("Problem: " + summary.problem_name + "\n")
output.write_string("Feasible: " + render_status(summary.feasible) + "\n")
output.write_string("Workers: " + summary.worker_count.to_string() + "\n")
output.write_string("Shifts: " + summary.shift_count.to_string() + "\n")
output.write_string(
"Assignments: " + summary.assignment_count.to_string() + "\n",
)
output.write_string(
"Coverage: " + summary.coverage_per_mille.to_string() + "/1000\n",
)
output.write_string("Objective: " + summary.score.total.to_string() + "\n")
output.write_string(
"Workload spread: " + summary.workload_spread.to_string() + " minutes\n",
)
output.write_string("\nAssignments\n")
output.write_string("-----------\n")
for shift in problem.shifts {
output.write_string(shift.label + ": ")
let names : Array[String] = []
for worker_id in solution.schedule.workers_for(shift.id) {
match problem.worker(worker_id) {
Some(worker) => names.push(worker.name)
None => names.push(worker_id)
}
}
output.write_string(names.join(", "))
output.write_char('\n')
}
let explanations = explain_solution(problem, solution)
if explanations.length() > 0 {
output.write_string("\nNotes\n")
output.write_string("-----\n")
for explanation in explanations {
output.write_string(
"- " + explanation.title + ": " + explanation.detail + "\n",
)
}
}
output.to_string()
}
///|
pub fn solution_to_json(
problem : PlanningProblem,
solution : Solution,
) -> String {
summarize_schedule(problem, solution).to_json().stringify(indent=2)
}