vrp_core/construction/features/
skills.rs1#[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
13pub struct JobSkills {
15 pub all_of: Option<HashSet<String>>,
17 pub one_of: Option<HashSet<String>>,
19 pub none_of: Option<HashSet<String>>,
21}
22
23impl JobSkills {
24 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
33pub 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}