Skip to content

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:

DateClock inClock out
Monday 28 December 2026, a holiday10:0014:00
Tuesday 29 December 202608:3012:00
Tuesday 29 December 202613:0018: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/LondonOn the clockContractedOvertime
Monday 28 December, 10:00–14:004 hoursNone: a holiday4 hours
Tuesday 29 December, 08:30–12:003 hours 30 minutes09:00–12:00, 3 hours30 minutes
Tuesday 29 December, 13:00–18:155 hours 15 minutes13:00–17:00, 4 hours1 hour 15 minutes
Total12 hours 45 minutes7 hours5 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 WorkDay counts across midnight, and the shift belongs to the weekday it starts on (Overnight shifts).
  • Holidays. Each date in Holidays removes 00:00 to the next 00:00 in the schedule's time zone (Rules: Holidays).
  • DST. between returns 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

Report a problem with this pageAsk a question in Discussions

On this page