vrp_pragmatic/validation/
vehicles.rs1#[cfg(test)]
2#[path = "../../tests/unit/validation/vehicles_test.rs"]
3mod vehicles_test;
4
5use super::*;
6use crate::utils::combine_error_results;
7use crate::validation::common::get_time_windows;
8use crate::{parse_time, parse_time_safe};
9use std::collections::HashSet;
10use vrp_core::models::common::TimeWindow;
11
12fn check_e1300_no_vehicle_types_with_duplicate_type_ids(ctx: &ValidationContext) -> Result<(), FormatError> {
14 get_duplicates(ctx.vehicles().map(|vehicle| &vehicle.type_id)).map_or(Ok(()), |ids| {
15 Err(FormatError::new(
16 "E1300".to_string(),
17 "duplicated vehicle type ids".to_string(),
18 format!("remove duplicated vehicle type ids: {}", ids.join(", ")),
19 ))
20 })
21}
22
23fn check_e1301_no_vehicle_types_with_duplicate_ids(ctx: &ValidationContext) -> Result<(), FormatError> {
25 get_duplicates(ctx.vehicles().flat_map(|vehicle| vehicle.vehicle_ids.iter())).map_or(Ok(()), |ids| {
26 Err(FormatError::new(
27 "E1301".to_string(),
28 "duplicated vehicle ids".to_string(),
29 format!("remove duplicated vehicle ids: {}", ids.join(", ")),
30 ))
31 })
32}
33
34fn check_e1302_vehicle_shift_time(ctx: &ValidationContext) -> Result<(), FormatError> {
36 let type_ids = ctx
37 .vehicles()
38 .filter_map(|vehicle| {
39 let tws = vehicle
40 .shifts
41 .iter()
42 .map(|shift| {
43 vec![
44 shift.start.earliest.clone(),
45 shift.end.as_ref().map_or_else(|| shift.start.earliest.clone(), |end| end.latest.clone()),
46 ]
47 })
48 .collect::<Vec<_>>();
49 if check_raw_time_windows(&tws, false) {
50 None
51 } else {
52 Some(vehicle.type_id.to_string())
53 }
54 })
55 .collect::<Vec<_>>();
56
57 if type_ids.is_empty() {
58 Ok(())
59 } else {
60 Err(FormatError::new(
61 "E1302".to_string(),
62 "invalid start or end times in vehicle shift".to_string(),
63 format!(
64 "ensure that start and end time conform shift time rules, vehicle type ids: {}",
65 type_ids.join(", ")
66 ),
67 ))
68 }
69}
70
71fn check_e1303_vehicle_breaks_time_is_correct(ctx: &ValidationContext) -> Result<(), FormatError> {
73 let type_ids = get_invalid_type_ids(
74 ctx,
75 Box::new(|_, shift, shift_time| {
76 shift
77 .breaks
78 .as_ref()
79 .map(|breaks| {
80 let tws = breaks
81 .iter()
82 .filter_map(|b| match b {
83 VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeWindow(tw), .. } => {
84 Some(get_time_window_from_vec(tw))
85 }
86 VehicleBreak::Required {
87 time: VehicleRequiredBreakTime::OffsetTime { earliest, latest },
88 duration,
89 } => {
90 let departure = parse_time(&shift.start.earliest);
91 Some(Some(TimeWindow::new(departure + *earliest, departure + *latest + *duration)))
92 }
93 VehicleBreak::Required {
94 time: VehicleRequiredBreakTime::ExactTime { earliest, latest },
95 duration,
96 } => Some(
97 parse_time_safe(earliest)
98 .ok()
99 .zip(parse_time_safe(latest).ok())
100 .map(|(start, end)| TimeWindow::new(start, end + *duration)),
101 ),
102 _ => None,
103 })
104 .collect::<Vec<_>>();
105
106 check_shift_time_windows(shift_time, tws, false)
107 })
108 .unwrap_or(true)
109 }),
110 );
111
112 if type_ids.is_empty() {
113 Ok(())
114 } else {
115 Err(FormatError::new(
116 "E1303".to_string(),
117 "invalid break time windows in vehicle shift".to_string(),
118 format!("ensure that break conform rules, vehicle type ids: '{}'", type_ids.join(", ")),
119 ))
120 }
121}
122
123fn check_e1304_vehicle_reload_time_is_correct(ctx: &ValidationContext) -> Result<(), FormatError> {
125 let type_ids = get_invalid_type_ids(
126 ctx,
127 Box::new(|_, shift, shift_time| {
128 shift
129 .reloads
130 .as_ref()
131 .map(|reloads| {
132 let tws = reloads
133 .iter()
134 .filter_map(|reload| reload.times.as_ref())
135 .flat_map(|tws| get_time_windows(tws))
136 .collect::<Vec<_>>();
137
138 check_shift_time_windows(shift_time, tws, true)
139 })
140 .unwrap_or(true)
141 }),
142 );
143
144 if type_ids.is_empty() {
145 Ok(())
146 } else {
147 Err(FormatError::new(
148 "E1304".to_string(),
149 "invalid reload time windows in vehicle shift".to_string(),
150 format!("ensure that reload conform rules, vehicle type ids: '{}'", type_ids.join(", ")),
151 ))
152 }
153}
154
155fn check_e1306_vehicle_has_no_zero_costs(ctx: &ValidationContext) -> Result<(), FormatError> {
157 let type_ids = ctx
158 .vehicles()
159 .filter(|vehicle| vehicle.costs.time == 0. && vehicle.costs.distance == 0.)
160 .map(|vehicle| vehicle.type_id.to_string())
161 .collect::<Vec<_>>();
162
163 if type_ids.is_empty() {
164 Ok(())
165 } else {
166 Err(FormatError::new(
167 "E1306".to_string(),
168 "time and duration costs are zeros".to_string(),
169 format!(
170 "ensure that either time or distance cost is non-zero, \
171 vehicle type ids: '{}'",
172 type_ids.join(", ")
173 ),
174 ))
175 }
176}
177
178fn check_e1307_vehicle_offset_break_rescheduling(ctx: &ValidationContext) -> Result<(), FormatError> {
179 let type_ids = get_invalid_type_ids(
180 ctx,
181 Box::new(|_, shift, _| {
182 shift
183 .breaks
184 .as_ref()
185 .map(|breaks| {
186 let has_time_offset = breaks.iter().any(|br| {
187 matches!(
188 br,
189 VehicleBreak::Required { time: VehicleRequiredBreakTime::OffsetTime { .. }, .. }
190 | VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeOffset { .. }, .. }
191 )
192 });
193 let has_rescheduling =
194 shift.start.latest.as_ref().map_or(true, |latest| *latest != shift.start.earliest);
195
196 !(has_time_offset && has_rescheduling)
197 })
198 .unwrap_or(true)
199 }),
200 );
201
202 if type_ids.is_empty() {
203 Ok(())
204 } else {
205 Err(FormatError::new(
206 "E1307".to_string(),
207 "time offset interval for break is used with departure rescheduling".to_string(),
208 format!("when time offset is used, start.latest should be set equal to start.earliest in the shift, check vehicle type ids: '{}'", type_ids.join(", ")),
209 ))
210 }
211}
212
213fn check_e1308_vehicle_reload_resources(ctx: &ValidationContext) -> Result<(), FormatError> {
214 let reload_resource_ids = ctx
215 .problem
216 .fleet
217 .resources
218 .iter()
219 .flat_map(|resources| resources.iter())
220 .map(|resource| match resource {
221 VehicleResource::Reload { id, .. } => id.to_string(),
222 })
223 .collect::<Vec<_>>();
224
225 let unique_resource_ids = reload_resource_ids.iter().cloned().collect::<HashSet<_>>();
226
227 if reload_resource_ids.len() != unique_resource_ids.len() {
228 return Err(FormatError::new(
229 "E1308".to_string(),
230 "invalid vehicle reload resource".to_string(),
231 "make sure that fleet reload resource ids are unique".to_string(),
232 ));
233 }
234
235 let type_ids = get_invalid_type_ids(
236 ctx,
237 Box::new(move |_, shift, _| {
238 shift
239 .reloads
240 .as_ref()
241 .iter()
242 .flat_map(|reloads| reloads.iter())
243 .filter_map(|reload| reload.resource_id.as_ref())
244 .all(|resource_id| unique_resource_ids.contains(resource_id))
245 }),
246 );
247
248 if type_ids.is_empty() {
249 Ok(())
250 } else {
251 Err(FormatError::new(
252 "E1308".to_string(),
253 "invalid vehicle reload resource".to_string(),
254 format!(
255 "make sure that fleet has all reload resources defined, check vehicle type ids: '{}'",
256 type_ids.join(", ")
257 ),
258 ))
259 }
260}
261
262type CheckShiftFn = Box<dyn Fn(&VehicleType, &VehicleShift, Option<TimeWindow>) -> bool>;
263
264fn get_invalid_type_ids(ctx: &ValidationContext, check_shift_fn: CheckShiftFn) -> Vec<String> {
265 ctx.vehicles()
266 .filter_map(|vehicle| {
267 let all_correct =
268 vehicle.shifts.iter().all(|shift| (check_shift_fn)(vehicle, shift, get_shift_time_window(shift)));
269
270 if all_correct {
271 None
272 } else {
273 Some(vehicle.type_id.clone())
274 }
275 })
276 .collect::<Vec<_>>()
277}
278
279fn check_shift_time_windows(
280 shift_time: Option<TimeWindow>,
281 tws: Vec<Option<TimeWindow>>,
282 skip_intersection_check: bool,
283) -> bool {
284 tws.is_empty()
285 || (check_time_windows(&tws, skip_intersection_check)
286 && shift_time
287 .as_ref()
288 .map_or(true, |shift_time| tws.into_iter().map(|tw| tw.unwrap()).all(|tw| tw.intersects(shift_time))))
289}
290
291fn get_shift_time_window(shift: &VehicleShift) -> Option<TimeWindow> {
292 get_time_window(
293 &shift.start.earliest,
294 &shift.end.clone().map_or_else(|| "2200-07-04T00:00:00Z".to_string(), |end| end.latest),
295 )
296}
297
298pub fn validate_vehicles(ctx: &ValidationContext) -> Result<(), MultiFormatError> {
300 combine_error_results(&[
301 check_e1300_no_vehicle_types_with_duplicate_type_ids(ctx),
302 check_e1301_no_vehicle_types_with_duplicate_ids(ctx),
303 check_e1302_vehicle_shift_time(ctx),
304 check_e1303_vehicle_breaks_time_is_correct(ctx),
305 check_e1304_vehicle_reload_time_is_correct(ctx),
306 check_e1306_vehicle_has_no_zero_costs(ctx),
307 check_e1307_vehicle_offset_break_rescheduling(ctx),
308 check_e1308_vehicle_reload_resources(ctx),
309 ])
310 .map_err(From::from)
311}