use super::funejson::Value; use super::dates_add_days::add_days; use super::fleet_drivers_hours::{driver_activity_from_value, hours_breach_to_value, DriverActivity, HoursBreach}; use super::fleet_tachograph_rest_data::{REST_LIMITS, REST_LIMITS_HISTORY}; const KINDS: [&str; 4] = ["driving", "work", "availability", "rest"]; fn is_iso_date(value: &str) -> bool { let bytes = value.as_bytes(); bytes.len() == 10 && bytes[4] == b'-' && bytes[7] == b'-' && bytes .iter() .enumerate() .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit()) } fn parse_hh_mm(value: &str) -> Option { let bytes = value.as_bytes(); if bytes.len() != 5 || bytes[2] != b':' { return None; } if !(bytes[0].is_ascii_digit() && bytes[1].is_ascii_digit() && bytes[3].is_ascii_digit() && bytes[4].is_ascii_digit()) { return None; } let hours = ((bytes[0] - b'0') * 10 + (bytes[1] - b'0')) as i64; let minutes = ((bytes[3] - b'0') * 10 + (bytes[4] - b'0')) as i64; if hours > 23 || minutes > 59 { return None; } Some(hours * 60 + minutes) } fn limit_on(rule: &str, on_date: &str) -> i64 { let mut best: Option<(i64, &'static str)> = None; let mut earliest: Option<&'static str> = None; for row in REST_LIMITS { if row.rule != rule { continue; } if earliest.map_or(true, |e| row.valid_from < e) { earliest = Some(row.valid_from); } if on_date < row.valid_from { continue; } if let Some(valid_to) = row.valid_to { if on_date > valid_to { continue; } } if best.map_or(true, |(_, from)| row.valid_from > from) { best = Some((row.value, row.valid_from)); } } if let Some((value, _)) = best { return value; } if REST_LIMITS_HISTORY != "full" { if let Some(earliest) = earliest { if on_date < earliest { panic!( "no rest limit \"{}\" on {}: this build was installed with history={}, so it only carries limits from {}. Reinstall with history=full for older records.", rule, on_date, REST_LIMITS_HISTORY, earliest ); } } } panic!("no rest limit \"{}\" on {}", rule, on_date) } fn local_time(start_date: &str, minute: i64) -> String { let day = minute.div_euclid(1440); let in_day = minute - day * 1440; format!("{}T{:02}:{:02}", add_days(start_date, day), in_day / 60, in_day % 60) } /// Check daily and weekly rest against Regulation (EC) 561/2006, Article 8, /// as it applies in Great Britain. /// /// The records must start straight after a weekly rest, so the first 24-hour /// and six-day windows open at start_date/start_time. A daily rest is judged /// on the part of it inside the 24-hour window (Art. 8(2)): 11 hours there is /// regular, 9 to 11 is reduced, and 3 hours then 9 is a split regular rest. /// /// # Panics /// Panics on a malformed date or time, an unknown kind, a length below one /// minute, or a date before the rules applied. pub fn check_tachograph_rest(start_date: &str, start_time: &str, activities: &[DriverActivity]) -> RestCheck { if !is_iso_date(start_date) { panic!("startDate must be an ISO date (YYYY-MM-DD), received \"{}\"", start_date); } let start_minute = match parse_hh_mm(start_time) { Some(m) => m, None => panic!("startTime must be HH:MM, 24-hour, received \"{}\"", start_time), }; let window = limit_on("daily-rest-window", start_date); let regular_daily = limit_on("regular-daily-rest", start_date); let reduced_daily = limit_on("reduced-daily-rest", start_date); let split_first = limit_on("split-rest-first", start_date); let split_second = limit_on("split-rest-second", start_date); let reduced_allowed = limit_on("reduced-daily-rests-allowed", start_date); let regular_weekly = limit_on("regular-weekly-rest", start_date); let reduced_weekly = limit_on("reduced-weekly-rest", start_date); let weekly_window = limit_on("weekly-rest-window", start_date); // Rests back to back are one uninterrupted rest: (start, end). let mut rests: Vec<(i64, i64)> = Vec::new(); let mut total: i64 = 0; let mut last_was_rest = false; for activity in activities { if !KINDS.contains(&activity.kind.as_str()) { panic!("unknown activity kind \"{}\"", activity.kind); } if activity.minutes < 1 { panic!("minutes must be a whole number of at least 1, received {}", activity.minutes); } let is_rest = activity.kind == "rest"; if is_rest { if last_was_rest { if let Some(last) = rests.last_mut() { last.1 += activity.minutes; } } else { rests.push((total, total + activity.minutes)); } } last_was_rest = is_rest; total += activity.minutes; } // (at_minute, rule, actual, limit) let mut breaches: Vec<(i64, &'static str, i64, i64)> = Vec::new(); // (start, end, weekly, reduced) let mut events: Vec<(i64, i64, bool, bool)> = Vec::new(); // Daily rest: one within each 24 hours after the previous rest ended. let mut window_start: i64 = 0; loop { let window_end = window_start + window; let mut found: Option<((i64, i64), bool)> = None; let mut three_hours_taken = false; let mut longest: i64 = 0; for rest in &rests { if rest.0 < window_start || rest.0 >= window_end { continue; } let portion = rest.1.min(window_end) - rest.0; if portion > longest { longest = portion; } if portion >= reduced_daily { found = Some((*rest, portion >= regular_daily || (three_hours_taken && portion >= split_second))); break; } if portion >= split_first { three_hours_taken = true; } } if let Some((rest, regular)) = found { if rest.1 - rest.0 < reduced_weekly { events.push((rest.0, rest.1, false, !regular)); } window_start = rest.1; continue; } if window_end > total { break; // the records stop before the window closes } breaches.push((window_end, "daily-rest", longest, reduced_daily)); // Pick the count up again after the next rest long enough to be one. match rests.iter().find(|r| r.0 >= window_start && r.1 - r.0 >= reduced_daily) { Some(next) => window_start = next.1, None => break, } } for rest in &rests { let length = rest.1 - rest.0; if length >= reduced_weekly { events.push((rest.0, rest.1, true, length < regular_weekly)); } } events.sort_by(|a, b| a.0.cmp(&b.0)); let mut daily_rests = 0; let mut reduced_daily_rests = 0; let mut weekly_rests = 0; let mut reduced_weekly_rests = 0; let mut previous_weekly_end: i64 = 0; let mut previous_reduced = false; let mut reduced_since_weekly: i64 = 0; for (start, end, weekly, reduced) in &events { if !*weekly { daily_rests += 1; if *reduced { reduced_daily_rests += 1; reduced_since_weekly += 1; if reduced_since_weekly > reduced_allowed { breaches.push((*start, "reduced-daily-rest", reduced_since_weekly, reduced_allowed)); } } continue; } weekly_rests += 1; let deadline = previous_weekly_end + weekly_window; if *start > deadline { breaches.push((deadline, "weekly-rest-late", *start - previous_weekly_end, weekly_window)); } if *reduced { reduced_weekly_rests += 1; if previous_reduced { breaches.push((*start, "reduced-weekly-rest", *end - *start, regular_weekly)); } } previous_weekly_end = *end; previous_reduced = *reduced; reduced_since_weekly = 0; } // No weekly rest yet, and the records run past the deadline without ending // in a rest that began in time (and may yet become one). let deadline = previous_weekly_end + weekly_window; let ends_in_rest = rests.last().map_or(false, |r| r.1 == total && r.0 <= deadline); if total > deadline && !ends_in_rest { breaches.push((deadline, "weekly-rest-late", total - previous_weekly_end, weekly_window)); } // sort_by is stable, so breaches at the same minute keep the order found. breaches.sort_by(|a, b| a.0.cmp(&b.0)); let ordered: Vec = breaches .iter() .map(|(at, rule, actual, limit)| HoursBreach { rule: rule.to_string(), at: local_time(start_date, start_minute + at), actual: *actual, limit: *limit, }) .collect(); RestCheck { compliant: ordered.is_empty(), daily_rests, reduced_daily_rests, weekly_rests, reduced_weekly_rests, breaches: ordered, } } pub fn rest_check_to_value(check: &RestCheck) -> Value { Value::obj(vec![ ("compliant", Value::Bool(check.compliant)), ("dailyRests", Value::Int(check.daily_rests)), ("reducedDailyRests", Value::Int(check.reduced_daily_rests)), ("weeklyRests", Value::Int(check.weekly_rests)), ("reducedWeeklyRests", Value::Int(check.reduced_weekly_rests)), ("breaches", Value::Arr(check.breaches.iter().map(hours_breach_to_value).collect())), ]) } pub fn fune_vector(args: &[Value]) -> Value { let activities: Vec = args[2].as_arr().iter().map(driver_activity_from_value).collect(); rest_check_to_value(&check_tachograph_rest(args[0].as_str(), args[1].as_str(), &activities)) }