Payroll and overtime
Split the time on a timesheet into contracted hours and overtime by counting the working time inside each clock-in and clock-out span with between.
An agent's timesheet lists when they clocked in and out. How many of those hours fell inside their contracted hours, and how many are overtime? This guide counts both with between, in Rust, Python and JavaScript.
The schedule
Agents on the London support desk are contracted for the desk's working hours. The schedule is the contract, in the Europe/London time zone:
WorkDay: 09:00–17:00, Monday to Friday.Break: 12:00–13:00, Monday to Friday, an unpaid lunch hour.Holidays: Friday 25 and Monday 28 December 2026.
The agent's timesheet, in London time. They clock out for lunch:
| Date | Clock in | Clock out |
|---|---|---|
| Monday 28 December 2026, a holiday | 10:00 | 14:00 |
| Tuesday 29 December 2026 | 08:30 | 12:00 |
| Tuesday 29 December 2026 | 13:00 | 18:15 |
For each row, between(clock_in, clock_out) on the contract is the contracted time in the span. The rest of the span, its elapsed time minus the contracted time, is overtime.
Count contracted hours and overtime
use jiff::civil::{date, time};
use jiff::{SignedDuration, Zoned};
use workclock::{Break, Holidays, Schedule, Weekdays, WorkDay};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let days = Weekdays::MON_TO_FRI;
let contract = Schedule::builder()
.time_zone("Europe/London")
.rule(WorkDay::new(time(9, 0, 0, 0), time(17, 0, 0, 0), days))
.rule(Break::new(time(12, 0, 0, 0), time(13, 0, 0, 0), days))
.rule(Holidays::new([date(2026, 12, 25), date(2026, 12, 28)]))
.build()?;
let timesheet = [
("2026-12-28T10:00", "2026-12-28T14:00"), // Monday, a holiday
("2026-12-29T08:30", "2026-12-29T12:00"), // Tuesday morning
("2026-12-29T13:00", "2026-12-29T18:15"), // Tuesday afternoon
];
let mut paid = SignedDuration::ZERO;
let mut overtime = SignedDuration::ZERO;
for (start, end) in timesheet {
let clock_in: Zoned = format!("{start}[Europe/London]").parse()?;
let clock_out: Zoned = format!("{end}[Europe/London]").parse()?;
let contracted = contract.between(&clock_in, &clock_out);
paid += contracted;
overtime += clock_in.duration_until(&clock_out) - contracted;
}
assert_eq!(paid, SignedDuration::from_hours(7));
assert_eq!(overtime, SignedDuration::from_mins(345));
Ok(())
}The result
The agent worked 12 hours 45 minutes: 7 hours of contracted time and 5 hours 45 minutes of overtime.
Span, Europe/London | On the clock | Contracted | Overtime |
|---|---|---|---|
| Monday 28 December, 10:00–14:00 | 4 hours | None: a holiday | 4 hours |
| Tuesday 29 December, 08:30–12:00 | 3 hours 30 minutes | 09:00–12:00, 3 hours | 30 minutes |
| Tuesday 29 December, 13:00–18:15 | 5 hours 15 minutes | 13:00–17:00, 4 hours | 1 hour 15 minutes |
| Total | 12 hours 45 minutes | 7 hours | 5 hours 45 minutes |
A holiday or a weekend day has no contracted time, so every hour worked on it is overtime without a separate rule. This page has no week strip: the strip draws the instant add reaches, and cannot show a span's time outside working hours.
Edge cases
Warning
In Python, subtracting two datetimes with the same tzinfo gives the difference in wall-clock time, not elapsed time. Across a daylight saving time (DST) change, a 22:00–06:00 night in America/New_York then comes out as 8 hours instead of 7, and the overtime is an hour off. Convert both datetimes to UTC before subtracting, as the Python tab does.
- Staying clocked in through lunch. A span that covers the 12:00–13:00 break counts the break as overtime, because the break is not contracted time. A single span from 08:30 to 18:15 on Tuesday has 7 hours of contracted time and 2 hours 45 minutes of overtime, including the lunch hour. Record the break as a clock-out, or subtract it.
- Night shifts. A contract with a 22:00–06:00
WorkDaycounts across midnight, and the shift belongs to the weekday it starts on (Overnight shifts). - Holidays. Each date in
Holidaysremoves 00:00 to the next 00:00 in the schedule's time zone (Rules: Holidays). - DST.
betweenreturns elapsed real time, so a night shift across a clock change is an hour shorter or longer (Time zones and DST).
Not in 0.1.1: contracts with different hours on one date, such as a half day on Christmas Eve. Working hours repeat by weekday, with no per-date overrides, and results are durations, not business days (DESIGN.md: Scope).
See also
- Measuring time: what
betweencounts, and its sign. - Rules:
WorkDay,BreakandHolidays. - Batch operations:
betweenfor many spans in one call. - Inputs and precision: instant and duration types in each language.
Report a problem with this pageAsk a question in Discussions