Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::fleet_drivers_hours::{driver_activity_from_value, hours_breach_to_value, DriverActivity, HoursBreach}; ← from fleet.drivers-hours ^1.0.0 · built alongside by fune
use super::fleet_tachograph_rest_data::{REST_LIMITS, REST_LIMITS_HISTORY}; ← this capability’s own data, compiled from data/rest-limits.json into the same file by fune build
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<i64> {
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<HoursBreach> = 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<DriverActivity> = 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))
}