Functional Weave
Code in Python

fleet.tachograph-rest Unreviewed

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

1.0.2 · published 2026-10-03 by charlie · Anterra

Pinned by 22 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 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 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.

def check_tachograph_rest(start_date: str, start_time: str, activities: Sequence[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

@dataclass(frozen=True)
class RestCheck:
    """The rests found, and the breaches in time order."""

    #: true when there are no breaches
    compliant: bool
    #: daily rests taken in time, not counting weekly rests
    daily_rests: int
    #: of those, how many were reduced (9 to 11 hours)
    reduced_daily_rests: int
    #: rests of 24 hours or more
    weekly_rests: int
    #: of those, how many were under 45 hours
    reduced_weekly_rests: int
    breaches: List[HoursBreach]

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

from fune.fleet.tachograph_rest import check_tachograph_rest  # fleet.tachograph-rest@^1
impl/python.py · 175 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.

import re
from typing import List, Optional, Sequence

from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .fleet_drivers_hours import DriverActivity, HoursBreach  ← from fleet.drivers-hours ^1.0.0 · built alongside by fune
from .fleet_tachograph_rest_data import REST_LIMITS, REST_LIMITS_HISTORY  ← this capability’s own data, compiled from data/rest-limits.json into the same file by fune build
from .fleet_tachograph_rest_types import RestCheck

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")
HH_MM = re.compile(r"([01][0-9]|2[0-3]):[0-5][0-9]")
KINDS = ("driving", "work", "availability", "rest")


def _limit_on(rule: str, on_date: str) -> int:
    best = None
    earliest = None
    for row in REST_LIMITS:
        if row.rule != rule:
            continue
        if earliest is None or row.valid_from < earliest:
            earliest = row.valid_from
        if on_date < row.valid_from:
            continue
        if row.valid_to is not None and on_date > row.valid_to:
            continue
        if best is None or row.valid_from > best.valid_from:
            best = row
    if best is not None:
        return best.value
    if REST_LIMITS_HISTORY != "full" and earliest is not None and on_date < earliest:
        raise ValueError(
            'no rest limit "%s" on %s: this build was installed with history=%s, '
            "so it only carries limits from %s. Reinstall with history=full for older records."
            % (rule, on_date, REST_LIMITS_HISTORY, earliest)
        )
    raise ValueError('no rest limit "%s" on %s' % (rule, on_date))


def _local_time(start_date: str, minute: int) -> str:
    day = minute // 1440
    in_day = minute - day * 1440
    return "%sT%02d:%02d" % (add_days(start_date, day), in_day // 60, in_day % 60)


def check_tachograph_rest(start_date: str, start_time: str, activities: Sequence[DriverActivity]) -> RestCheck:
    """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.
    """
    if not isinstance(start_date, str) or not ISO_DATE.fullmatch(start_date):
        raise ValueError('startDate must be an ISO date (YYYY-MM-DD), received "%s"' % (start_date,))
    if not isinstance(start_time, str) or not HH_MM.fullmatch(start_time):
        raise ValueError('startTime must be HH:MM, 24-hour, received "%s"' % (start_time,))
    window = _limit_on("daily-rest-window", start_date)
    regular_daily = _limit_on("regular-daily-rest", start_date)
    reduced_daily = _limit_on("reduced-daily-rest", start_date)
    split_first = _limit_on("split-rest-first", start_date)
    split_second = _limit_on("split-rest-second", start_date)
    reduced_allowed = _limit_on("reduced-daily-rests-allowed", start_date)
    regular_weekly = _limit_on("regular-weekly-rest", start_date)
    reduced_weekly = _limit_on("reduced-weekly-rest", start_date)
    weekly_window = _limit_on("weekly-rest-window", start_date)

    # Rests back to back are one uninterrupted rest: [start, end] pairs.
    rests: List[List[int]] = []
    total = 0
    last_kind: Optional[str] = None
    for activity in activities:
        if activity.kind not in KINDS:
            raise ValueError('unknown activity kind "%s"' % (activity.kind,))
        minutes = activity.minutes
        if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 1:
            raise ValueError("minutes must be a whole number of at least 1, received %s" % (minutes,))
        if activity.kind == "rest":
            if last_kind == "rest":
                rests[-1][1] += minutes
            else:
                rests.append([total, total + minutes])
        last_kind = activity.kind
        total += minutes

    breaches: List[tuple] = []  # (at_minute, rule, actual, limit)
    events: List[tuple] = []  # (start, end, weekly, reduced)

    # Daily rest: one within each 24 hours after the previous rest ended.
    window_start = 0
    while True:
        window_end = window_start + window
        found = None
        three_hours_taken = False
        longest = 0
        for rest in rests:
            if rest[0] < window_start or rest[0] >= window_end:
                continue
            portion = min(rest[1], window_end) - rest[0]
            if portion > longest:
                longest = portion
            if portion >= reduced_daily:
                found = (rest, portion >= regular_daily or (three_hours_taken and portion >= split_second))
                break
            if portion >= split_first:
                three_hours_taken = True
        if found is not None:
            rest, regular = found
            if rest[1] - rest[0] < reduced_weekly:
                events.append((rest[0], rest[1], False, not regular))
            window_start = rest[1]
            continue
        if window_end > total:
            break  # the records stop before the window closes
        breaches.append((window_end, "daily-rest", longest, reduced_daily))
        # Pick the count up again after the next rest long enough to be one.
        following = [r for r in rests if r[0] >= window_start and r[1] - r[0] >= reduced_daily]
        if not following:
            break
        window_start = following[0][1]

    for rest in rests:
        length = rest[1] - rest[0]
        if length >= reduced_weekly:
            events.append((rest[0], rest[1], True, length < regular_weekly))
    events.sort(key=lambda e: e[0])

    daily_rests = 0
    reduced_daily_rests = 0
    weekly_rests = 0
    reduced_weekly_rests = 0
    previous_weekly_end = 0
    previous_reduced = False
    reduced_since_weekly = 0
    for start, end, weekly, reduced in events:
        if not weekly:
            daily_rests += 1
            if reduced:
                reduced_daily_rests += 1
                reduced_since_weekly += 1
                if reduced_since_weekly > reduced_allowed:
                    breaches.append((start, "reduced-daily-rest", reduced_since_weekly, reduced_allowed))
            continue
        weekly_rests += 1
        deadline = previous_weekly_end + weekly_window
        if start > deadline:
            breaches.append((deadline, "weekly-rest-late", start - previous_weekly_end, weekly_window))
        if reduced:
            reduced_weekly_rests += 1
            if previous_reduced:
                breaches.append((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).
    deadline = previous_weekly_end + weekly_window
    ends_in_rest = bool(rests) and rests[-1][1] == total and rests[-1][0] <= deadline
    if total > deadline and not ends_in_rest:
        breaches.append((deadline, "weekly-rest-late", total - previous_weekly_end, weekly_window))

    start_minute = int(start_time[0:2]) * 60 + int(start_time[3:5])
    # sorted() is stable, so breaches at the same minute keep the order found.
    ordered = [
        HoursBreach(rule=rule, at=_local_time(start_date, start_minute + at), actual=actual, limit=limit)
        for at, rule, actual, limit in sorted(breaches, key=lambda b: b[0])
    ]
    return RestCheck(
        compliant=len(ordered) == 0,
        daily_rests=daily_rests,
        reduced_daily_rests=reduced_daily_rests,
        weekly_rests=weekly_rests,
        reduced_weekly_rests=reduced_weekly_rests,
        breaches=ordered,
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python 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. Or pin a range in fune.project and build in one step:

fune add fleet.tachograph-rest
Download for Python fleet.tachograph-rest-1.0.2-python.fune · 32,266 bytes sha256 999260bfc6bbaf4e89685f30126cebc93a8a815e5e2cda9f88f56cc996e07f47

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

The whole function, every language, is one file too: fleet.tachograph-rest-1.0.2.fune, 50,956 bytes, sha256 19cdd744983e4308d0f5046caeb2179a4538315474774285c3a3eec012f54b99. 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.

## 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 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 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

PathBytes
NOTICE234
README.md6,430
data/rest-limits.json1,572
impl/python.py7,685
impl/rust.rs10,168
impl/typescript.ts7,888
vectors.json10,896