vrp_core/construction/features/
skills.rs

1//! A job-vehicle skills feature.
2
3#[cfg(test)]
4#[path = "../../../tests/unit/construction/features/skills_test.rs"]
5mod skills_test;
6
7use super::*;
8use std::collections::HashSet;
9
10custom_dimension!(JobSkills typeof JobSkills);
11custom_dimension!(VehicleSkills typeof HashSet<String>);
12
13/// A job skills limitation for a vehicle.
14pub struct JobSkills {
15    /// Vehicle should have all of these skills defined.
16    pub all_of: Option<HashSet<String>>,
17    /// Vehicle should have at least one of these skills defined.
18    pub one_of: Option<HashSet<String>>,
19    /// Vehicle should have none of these skills defined.
20    pub none_of: Option<HashSet<String>>,
21}
22
23impl JobSkills {
24    /// Creates a new instance of [`JobSkills`].
25    pub fn new(all_of: Option<Vec<String>>, one_of: Option<Vec<String>>, none_of: Option<Vec<String>>) -> Self {
26        let map: fn(Option<Vec<_>>) -> Option<HashSet<_>> =
27            |skills| skills.and_then(|v| if v.is_empty() { None } else { Some(v.into_iter().collect()) });
28
29        Self { all_of: map(all_of), one_of: map(one_of), none_of: map(none_of) }
30    }
31}
32
33/// Creates a skills feature as hard constraint.
34pub fn create_skills_feature(name: &str, code: ViolationCode) -> Result<Feature, GenericError> {
35    FeatureBuilder::default().with_name(name).with_constraint(SkillsConstraint { code }).build()
36}
37
38struct SkillsConstraint {
39    code: ViolationCode,
40}
41
42impl FeatureConstraint for SkillsConstraint {
43    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
44        match move_ctx {
45            MoveContext::Route { route_ctx, job, .. } => {
46                if let Some(job_skills) = job.dimens().get_job_skills() {
47                    let vehicle_skills = route_ctx.route().actor.vehicle.dimens.get_vehicle_skills();
48                    let is_ok = check_all_of(job_skills, &vehicle_skills)
49                        && check_one_of(job_skills, &vehicle_skills)
50                        && check_none_of(job_skills, &vehicle_skills);
51                    if !is_ok {
52                        return ConstraintViolation::fail(self.code);
53                    }
54                }
55
56                None
57            }
58            MoveContext::Activity { .. } => None,
59        }
60    }
61
62    fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
63        let source_skills = source.dimens().get_job_skills();
64        let candidate_skills = candidate.dimens().get_job_skills();
65
66        let check_skill_sets = |source_set: Option<&HashSet<String>>, candidate_set: Option<&HashSet<String>>| match (
67            source_set,
68            candidate_set,
69        ) {
70            (Some(_), None) | (None, None) => true,
71            (None, Some(_)) => false,
72            (Some(source_skills), Some(candidate_skills)) => candidate_skills.is_subset(source_skills),
73        };
74
75        let has_comparable_skills = match (source_skills, candidate_skills) {
76            (Some(_), None) | (None, None) => true,
77            (None, Some(_)) => false,
78            (Some(source_skills), Some(candidate_skills)) => {
79                check_skill_sets(source_skills.all_of.as_ref(), candidate_skills.all_of.as_ref())
80                    && check_skill_sets(source_skills.one_of.as_ref(), candidate_skills.one_of.as_ref())
81                    && check_skill_sets(source_skills.none_of.as_ref(), candidate_skills.none_of.as_ref())
82            }
83        };
84
85        if has_comparable_skills {
86            Ok(source)
87        } else {
88            Err(self.code)
89        }
90    }
91}
92
93fn check_all_of(job_skills: &JobSkills, vehicle_skills: &Option<&HashSet<String>>) -> bool {
94    match (job_skills.all_of.as_ref(), vehicle_skills) {
95        (Some(job_skills), Some(vehicle_skills)) => job_skills.is_subset(vehicle_skills),
96        (Some(skills), None) if skills.is_empty() => true,
97        (Some(_), None) => false,
98        _ => true,
99    }
100}
101
102fn check_one_of(job_skills: &JobSkills, vehicle_skills: &Option<&HashSet<String>>) -> bool {
103    match (job_skills.one_of.as_ref(), vehicle_skills) {
104        (Some(job_skills), Some(vehicle_skills)) => job_skills.iter().any(|skill| vehicle_skills.contains(skill)),
105        (Some(skills), None) if skills.is_empty() => true,
106        (Some(_), None) => false,
107        _ => true,
108    }
109}
110
111fn check_none_of(job_skills: &JobSkills, vehicle_skills: &Option<&HashSet<String>>) -> bool {
112    match (job_skills.none_of.as_ref(), vehicle_skills) {
113        (Some(job_skills), Some(vehicle_skills)) => job_skills.is_disjoint(vehicle_skills),
114        _ => true,
115    }
116}