Skip to content

Measuring time

Count the working time between two instants with between, and read the sign of the result.

between(a, b) returns the working time in [a, b): the time from a up to, but not including, b that the schedule counts. It is negative when b is before a. This page covers between in Rust, Python and JavaScript.

Count working time

A support desk in London works 09:00–17:00 on weekdays with a lunch hour, and is closed on 25 and 28 December 2026. A ticket opened at 16:30 on Thursday 24 December 2026, London time. By 11:00 on Tuesday 29 December it has used 2 hours 30 minutes of working time:

Date, Europe/LondonWhyWorking time counted
Thursday 24 December, from 16:30Opened 30 minutes before the end of the working day16:30–17:00, 30 minutes
Friday 25 DecemberHolidayNone
Saturday 26 and Sunday 27 DecemberNot working daysNone
Monday 28 DecemberHolidayNone
Tuesday 29 December, until 11:00Before the lunch break09:00–11:00, 2 hours
Total2 hours 30 minutes

Each tab counts that time, then swaps the two instants to get the same amount with a minus sign.

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 support = 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 opened: Zoned = "2026-12-24T16:30[Europe/London]".parse()?;
    let now: Zoned = "2026-12-29T11:00[Europe/London]".parse()?;
    let used = support.between(&opened, &now);
    assert_eq!(used, SignedDuration::from_mins(150));
    assert_eq!(support.between(&now, &opened), -used);
    Ok(())
}

What each language takes and returns:

RustPythonJavaScript
Callworkclock::Schedule::between(&self, a, b)workclock.Schedule.between(a, b)between(a, b)
InstantsAnything that is Into<jiff::Timestamp>, such as jiff::Timestamp or &jiff::ZonedTimezone-aware datetimeDate, or an ISO 8601 string with a UTC offset
Resultjiff::SignedDuration, exact to the nanoseconddatetime.timedelta, exact to the microsecondA number of milliseconds
ErrorsNone: the result is not a ResultTypeError for a value that is not a datetime, ValueError for a naive one, OutOfRangeError outside the supported rangeTypeError for a value that is neither a Date nor a string, RangeError for an invalid Date or a string without an offset, OutOfRangeError outside the supported range

The supported range is -009999-01-02T01:59:59Z to 9999-12-30T22:00:00Z. Python's between_many and JavaScript's betweenMany measure many pairs in one call.

The sign of the result

  • a before b: the working time in [a, b), zero or more.
  • a after b: minus between(b, a). The result is signed, like b - a for two instants, which resolves businesstimedelta issue #12.
  • a equal to b: zero.

So between(a, b) is always -between(b, a), and an amount of time passed to add comes back from between: between(t, add(t, d)) is d for every t and d (Adding time).

What is counted

between counts working time only, so the same calls hold for every rule:

  • A break inside the span is left out, and a span that starts inside a break counts from the break's end (DESIGN.md K1, K2).
  • Holidays and days not in a WorkDay's days count nothing (DESIGN.md H1, B3).
  • An overnight shift counts across midnight, on the weekday it starts on (DESIGN.md O1, O5, O6).
  • Durations are elapsed real time, so a night shift is an hour shorter or longer when clocks change for daylight saving time (DST) (DESIGN.md D1, D2). Time zones and DST has the details.

Edge cases

These examples are from DESIGN.md, and the Rust, Python and JavaScript test suites all run them from tests/fixtures/design_examples.json. Unless the row says otherwise, they use the week of Monday 5 January 2026 in UTC, and 9–17 Mon–Fri is a WorkDay from 09:00 to 17:00, Monday to Friday.

IDScheduleQueryResultWhat it shows
B19–17 Mon–Fribetween(Mon 10:00, Mon 12:00)2hInside one period
B2Samebetween(Fri 16:00, next Mon 10:00)2hAcross a weekend
B3Samebetween(Sat 10:00, Sun 10:00)0hNo working time in the span
B4Samebetween(Mon 12:00, Mon 10:00)-2hb before a gives a negative result
B5Samebetween(Mon 10:00, Mon 10:00)0hEqual instants give zero
K19–17 Mon–Fri, break 12–13 Mon–Fribetween(Mon 09:00, Mon 17:00)7hA break is left out
K2Samebetween(Mon 12:30, Mon 14:00)1hA span that starts inside a break
O122–06 Mon–Fribetween(Mon 22:00, Tue 06:00)8hAn overnight shift counts across midnight
O5Samebetween(Sat 00:00, Sat 06:00)6hFriday's shift runs into Saturday
O6Samebetween(Mon 00:00, Mon 06:00)0hNo shift starts on Sunday
O700–00 Mon–Sunbetween(Mon 00:00, next Mon 00:00)168hRound-the-clock working time
H19–17 Mon–Fri, holiday Wed 2026-01-07between(Mon 09:00, Sat 00:00)32hA holiday counts nothing
H3Samebetween(Wed 12:00, Thu 12:00)3hA span that starts inside a holiday
H4Samebetween(Mon 2027-01-04 09:00, 17:00)8hNo time off after the last holiday
HO122–06 Mon–Fri, holiday Tue 2026-01-06between(Mon 22:00, Tue 06:00)2hA holiday cuts an overnight shift at midnight
Z19–17 Mon–Fri Asia/Kolkata, holiday 2026-01-26between(2026-01-26T03:30Z, 2026-01-26T11:30Z)0hInstants in UTC, holiday in the schedule's zone
Z59–18 Mon–Fri America/Santiago, break 12–13between(2016-01-18 09:00, 18:00) in America/Los_Angeles4hInstants in one zone, rules in another
D122–06 every day, America/New_Yorkbetween(Sat 2026-03-07 22:00 EST, Sun 06:00 EDT)7hThe night clocks go forward
D2Samebetween(Sat 2026-10-31 22:00 EDT, Sun 06:00 EST)9hThe night clocks go back
D600–00 every day, Europe/Londonbetween(Sun 2026-03-29 00:00 GMT, Mon 00:00 BST)23hA day with a clock change is not 24 hours
D700–00 every day, America/New_York, holiday 2026-03-08between(Sat 2026-03-07 00:00, Tue 2026-03-10 00:00)48hA holiday on that day removes 23 hours

See also

Report a problem with this pageAsk a question in Discussions

On this page