#[cfg(test)]
#[path = "../../tests/unit/checker/checker_test.rs"]
mod checker_test;
use crate::format::problem::*;
use crate::format::solution::*;
use crate::format::Location;
use crate::parse_time;
use hashbrown::{HashMap, HashSet};
use std::sync::Arc;
use vrp_core::models::common::TimeWindow;
use vrp_core::models::Problem as CoreProblem;
pub struct CheckerContext {
pub problem: Problem,
pub matrices: Option<Vec<Matrix>>,
pub solution: Solution,
job_map: HashMap<String, Job>,
core_problem: Arc<CoreProblem>,
}
enum ActivityType {
Terminal,
Job(Box<Job>),
Depot(VehicleDispatch),
Break(VehicleBreak),
Reload(VehicleReload),
}
impl CheckerContext {
pub fn new(
core_problem: Arc<CoreProblem>,
problem: Problem,
matrices: Option<Vec<Matrix>>,
solution: Solution,
) -> Self {
let job_map = problem.plan.jobs.iter().map(|job| (job.id.clone(), job.clone())).collect();
Self { problem, matrices, solution, job_map, core_problem }
}
pub fn check(&self) -> Result<(), Vec<String>> {
let errors = check_vehicle_load(&self)
.err()
.into_iter()
.chain(check_relations(&self).err().into_iter())
.chain(check_breaks(&self).err().into_iter())
.chain(check_assignment(&self).err().into_iter())
.chain(check_routing(&self).err().into_iter())
.chain(check_limits(&self).err().into_iter())
.flatten()
.collect::<Vec<_>>();
let (_, errors) = errors.into_iter().fold((HashSet::new(), Vec::default()), |(mut used, mut errors), error| {
if !used.contains(&error) {
errors.push(error.clone());
used.insert(error);
}
(used, errors)
});
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn get_vehicle(&self, vehicle_id: &str) -> Result<&VehicleType, String> {
self.problem
.fleet
.vehicles
.iter()
.find(|v| v.vehicle_ids.contains(&vehicle_id.to_string()))
.ok_or_else(|| format!("cannot find vehicle with id '{}'", vehicle_id))
}
fn get_activity_time(&self, stop: &Stop, activity: &Activity) -> TimeWindow {
let time = activity
.time
.clone()
.unwrap_or_else(|| Interval { start: stop.time.arrival.clone(), end: stop.time.departure.clone() });
TimeWindow::new(parse_time(&time.start), parse_time(&time.end))
}
fn get_activity_location(&self, stop: &Stop, activity: &Activity) -> Location {
activity.location.clone().unwrap_or_else(|| stop.location.clone())
}
fn get_vehicle_shift(&self, tour: &Tour) -> Result<VehicleShift, String> {
let tour_time = TimeWindow::new(
parse_time(
&tour.stops.first().as_ref().ok_or_else(|| "cannot get first activity".to_string())?.time.arrival,
),
parse_time(&tour.stops.last().as_ref().ok_or_else(|| "cannot get last activity".to_string())?.time.arrival),
);
self.get_vehicle(&tour.vehicle_id)?
.shifts
.iter()
.find(|shift| {
let shift_time = TimeWindow::new(
parse_time(&shift.start.earliest),
shift.end.as_ref().map_or_else(|| f64::MAX, |place| parse_time(&place.latest)),
);
shift_time.intersects(&tour_time)
})
.cloned()
.ok_or_else(|| format!("cannot find shift for tour with vehicle if: '{}'", tour.vehicle_id))
}
fn get_stop_activity_types(&self, stop: &Stop) -> Vec<String> {
stop.activities.iter().map(|a| a.activity_type.clone()).collect()
}
fn get_activity_type(&self, tour: &Tour, stop: &Stop, activity: &Activity) -> Result<ActivityType, String> {
let shift = self.get_vehicle_shift(tour)?;
let time = self.get_activity_time(stop, activity);
let location = self.get_activity_location(stop, activity);
match activity.activity_type.as_str() {
"departure" | "arrival" => Ok(ActivityType::Terminal),
"pickup" | "delivery" | "service" | "replacement" => {
self.job_map.get(activity.job_id.as_str()).map_or_else(
|| Err(format!("cannot find job with id '{}'", activity.job_id)),
|job| Ok(ActivityType::Job(Box::new(job.clone()))),
)
}
"break" => shift
.breaks
.as_ref()
.and_then(|breaks| {
breaks.iter().find(|b| match &b.time {
VehicleBreakTime::TimeWindow(tw) => parse_time_window(tw).intersects(&time),
VehicleBreakTime::TimeOffset(offset) => {
assert_eq!(offset.len(), 2);
let stops = &tour.stops;
let start = parse_time(&stops.first().unwrap().time.arrival) + *offset.first().unwrap();
let end = parse_time(&stops.first().unwrap().time.departure) + *offset.last().unwrap();
TimeWindow::new(start, end).intersects(&time)
}
})
})
.map(|b| ActivityType::Break(b.clone()))
.ok_or_else(|| format!("cannot find break for tour '{}'", tour.vehicle_id)),
"reload" => shift
.reloads
.as_ref()
.and_then(|reload| reload.iter().find(|r| r.location == location && r.tag == activity.job_tag))
.map(|r| ActivityType::Reload(r.clone()))
.ok_or_else(|| format!("cannot find reload for tour '{}'", tour.vehicle_id)),
"dispatch" => shift
.dispatch
.as_ref()
.and_then(|dispatch| dispatch.iter().find(|d| d.location == location))
.map(|d| ActivityType::Depot(d.clone()))
.ok_or_else(|| format!("cannot find dispatch for tour '{}'", tour.vehicle_id)),
_ => Err(format!("unknown activity type: '{}'", activity.activity_type)),
}
}
fn get_job_by_id(&self, job_id: &str) -> Option<&Job> {
self.problem.plan.jobs.iter().find(|job| job.id == job_id)
}
fn visit_job<F1, F2, R>(
&self,
activity: &Activity,
activity_type: &ActivityType,
job_visitor: F1,
other_visitor: F2,
) -> Result<R, String>
where
F1: Fn(&Job, &JobTask) -> R,
F2: Fn() -> R,
{
match activity_type {
ActivityType::Job(job) => {
let pickups = job_task_size(&job.pickups);
let deliveries = job_task_size(&job.deliveries);
let tasks = pickups + deliveries + job_task_size(&job.services) + job_task_size(&job.replacements);
if tasks < 2 || (tasks == 2 && pickups == 1 && deliveries == 1) {
match_job_task(activity.activity_type.as_str(), job, |tasks| tasks.first())
} else {
activity.job_tag.as_ref().ok_or_else(|| {
format!("checker requires that multi job activity must have tag: '{}'", activity.job_id)
})?;
match_job_task(activity.activity_type.as_str(), job, |tasks| {
tasks.iter().find(|task| task.tag == activity.job_tag)
})
}
.map(|task| job_visitor(job, task))
}
.ok_or_else(|| "cannot match activity to job place".to_string()),
_ => Ok(other_visitor()),
}
}
}
fn job_task_size(tasks: &Option<Vec<JobTask>>) -> usize {
tasks.as_ref().map_or(0, |p| p.len())
}
fn match_job_task<'a>(
activity_type: &str,
job: &'a Job,
tasks_fn: impl Fn(&'a Vec<JobTask>) -> Option<&'a JobTask>,
) -> Option<&'a JobTask> {
let tasks = match activity_type {
"pickup" => job.pickups.as_ref(),
"delivery" => job.deliveries.as_ref(),
"service" => job.services.as_ref(),
"replacement" => job.replacements.as_ref(),
_ => None,
};
tasks.and_then(|tasks| tasks_fn(tasks))
}
fn parse_time_window(tw: &[String]) -> TimeWindow {
TimeWindow::new(parse_time(tw.first().unwrap()), parse_time(tw.last().unwrap()))
}
fn get_time_window(stop: &Stop, activity: &Activity) -> TimeWindow {
let (start, end) = activity
.time
.as_ref()
.map_or_else(|| (&stop.time.arrival, &stop.time.departure), |interval| (&interval.start, &interval.end));
TimeWindow::new(parse_time(start), parse_time(end))
}
fn get_location(stop: &Stop, activity: &Activity) -> Location {
activity.location.as_ref().unwrap_or(&stop.location).clone()
}
mod assignment;
use crate::checker::assignment::check_assignment;
mod capacity;
use crate::checker::capacity::check_vehicle_load;
mod limits;
use crate::checker::limits::check_limits;
mod breaks;
use crate::checker::breaks::check_breaks;
mod relations;
use crate::checker::relations::check_relations;
mod routing;
use crate::checker::routing::check_routing;