Functional Weave
Code in Rust

fleet.tachograph-rest

Check daily and weekly rest against GB/EU drivers' hours: 11h daily (9h reduced, 3+9 split), 45h weekly (24h reduced).

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

Pinned by 22 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 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 for daily and weekly rest under Article 8 of Regulation (EC) No 561/2006, as it applies in Great Britain (assimilated law). It counts the daily and weekly rests taken and returns every breach in time order. Driving and break limits are checked by `fleet.drivers-hours`, which takes exactly the same input and whose `DriverActivity` and `HoursBreach` types this capability uses.

For example

  • check_tachograph_rest(2026-09-21, 06:00, activities ×5) → compliant true, daily rests 1, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches an 11 hour rest after the day's work is a regular daily rest
  • check_tachograph_rest(2026-09-21, 06:00, activities ×5) → compliant true, daily rests 1, reduced daily rests 1, weekly rests 0, reduced weekly rests 0, breaches a 10 hour rest is a reduced daily rest
  • check_tachograph_rest(2026-09-21, 06:00, activities ×5) → compliant true, daily rests 1, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches 3 hours then 9 hours is a split regular daily rest

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_tachograph_rest(start_date: &str, start_time: &str, activities: &[DriverActivity]) -> RestCheck
start_datedatelocal date the first activity starts, straight after a weekly rest
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
returnsRestCheck

The type it declares, generated into your project

/// The rests found, and the breaches in time order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestCheck {
    /// true when there are no breaches
    pub compliant: bool,
    /// daily rests taken in time, not counting weekly rests
    pub daily_rests: i64,
    /// of those, how many were reduced (9 to 11 hours)
    pub reduced_daily_rests: i64,
    /// rests of 24 hours or more
    pub weekly_rests: i64,
    /// of those, how many were under 45 hours
    pub reduced_weekly_rests: i64,
    pub breaches: Vec<HoursBreach>,
}

Your code names it in one line, in the file that uses it

fune!(fleet.tachograph-rest@^1);  // then call check_tachograph_rest(…)
impl/rust.rs · 264 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::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))
}

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.tachograph-rest
Download for Rust fleet.tachograph-rest-1.0.1-rust.fune · 33,160 bytes sha256 98a5685964c49ee47b49272f1706d841a10c5136b97b0edfa08f7300631babe1

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

The whole function, every language, is one file too: fleet.tachograph-rest-1.0.1.fune, 49,264 bytes, sha256 02583a23e07d64e164b157cd17e1b450d5ecac28ecc7695202ba1bb5144cc86b. 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.tachograph-rest

after — your function gets the result and the arguments, and returns the final result.

// fune: after fleet.tachograph-rest

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.tachograph-rest
// fune: replace fleet.drivers-hours in fleet.tachograph-rest

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.tachograph-rest --steps.

// fune: step fleet.tachograph-rest 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
an 11 hour rest after the day's work is a regular daily rest 2026-09-21, 06:00, activities ×5 → compliant true, daily rests 1, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches
a 10 hour rest is a reduced daily rest 2026-09-21, 06:00, activities ×5 → compliant true, daily rests 1, reduced daily rests 1, weekly rests 0, reduced weekly rests 0, breaches
3 hours then 9 hours is a split regular daily rest 2026-09-21, 06:00, activities ×5 → compliant true, daily rests 1, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches
9 hours then 3 hours is not a split rest: the 9 hours is a reduced daily rest 2026-09-21, 06:00, activities ×5 → compliant true, daily rests 1, reduced daily rests 1, weekly rests 0, reduced weekly rests 0, breaches
an 11 hour rest after a 14 hour day has only 10 hours inside the 24, so it is reduced 2026-09-21, 06:00, activities ×3 → compliant true, daily rests 1, reduced daily rests 1, weekly rests 0, reduced weekly rests 0, breaches
no rest of 9 hours inside the first 24 hours is a daily rest breach at the end of them 2026-09-21, 06:00, activities ×9 → compliant false, daily rests 0, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches ×1
a fourth reduced daily rest before a weekly rest is one too many 2026-09-21, 06:00, activities ×25 → compliant false, daily rests 4, reduced daily rests 4, weekly rests 0, reduced weekly rests 0, breaches ×1
five days then a 45 hour weekly rest on the sixth is compliant 2026-09-21, 06:00, activities ×25 → compliant true, daily rests 5, reduced daily rests 0, weekly rests 1, reduced weekly rests 0, breaches
a weekly rest starting after six 24-hour periods is late 2026-09-21, 06:00, activities ×29 → compliant false, daily rests 6, reduced daily rests 0, weekly rests 1, reduced weekly rests 0, breaches ×1
two reduced weekly rests in a row 2026-09-21, 06:00, activities ×49 → compliant false, daily rests 10, reduced daily rests 0, weekly rests 2, reduced weekly rests 2, breaches ×1
Show the other 12 tests
CaseArgumentsExpected
seven days without a weekly rest: late from the end of the sixth 24 hours 2026-09-21, 06:00, activities ×29 → compliant false, daily rests 7, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches ×1
records that stop before the first 24 hours are up are not a breach 2026-09-21, 06:00, activities ×3 → compliant true, daily rests 0, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches
no activities is compliant 2026-09-21, 06:00, → compliant true, daily rests 0, reduced daily rests 0, weekly rests 0, reduced weekly rests 0, breaches
a start time of 7:00 without a leading zero is refused 2026-09-21, 7:00, activities ×1 → error: startTime must be HH:MM, 24-hour
an unknown activity kind is refused 2026-09-21, 06:00, activities ×1 → error: unknown activity kind "sleep"
a fractional minute is refused 2026-09-21, 06:00, activities ×1 → error: minutes must be a whole number of at least 1
a negative length 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 are refused 2006-12-31, 06:00, activities ×1 → error: no rest limit
a start date with a trailing newline is refused 2026-09-21 , 06:00, activities ×1 → error: startDate must be an ISO date
a start date in Arabic-Indic digits is refused ٢٠٢٦-09-21, 06:00, activities ×1 → error: startDate must be an ISO date
a start time with a trailing newline is refused 2026-09-21, 06:00 , activities ×1 → error: startTime must be HH:MM, 24-hour
a start time in Arabic-Indic digits is refused 2026-09-21, 0٦:3٠, activities ×1 → error: startTime must be HH:MM, 24-hour

