Functional Weave
Code in Rust

fleet.drivers-hours

Check a driver's activities against GB/EU drivers' hours: breaks after 4.5h, daily, weekly and fortnightly driving.

1.0.1 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 26 tests, run in TypeScript, Python and Rust.

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 rest
  • check_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:30
  • check_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_datedatelocal date the first activity starts
start_timestringHH:MM, 24-hour, local time the first activity starts
activitiesDriverActivity[]back to back and in order: each starts when the one before ends
returnsDriversHoursCheck

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(…)
impl/rust.rs · 298 lines · open · raw

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
Download for Rust fleet.drivers-hours-1.0.1-rust.fune · 34,939 bytes sha256 af2c5d6f2194e8751f301ab614804d06c5515876ff4a69a9ee9352719fcdc856

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./fleet.drivers-hours-1.0.1-rust.fune, or fetch it from a terminal with fune pull fleet.drivers-hours@1.0.1:rust.

The whole function, every language, is one file too: fleet.drivers-hours-1.0.1.fune, 52,953 bytes, sha256 382070925b09f173e31ff7c9f0e3b74a49f5ae30a02fe2b70b5db2234858199d. 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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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.

Files

PathBytes
README.md4,340
data/limits.json1,581
impl/python.py8,954
impl/rust.rs12,435
impl/typescript.ts8,414
vectors.json10,305