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.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 18 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 restcheck_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 restcheck_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_date | date | local date the first activity starts, straight after a weekly rest |
| 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 | RestCheck |
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
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"^\d{4}-\d{2}-\d{2}$")
HH_MM = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$")
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.match(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.match(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
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./fleet.tachograph-rest-1.0.0-python.fune, or fetch it from a terminal with fune pull fleet.tachograph-rest@1.0.0:python.
The whole function, every language, is one file too: fleet.tachograph-rest-1.0.0.fune, 48,311 bytes, sha256 b7d68bfee5fe23e7320df4dd4cecfa051c1f33320d6ad24f44fc5ad963348769. 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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 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 |
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
Files
| Path | Bytes |
|---|---|
| README.md | 4,932 |
| data/rest-limits.json | 1,572 |
| impl/python.py | 7,666 |
| impl/rust.rs | 10,168 |
| impl/typescript.ts | 7,888 |
| vectors.json | 10,154 |