1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#[cfg(test)]
#[path = "../../tests/unit/checker/breaks_test.rs"]
mod breaks_test;

use super::*;

/// Checks that breaks are properly assigned.
pub fn check_breaks(context: &CheckerContext) -> Result<(), String> {
    context.solution.tours.iter().try_for_each(|tour| {
        let vehicle_shift = context.get_vehicle_shift(tour)?;
        let actual_break_count = tour
            .stops
            .iter()
            .flat_map(|stop| stop.activities.iter())
            .filter(|activity| activity.activity_type == "break")
            .count();
        let matched_break_count = tour.stops.iter().try_fold(0, |acc, stop| {
            stop.activities
                .windows(stop.activities.len().min(2))
                .flat_map(|leg| as_leg_info_with_break(context, tour, stop, leg))
                .try_fold(acc, |acc, (from_loc, to, vehicle_break)| {
                    // check time
                    let visit_time = get_time_window(stop, to);
                    let break_time_window = get_break_time_window(tour, &vehicle_break)?;
                    if !visit_time.intersects(&break_time_window) {
                        return Err(format!(
                            "Break visit time '{:?}' is invalid: expected is in '{:?}'",
                            visit_time, break_time_window
                        ));
                    }

                    // check location
                    let actual_location = get_location(stop, to);
                    match &vehicle_break.locations {
                        Some(locations) => {
                            let is_correct = locations.iter().any(|location| actual_location == *location);
                            if !is_correct {
                                return Err(format!(
                                    "Break location '{:?}' is invalid: expected one of '{:?}'",
                                    actual_location, locations
                                ));
                            }
                        }
                        None => {
                            if *from_loc != actual_location {
                                return Err(format!(
                                    "Break location '{:?}' is invalid: expected previous activity location '{:?}'",
                                    actual_location, from_loc
                                ));
                            }
                        }
                    }

                    Ok(acc + 1)
                })
        })?;

        if actual_break_count != matched_break_count {
            return Err(format!(
                "Cannot match all breaks, matched: '{}', actual '{}' for vehicle '{}', shift index '{}'",
                matched_break_count, actual_break_count, tour.vehicle_id, tour.shift_index
            ));
        }

        let arrival = tour
            .stops
            .last()
            .map(|stop| parse_time(&stop.time.arrival))
            .ok_or_else(|| format!("Cannot get arrival for tour '{}'", tour.vehicle_id))?;

        let expected_break_count =
            vehicle_shift.breaks.iter().flat_map(|breaks| breaks.iter()).fold(0, |acc, vehicle_break| {
                let break_tw = get_break_time_window(tour, vehicle_break).expect("Cannot get break time windows");
                if break_tw.start < arrival {
                    acc + 1
                } else {
                    acc
                }
            });

        let total_break_count = actual_break_count + get_break_violation_count(&context.solution, tour);

        if expected_break_count != total_break_count {
            Err(format!(
                "Amount of breaks does not match, expected: '{}', got '{}' for vehicle '{}', shift index '{}'",
                expected_break_count, total_break_count, tour.vehicle_id, tour.shift_index
            ))
        } else {
            Ok(())
        }
    })
}

fn as_leg_info_with_break<'a>(
    context: &CheckerContext,
    tour: &Tour,
    stop: &'a Stop,
    leg: &'a [Activity],
) -> Option<(&'a Location, &'a Activity, VehicleBreak)> {
    let leg = match leg {
        [from, to] => Some((from.location.as_ref().unwrap_or(&stop.location), to)),
        [to] => Some((&stop.location, to)),
        _ => None,
    };

    if let Some((from_loc, to)) = leg {
        if let Ok(activity_type) = context.get_activity_type(tour, stop, to) {
            if let ActivityType::Break(vehicle_break) = activity_type {
                return Some((from_loc, to, vehicle_break));
            }
        }
    }
    None
}

fn get_break_time_window(tour: &Tour, vehicle_break: &VehicleBreak) -> Result<TimeWindow, String> {
    match &vehicle_break.time {
        VehicleBreakTime::TimeWindow(tw) => Ok(parse_time_window(tw)),
        VehicleBreakTime::TimeOffset(offset) => {
            if offset.len() != 2 {
                return Err(format!("Invalid offset break for tour: '{}'", tour.vehicle_id));
            }

            let departure = tour
                .stops
                .first()
                .map(|stop| parse_time(&stop.time.departure))
                .ok_or_else(|| format!("Cannot get departure time for tour: '{}'", tour.vehicle_id))?;
            Ok(TimeWindow::new(departure + *offset.first().unwrap(), departure + *offset.last().unwrap()))
        }
    }
}

fn get_break_violation_count(solution: &Solution, tour: &Tour) -> usize {
    solution.violations.as_ref().map_or(0, |violations| {
        violations
            .iter()
            .filter(|v| match v {
                Violation::Break { vehicle_id, shift_index, .. }
                    if *vehicle_id == tour.vehicle_id && *shift_index == tour.shift_index =>
                {
                    true
                }
                _ => false,
            })
            .count()
    })
}