More from the author

## Input

`startDate` and `startTime` are when the first activity starts, and **the records must begin straight after a weekly rest**: that is when the first 24-hour daily rest window and the six-day weekly rest window open, and the weekly rest before it is taken to be a regular one. Activities are back to back; neighbouring rests are merged into one uninterrupted rest. Local wall-clock time, no time zones.

## How rests are judged

- **Daily rest, Art. 8(2).** Within each 24 hours after the end of the previous daily or weekly rest, the driver must have taken a new daily rest. Only the part of a rest inside that 24-hour window counts: 11 hours or more inside it is a regular daily rest, 9 to 11 hours is reduced, so an 11-hour rest that starts 14 hours into the window is a reduced one. A rest of at least 3 hours earlier in the same window followed by one with 9 hours inside it is a split regular rest (Art. 4(g)); 9 then 3 is a reduced rest followed by a break. The next window opens when the qualifying rest ends. If no rest of 9 hours starts in the window, that is a `daily-rest` breach at the window's end, and checking picks up again after the next rest of 9 hours or more. - **Reduced daily rests, Art. 8(4).** At most three between two weekly rests; the fourth and later are `reduced-daily-rest` breaches. - **Weekly rest, Art. 8(6).** Any rest of 24 hours or more is a weekly rest, regular at 45 hours or more, otherwise reduced. It must start no later than six 24-hour periods (144 hours) after the previous weekly rest ended, or it is `weekly-rest-late`. If the records run past that point with no weekly rest at all, the breach is reported at the deadline, unless the records end in a rest that began in time and may yet become one. - **Two reduced weekly rests in a row** are a `reduced-weekly-rest` breach. This is a simplification of Art. 8(6) ("in any two consecutive weeks ... two regular weekly rest periods, or one regular and one reduced").

| rule | `actual` | `limit` | |------|----------|---------| | `daily-rest` | longest rest inside the window, minutes | 540 | | `reduced-daily-rest` | reduced daily rests since the last weekly rest | 3 | | `weekly-rest-late` | minutes from the previous weekly rest to the next one (or to the end of the records) | 8640 | | `reduced-weekly-rest` | length of the second reduced weekly rest, minutes | 2700 |

A rest cut off by the end of the records is judged at the length recorded.

## What it does not do

- Compensation for a reduced weekly rest (Art. 8(6), 8(7)): the lost hours must be made up, attached to another rest of 9 hours or more, before the end of the third week. Not checked. - The 2020 amendments for international goods drivers (two consecutive reduced weekly rests outside the member state, four-week pattern; return home every four weeks; no regular weekly rest in the cab). Not checked. - Multi-manning (9 hours within 30), ferry and train interruptions (Art. 9), the 12-day derogation for occasional passenger services, and the exemptions and national derogations. - GB domestic rules (Transport Act 1968) and AETR.

## Data

The limits are dated rows in `data/rest-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(g) and (h) (regular, reduced and split daily rest; regular and reduced weekly rest), Article 8(2) (a new daily rest within each 24 hours; the portion inside the 24 hours decides whether it is reduced), 8(4) (at most three reduced daily rests between two weekly rests), 8(6) (two consecutive weeks; weekly rest to start within six 24-hour periods). https://www.legislation.gov.uk/eur/2006/561/article/8 and https://www.legislation.gov.uk/eur/2006/561/article/4 - DVSA, "Drivers' hours: Assimilated rules" (11 hours, reducible to 9 three times between weekly rests; 45 hours weekly, reducible to 24 every other week; weekly rest after six consecutive 24-hour periods): https://www.gov.uk/drivers-hours/eu-rules - DVSA, "Drivers' hours and tachographs: goods vehicles", section 1.5 (the 24-hour period runs from the end of the previous rest; split rest 3 hours then 9): 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.md5,041
data/rest-limits.json1,572
impl/python.py7,685
impl/rust.rs10,168
impl/typescript.ts7,888
vectors.json10,896