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::dates_day_of_week::day_of_week; ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::fleet_drivers_hours_data::{DRIVING_LIMITS, DRIVING_LIMITS_HISTORY}; ← this capability’s own data, compiled from data/limits.json into the same file by fune build
const KINDS: [&str; 4] = ["driving", "work", "availability", "rest"];
const WEEK: i64 = 10080;
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 DRIVING_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 DRIVING_LIMITS_HISTORY != "full" {
if let Some(earliest) = earliest {
if on_date < earliest {
panic!(
"no drivers' hours limit \"{}\" on {}: this build was installed with history={}, so it only carries limits from {}. Reinstall with history=full for older records.",
rule, on_date, DRIVING_LIMITS_HISTORY, earliest
);
}
}
}
panic!("no drivers' hours 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)
}
struct Pending {
rule: &'static str,
at_minute: i64,
actual: i64,
limit: i64,
}
/// Check a run of tachograph activities against the GB/EU drivers' hours
/// rules on driving time and breaks (Regulation (EC) 561/2006, Articles 6
/// and 7).
///
/// Activities are back to back from start_date/start_time, in local time. A
/// rest of 45 minutes, or 15 then 30, ends a driving period; a rest of 9 hours
/// or more is a daily rest and ends the daily driving time. Weeks are fixed
/// weeks, Monday 00:00 to Sunday 24:00, so driving across Sunday midnight is
/// split.
///
/// # 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_drivers_hours(start_date: &str, start_time: &str, activities: &[DriverActivity]) -> DriversHoursCheck {
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 driving_period = limit_on("driving-period", start_date);
let full_break = limit_on("break", start_date);
let split_first = limit_on("split-break-first", start_date);
let split_second = limit_on("split-break-second", start_date);
let daily = limit_on("daily-driving", start_date);
let daily_extended = limit_on("daily-driving-extended", start_date);
let extensions_allowed = limit_on("extended-days-per-week", start_date);
let weekly_limit = limit_on("weekly-driving", start_date);
let fortnight_limit = limit_on("fortnightly-driving", start_date);
let daily_rest = limit_on("daily-rest-minimum", start_date);
// Merge neighbours of the same kind: two rests back to back are one
// uninterrupted rest, whatever the recording split them into.
let mut segments: Vec<(String, i64)> = Vec::new();
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);
}
match segments.last_mut() {
Some(last) if last.0 == activity.kind => last.1 += activity.minutes,
_ => segments.push((activity.kind.clone(), activity.minutes)),
}
}
let week_base = (day_of_week(start_date) - 1) * 1440 + start_minute;
let mut breaches: Vec<Pending> = Vec::new();
let mut accumulated: i64 = 0;
let mut first_part = false;
let mut break_breach: Option<usize> = None;
let mut day_driving: i64 = 0;
let mut day_passed_daily: Option<i64> = None;
let mut day_week: Option<i64> = None;
let mut day_breach: Option<usize> = None;
let mut extensions: Vec<(i64, i64)> = Vec::new();
let mut weekly: Vec<(i64, i64)> = Vec::new();
let mut weekly_breaches: Vec<(i64, usize)> = Vec::new();
let mut fortnight_breaches: Vec<(i64, usize)> = Vec::new();
fn get(map: &[(i64, i64)], key: i64) -> i64 {
map.iter().find(|(k, _)| *k == key).map_or(0, |(_, v)| *v)
}
fn set(map: &mut Vec<(i64, i64)>, key: i64, value: i64) {
match map.iter_mut().find(|(k, _)| *k == key) {
Some(entry) => entry.1 = value,
None => map.push((key, value)),
}
}
let mut total_driving: i64 = 0;
let mut offset: i64 = 0;
let segment_count = segments.len();
for index in 0..=segment_count {
// One pass past the end closes the open driving period and day.
let (kind, minutes) = if index < segment_count {
(segments[index].0.as_str(), segments[index].1)
} else {
("end", 0)
};
if kind == "driving" {
total_driving += minutes;
let mut start = offset;
let end = offset + minutes;
while start < end {
let week = (week_base + start).div_euclid(WEEK);
let chunk_end = end.min((week + 1) * WEEK - week_base);
let m = chunk_end - start;
if break_breach.is_none() && accumulated + m > driving_period {
breaches.push(Pending { rule: "break", at_minute: start + (driving_period - accumulated), actual: 0, limit: driving_period });
break_breach = Some(breaches.len() - 1);
}
accumulated += m;
if day_week.is_none() {
day_week = Some(week);
}
if day_passed_daily.is_none() && day_driving + m > daily {
day_passed_daily = Some(start + (daily - day_driving));
}
if day_breach.is_none() && day_driving + m > daily_extended {
breaches.push(Pending { rule: "daily-driving", at_minute: start + (daily_extended - day_driving), actual: 0, limit: daily_extended });
day_breach = Some(breaches.len() - 1);
}
day_driving += m;
let before = get(&weekly, week);
if !weekly_breaches.iter().any(|(w, _)| *w == week) && before + m > weekly_limit {
breaches.push(Pending { rule: "weekly-driving", at_minute: start + (weekly_limit - before), actual: 0, limit: weekly_limit });
weekly_breaches.push((week, breaches.len() - 1));
}
let pair_before = get(&weekly, week - 1) + before;
if !fortnight_breaches.iter().any(|(w, _)| *w == week) && pair_before + m > fortnight_limit {
breaches.push(Pending { rule: "fortnightly-driving", at_minute: start + (fortnight_limit - pair_before), actual: 0, limit: fortnight_limit });
fortnight_breaches.push((week, breaches.len() - 1));
}
set(&mut weekly, week, before + m);
start = chunk_end;
}
} else if kind == "rest" || kind == "end" {
let is_end = kind == "end";
if is_end || minutes >= full_break || (first_part && minutes >= split_second) {
if let Some(i) = break_breach {
breaches[i].actual = accumulated;
}
accumulated = 0;
first_part = false;
break_breach = None;
} else if minutes >= split_first && accumulated > 0 {
first_part = true;
}
if is_end || minutes >= daily_rest {
if day_driving > daily {
if let Some(week) = day_week {
let used = get(&extensions, week) + 1;
set(&mut extensions, week, used);
if used > extensions_allowed && day_driving <= daily_extended {
if let Some(at) = day_passed_daily {
breaches.push(Pending { rule: "daily-driving", at_minute: at, actual: day_driving, limit: daily });
}
}
}
}
if let Some(i) = day_breach {
breaches[i].actual = day_driving;
}
day_driving = 0;
day_passed_daily = None;
day_week = None;
day_breach = None;
}
}
offset += minutes;
}
for (week, i) in &weekly_breaches {
breaches[*i].actual = get(&weekly, *week);
}
for (week, i) in &fortnight_breaches {
breaches[*i].actual = get(&weekly, *week - 1) + get(&weekly, *week);
}
// sort_by is stable, so breaches at the same minute keep the order found.
breaches.sort_by(|a, b| a.at_minute.cmp(&b.at_minute));
let ordered: Vec<HoursBreach> = breaches
.iter()
.map(|b| HoursBreach {
rule: b.rule.to_string(),
at: local_time(start_date, start_minute + b.at_minute),
actual: b.actual,
limit: b.limit,
})
.collect();
DriversHoursCheck { compliant: ordered.is_empty(), driving_minutes: total_driving, breaches: ordered }
}
/// Read one activity from vector JSON, refusing a length Rust's i64 cannot hold.
pub fn driver_activity_from_value(value: &Value) -> DriverActivity {
let minutes = value.get("minutes");
let whole = match minutes {
Value::Int(i) => *i,
Value::Float(f) => panic!("minutes must be a whole number of at least 1, received {}", f),
_ => panic!("minutes must be a whole number of at least 1, received a non-number"),
};
let kind = match value.get("kind") {
Value::Str(s) => s.clone(),
_ => panic!("unknown activity kind \"\""),
};
DriverActivity { kind, minutes: whole }
}
pub fn hours_breach_to_value(breach: &HoursBreach) -> Value {
Value::obj(vec![
("rule", Value::str(&breach.rule)),
("at", Value::str(&breach.at)),
("actual", Value::Int(breach.actual)),
("limit", Value::Int(breach.limit)),
])
}
pub fn drivers_hours_check_to_value(check: &DriversHoursCheck) -> Value {
Value::obj(vec![
("compliant", Value::Bool(check.compliant)),
("drivingMinutes", Value::Int(check.driving_minutes)),
("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();
drivers_hours_check_to_value(&check_drivers_hours(args[0].as_str(), args[1].as_str(), &activities))
}