///|
/// One assignment-level difference between two schedules.
pub(all) enum AssignmentChange {
Added(assignment~ : Assignment)
Removed(assignment~ : Assignment)
Reassigned(before~ : Assignment, after~ : Assignment)
Retimed(before~ : Assignment, after~ : Assignment)
ReassignedAndRetimed(before~ : Assignment, after~ : Assignment)
Unchanged(assignment~ : Assignment)
} derive(Debug, Eq)
///|
/// Complete impact summary for an event-driven reschedule.
pub(all) struct RescheduleReport {
baseline : Schedule
updated : Schedule
events : Array[ScheduleEvent]
changes : Array[AssignmentChange]
diagnostics : Array[Diagnostic]
changed_assignments : Int
preserved_assignments : Int
newly_unassigned : Int
score_change : Int
} derive(Debug, Eq)
///|
/// Stable name for assignment change categories.
pub fn assignment_change_name(change : AssignmentChange) -> String {
match change {
Added(..) => "added"
Removed(..) => "removed"
Reassigned(..) => "reassigned"
Retimed(..) => "retimed"
ReassignedAndRetimed(..) => "reassigned_and_retimed"
Unchanged(..) => "unchanged"
}
}
///|
/// Visit id affected by one change.
pub fn assignment_change_visit_id(change : AssignmentChange) -> String {
match change {
Added(assignment~) => assignment.visit_id
Removed(assignment~) => assignment.visit_id
Reassigned(before~, ..) => before.visit_id
Retimed(before~, ..) => before.visit_id
ReassignedAndRetimed(before~, ..) => before.visit_id
Unchanged(assignment~) => assignment.visit_id
}
}
///|
/// Compare two assignments for the same visit.
fn compare_assignment(
before : Assignment,
after : Assignment,
) -> AssignmentChange {
let worker_changed = before.worker_id != after.worker_id
let time_changed = before.start_minute != after.start_minute ||
before.end_minute != after.end_minute
if worker_changed && time_changed {
ReassignedAndRetimed(before~, after~)
} else if worker_changed {
Reassigned(before~, after~)
} else if time_changed {
Retimed(before~, after~)
} else {
Unchanged(assignment=after)
}
}
///|
/// Compute assignment changes in stable visit-id order.
pub fn diff_schedules(
baseline : Schedule,
updated : Schedule,
) -> Array[AssignmentChange] {
let visit_ids : Array[String] = []
for assignment in baseline.assignments {
push_unique_string(visit_ids, assignment.visit_id)
}
for assignment in updated.assignments {
push_unique_string(visit_ids, assignment.visit_id)
}
let changes : Array[AssignmentChange] = []
for visit_id in sort_strings(visit_ids) {
match
(
find_assignment(baseline.assignments, visit_id),
find_assignment(updated.assignments, visit_id),
) {
(Some(before), Some(after)) =>
changes.push(compare_assignment(before, after))
(Some(before), None) => changes.push(Removed(assignment=before))
(None, Some(after)) => changes.push(Added(assignment=after))
(None, None) => ()
}
}
changes
}
///|
/// Count visits newly unassigned after an update.
fn count_newly_unassigned(baseline : Schedule, updated : Schedule) -> Int {
let mut count = 0
for missing in updated.unassigned {
let mut existed = false
for previous in baseline.unassigned {
if previous.visit_id == missing.visit_id {
existed = true
}
}
if !existed {
count = count + 1
}
}
count
}
///|
/// Re-solve after operational events while penalizing unnecessary changes.
pub fn reschedule(
baseline : Schedule,
request : ScheduleRequest,
events : Array[ScheduleEvent],
) -> RescheduleReport {
let (mutated, event_diagnostics) = apply_events(request, events)
if diagnostics_have_errors(event_diagnostics) {
return {
baseline,
updated: baseline,
events,
changes: [],
diagnostics: event_diagnostics,
changed_assignments: 0,
preserved_assignments: baseline.assignments.length(),
newly_unassigned: 0,
score_change: 0,
}
}
let updated = solve({ ..mutated, baseline: Some(baseline) })
let changes = diff_schedules(baseline, updated)
let mut changed = 0
let mut preserved = 0
for change in changes {
match change {
Unchanged(..) => preserved = preserved + 1
_ => changed = changed + 1
}
}
{
baseline,
updated,
events,
changes,
diagnostics: event_diagnostics,
changed_assignments: changed,
preserved_assignments: preserved,
newly_unassigned: count_newly_unassigned(baseline, updated),
score_change: updated.score.total - baseline.score.total,
}
}
///|
/// Concise human explanation of one schedule change.
pub fn describe_assignment_change(change : AssignmentChange) -> String {
match change {
Added(assignment~) =>
"\{assignment.visit_id} added for \{assignment.worker_id} at \{assignment.start_minute}"
Removed(assignment~) =>
"\{assignment.visit_id} removed from \{assignment.worker_id}"
Reassigned(before~, after~) =>
"\{before.visit_id} reassigned from \{before.worker_id} to \{after.worker_id}"
Retimed(before~, after~) =>
"\{before.visit_id} moved from \{before.start_minute} to \{after.start_minute}"
ReassignedAndRetimed(before~, after~) =>
"\{before.visit_id} moved from \{before.worker_id} at \{before.start_minute} to \{after.worker_id} at \{after.start_minute}"
Unchanged(assignment~) =>
"\{assignment.visit_id} preserved for \{assignment.worker_id} at \{assignment.start_minute}"
}
}
///|
/// Deterministic summary for logs and the CLI.
pub fn reschedule_summary(report : RescheduleReport) -> String {
"events=\{report.events.length()}, changed=\{report.changed_assignments}, preserved=\{report.preserved_assignments}, newly_unassigned=\{report.newly_unassigned}, score_change=\{report.score_change}"
}