fleet.drivers-hours Unreviewed
Check a driver's activities against GB/EU drivers' hours: breaks after 4.5h, daily, weekly and fortnightly driving.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 26 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified transport compliance specialist (DVSA rules) has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates drivers’ hours figures from published rules. It is a software component for developers, not legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a transport compliance specialist (DVSA rules) review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Status: needs review by a drivers' hours / transport compliance expert before it is published.
Checks a driver's recorded activities against the driving-time and break rules of Regulation (EC) No 561/2006, as it applies in Great Britain (assimilated law, formerly "retained EU law"). It returns every breach it finds, in time order, with the local date and time the limit was passed.
For example
check_drivers_hours(2026-09-21, 06:00, activities ×4)→ compliant true, driving minutes 540, breaches a legal day: 4h30, 45 minute break, 4h30, then daily restcheck_drivers_hours(2026-09-21, 06:00, activities ×2)→ compliant false, driving minutes 300, breaches ×1 five hours without a break passes the 4h30 limit at 10:30check_drivers_hours(2026-09-21, 06:00, activities ×5)→ compliant true, driving minutes 510, breaches a split break of 15 then 30 minutes is legal
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn check_drivers_hours(start_date: &str, start_time: &str, activities: &[DriverActivity]) -> DriversHoursCheck
| start_date | date | local date the first activity starts |
| start_time | string | HH:MM, 24-hour, local time the first activity starts |
| activities | DriverActivity[] | back to back and in order: each starts when the one before ends |
| returns | DriversHoursCheck |
The types it declares, generated into your project
// ActivityKind is a string in Rust, one of: "driving", "work", "availability", "rest".
// Parameters take it as &str and results hold it as String.
/// One period of a driver's time, as a tachograph records it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DriverActivity {
/// rest covers breaks and rest periods alike
pub kind: String,
/// length in whole minutes, at least 1
pub minutes: i64,
}
/// One rule broken, and when.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HoursBreach {
/// the rule's name, e.g. break or weekly-driving
pub rule: String,
/// local date and time the limit was passed, YYYY-MM-DDTHH:MM
pub at: String,
/// what was recorded, in the rule's unit (minutes, or a count)
pub actual: i64,
/// the limit in the same unit
pub limit: i64,
}
/// The breaches found, in time order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DriversHoursCheck {
/// true when there are no breaches
pub compliant: bool,
/// all driving in the activities
pub driving_minutes: i64,
pub breaches: Vec<HoursBreach>,
}
Your code names it in one line, in the file that uses it
fune!(fleet.drivers-hours@^1); // then call check_drivers_hours(…)
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))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add fleet.drivers-hours
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./fleet.drivers-hours-1.0.2-rust.fune, or fetch it from a terminal with fune pull fleet.drivers-hours@1.0.2:rust.
The whole function, every language, is one file too: fleet.drivers-hours-1.0.2.fune, 54,669 bytes, sha256 4e3059f6a0676d4a40c653432cf048beb2de571f37281e05606dd7975e093ab8. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before fleet.drivers-hours
after — your function gets the result and the arguments, and returns the final result.
// fune: after fleet.drivers-hours
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.add-days in fleet.drivers-hours
// fune: replace dates.day-of-week in fleet.drivers-hours
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show fleet.drivers-hours --steps.
// fune: step fleet.drivers-hours after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a legal day: 4h30, 45 minute break, 4h30, then daily rest | 2026-09-21, 06:00, activities ×4 | → | compliant true, driving minutes 540, breaches |
| five hours without a break passes the 4h30 limit at 10:30 | 2026-09-21, 06:00, activities ×2 | → | compliant false, driving minutes 300, breaches ×1 |
| a split break of 15 then 30 minutes is legal | 2026-09-21, 06:00, activities ×5 | → | compliant true, driving minutes 510, breaches |
| 30 then 15 is not a split break: the second part must be at least 30 | 2026-09-21, 06:00, activities ×5 | → | compliant false, driving minutes 330, breaches ×1 |
| an hour of other work is not a break | 2026-09-21, 06:00, activities ×3 | → | compliant false, driving minutes 300, breaches ×1 |
| a period of availability is not a break | 2026-09-21, 06:00, activities ×3 | → | compliant false, driving minutes 280, breaches ×1 |
| two rests recorded back to back are one 45 minute break | 2026-09-21, 06:00, activities ×4 | → | compliant true, driving minutes 540, breaches |
| a 15 minute rest before any driving is not the first part of a split break | 2026-09-21, 06:00, activities ×4 | → | compliant false, driving minutes 300, breaches ×1 |
| 10h30 of driving in a day passes the 10 hour ceiling at 16:30 | 2026-09-21, 05:00, activities ×5 | → | compliant false, driving minutes 630, breaches ×1 |
| two 10 hour days in a week are allowed | 2026-09-21, 06:00, activities ×11 | → | compliant true, driving minutes 1,200, breaches |
Show the other 16 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a third 10 hour day in the same fixed week breaks the 9 hour limit | 2026-09-21, 06:00, activities ×17 | → | compliant false, driving minutes 1,800, breaches ×1 |
| six 9 hour days and 3 hours on Sunday pass 56 hours at 08:00 on Sunday | 2026-09-21, 06:00, activities ×25 | → | compliant false, driving minutes 3,420, breaches ×1 |
| driving across Sunday midnight is split between the two fixed weeks | 2026-09-21, 06:00, activities ×25 | → | compliant true, driving minutes 3,480, breaches |
| 54 hours then 36 hours is exactly 90; half an hour more passes the fortnightly limit | 2026-09-21, 06:00, activities ×41 | → | compliant false, driving minutes 5,430, breaches ×1 |
| breaches come back in time order | 2026-09-21, 06:00, activities ×3 | → | compliant false, driving minutes 610, breaches ×3 |
| no activities is compliant | 2026-09-21, 06:00, | → | compliant true, driving minutes 0, breaches |
| a start time of 24:00 is refused | 2026-09-21, 24:00, activities ×1 | → | error: startTime must be HH:MM, 24-hour |
| a date that is not ISO is refused | 21/09/2026, 06:00, activities ×1 | → | error: startDate must be an ISO date (YYYY-MM-DD) |
| an unknown activity kind is refused | 2026-09-21, 06:00, activities ×1 | → | error: unknown activity kind "sleeping" |
| a zero-length activity is refused | 2026-09-21, 06:00, activities ×1 | → | error: minutes must be a whole number of at least 1 |
| a fractional minute is refused | 2026-09-21, 06:00, activities ×1 | → | error: minutes must be a whole number of at least 1 |
| dates before Regulation 561/2006 applied (11 April 2007) are refused | 2007-04-10, 06:00, activities ×1 | → | error: no drivers' hours limit |
| a start date with a trailing newline is refused | 2026-09-21 , 06:00, activities ×1 | → | error: startDate must be an ISO date (YYYY-MM-DD), received "2026-09-21 " |
| a start date in Arabic-Indic digits is refused | ٢٠٢٦-٠٩-٢١, 06:00, activities ×1 | → | error: startDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-٢١" |
| a start time with a trailing newline is refused | 2026-09-21, 06:00 , activities ×1 | → | error: startTime must be HH:MM, 24-hour, received "06:00 " |
| a start time in Arabic-Indic digits is refused | 2026-09-21, 0٦:0٠, activities ×1 | → | error: startTime must be HH:MM, 24-hour, received "0٦:0٠" |
More from the author
checkDriversHours("2026-09-21", "06:00", [
{ kind: "driving", minutes: 300 }, { kind: "rest", minutes: 45 }
])
// { compliant: false, drivingMinutes: 300,
// breaches: [{ rule: "break", at: "2026-09-21T10:30", actual: 300, limit: 270 }] }## Input
The activities are back to back from `startDate` and `startTime`: each one starts when the previous one ends, as on a tachograph printout. `kind` is `driving`, `work` (other work), `availability` (a period of availability) or `rest` (any break or rest). Neighbouring activities of the same kind are merged, so a rest recorded as 20 minutes then 25 minutes is one uninterrupted 45-minute break.
Times are local wall-clock time with no time zone or clock change handling.
## Rules checked
| rule | limit | meaning | |------|-------|---------| | `break` | 270 minutes | Art. 7: more than 4h30 of driving without a qualifying break. `actual` is the driving in that driving period, up to the break that ended it | | `daily-driving` | 600 minutes | Art. 6(1): daily driving time over 10 hours | | `daily-driving` | 540 minutes | Art. 6(1): a third (or later) day over 9 hours in one fixed week; `at` is when that day passed 9 hours | | `weekly-driving` | 3360 minutes | Art. 6(2): over 56 hours in one fixed week | | `fortnightly-driving` | 5400 minutes | Art. 6(3): over 90 hours in two consecutive fixed weeks; `at` falls in the second week |
How the activities are read:
- **A break** is a rest of at least 45 minutes, or a rest of at least 15 minutes followed later by one of at least 30 (in that order: 30 then 15 does not count). Other work and periods of availability are not breaks. A 15-minute rest before any driving in the period is not the first part of a split break, since it is not taken during the driving period. - **A daily rest** is a rest of 9 hours or more. It ends the daily driving time (Art. 4(k)); a shorter rest, including the 3-hour first part of a split daily rest, does not. - **Weeks** are fixed weeks, 00:00 Monday to 24:00 Sunday (Art. 4(i)). Driving that crosses Sunday midnight counts in both weeks, minute by minute. A day's 10-hour extension counts in the week its first driving falls in. - **The end of the records** closes the last driving period and day, so records that stop mid-day are judged on what they show.
Whether daily and weekly *rest* was taken is a separate check: `fleet.tachograph-rest`.
## What it does not do
- GB domestic drivers' hours rules (Transport Act 1968), AETR, or mixed domestic/EU days. - Multi-manning, ferry or train crossings (Art. 9), the Art. 12 emergency departure, and the exemptions and national derogations (Art. 3 and 13). - The working-time limits (Road Transport (Working Time) Regulations 2005), including the 6-hour work break rule. - Weeks outside the records: the fortnightly check only adds up weeks it has data for, so pass the previous week's activities too if they matter.
## Data
The limits are dated rows in `data/limits.json`, effective from 11 April 2007 (Art. 29) and still in force. A date before that is an error.
## Sources
- Regulation (EC) No 561/2006, as it applies in the UK, legislation.gov.uk: Article 4 (definitions of break, rest, daily rest, week, daily driving time, driving period), Article 6 (9h/10h twice a week, 56h, 90h), Article 7 (45 minutes after 4h30; 15 then 30), Article 29 (entry into force 11 April 2007). https://www.legislation.gov.uk/eur/2006/561/contents - DVSA, "Drivers' hours: Assimilated rules": https://www.gov.uk/drivers-hours/eu-rules - DVSA, "Drivers' hours and tachographs: goods vehicles", section 1 (split break order 15 then 30, other work and availability are not breaks, fixed week): https://www.gov.uk/guidance/drivers-hours-goods-vehicles/1-eu-and-aetr-rules-on-drivers-hours
1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in startDate and startTime; adds tests.
## Before you rely on this
**Not professional advice.** This capability calculates drivers’ hours figures from published rules. It is a software component for developers, not legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a transport compliance specialist (DVSA rules) review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified transport compliance specialist (DVSA rules) has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
## Notices
Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
Legislation: Crown copyright and database right.
1.0.2 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 232 |
| README.md | 5,755 |
| data/limits.json | 1,581 |
| impl/python.py | 8,954 |
| impl/rust.rs | 12,435 |
| impl/typescript.ts | 8,414 |
| vectors.json | 10,305 |