Skip to content

Time zones and DST

How rule times follow each rule's IANA time zone, and how workclock resolves daylight saving time gaps and overlaps.

Working hours are wall-clock times in a rule's IANA time zone, and durations are elapsed real time. So a shift that spans a daylight saving time (DST) change is an hour shorter or longer than its wall-clock length. This page covers time zones and DST in Rust, Python and JavaScript.

Zones of a schedule and its rules

  • Every schedule has a default time zone: time_zone in Rust and Python, timeZone in JavaScript. It is UTC if omitted.
  • Each rule can set its own zone. A rule without one uses the schedule's zone; holidays do too, not UTC. Rules has examples with rules in different zones.
  • Zones are IANA names, such as Europe/London or America/New_York. Python also accepts a zoneinfo.ZoneInfo, a pytz zone or datetime.timezone.utc, and converts it to its name.
  • An unknown name is an unknown-zone error when the schedule is built (Errors).

Instants are absolute, so the zone an instant is written in does not change a result. It only changes how the result is shown: Rust returns a jiff::Timestamp, Python returns a datetime in the tzinfo of the datetime passed in, and JavaScript returns a Date.

Gaps and overlaps

When a rule's start or end is not a unique local time, workclock resolves it with the compatible strategy, the default of jiff, RFC 5545 and JavaScript Temporal:

  • Gap (clocks go forward): a time that does not exist moves forward by the length of the gap. On a day when clocks jump from 02:00 to 03:00, 02:30 becomes 03:30.
  • Overlap (clocks go back): a time that occurs twice resolves to the earlier occurrence.

If resolving leaves a block's start at or after its end, the rule has no working time that day (D8).

A holiday removes local 00:00 to the next local 00:00, so a holiday on a DST change day is 23 or 25 hours long when the change is one hour (D7).

A night shift across a DST change

In America/New_York, clocks go forward on Sunday 8 March 2026: at 02:00 EST they jump to 03:00 EDT. A 22:00–06:00 shift that night has 7 hours of working time:

Sat 7 Mar   22:00 EST ──── 4 hours ────▶ 02:00 EST
            02:00 EST is the same instant as 03:00 EDT
Sun 8 Mar   03:00 EDT ──── 3 hours ────▶ 06:00 EDT

Adding 7 hours of working time to 22:00 on Saturday therefore ends at 06:00 EDT on Sunday, which is 2026-03-08T10:00:00Z.

use jiff::civil::time;
use jiff::{SignedDuration, Zoned};
use workclock::{Schedule, Weekdays, WorkDay};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let nights = Schedule::builder()
        .time_zone("America/New_York")
        .rule(WorkDay::new(time(22, 0, 0, 0), time(6, 0, 0, 0), Weekdays::ALL))
        .build()?;

    let start: Zoned = "2026-03-07T22:00[America/New_York]".parse()?;
    let end: Zoned = "2026-03-08T06:00[America/New_York]".parse()?;
    assert_eq!(nights.between(&start, &end), SignedDuration::from_hours(7));

    let done = nights.add(&start, SignedDuration::from_hours(7))?;
    let local = done.to_zoned(start.time_zone().clone());
    assert_eq!(local.to_string(), "2026-03-08T06:00:00-04:00[America/New_York]");
    Ok(())
}

The same shift on the night clocks go back, Saturday 31 October to Sunday 1 November 2026, has 9 hours (D2).

workclock.compat resolves overlaps to the earlier occurrence too, while businesstimedelta's pytz localize() picks the later one, so results differ for rule boundaries inside a repeated hour (Migrating from businesstimedelta).

Time zone data

Zone rules come from a copy of the IANA time zone database, and which copy depends on the package:

PackageDatabase used
npm packageAlways its bundled copy, as current as the workclock release
Python wheelsThe system database when there is one, otherwise the bundled copy
Rust cratejiff's defaults: the system database (/usr/share/zoneinfo, or the directory in TZDIR) on Linux and macOS, and a bundled copy on Windows

Note

On a Linux image without tzdata, as some slim and distroless images are, every named zone in the Rust crate, even "UTC", is an unknown-zone error. Install the tzdata package, point TZDIR at a zoneinfo directory, or enable jiff's tzdb-bundle-always feature to bundle the database.

Edge cases

These are the DST examples from DESIGN.md. Each ID is a case in tests/fixtures/design_examples.json, which the Rust, Python and JavaScript test suites all run. Unless noted, the zone is America/New_York, where clocks go forward on Sunday 8 March 2026 and back on Sunday 1 November 2026.

IDScheduleQueryResultShows
D122–06 every daybetween(Sat 2026-03-07 22:00 EST, Sun 06:00 EDT)7hA night shift loses the hour clocks skip
D222–06 every daybetween(Sat 2026-10-31 22:00 EDT, Sun 06:00 EST)9hA night shift gains the hour clocks repeat
D322–06 every dayadd(Sat 2026-03-07 22:00 EST, 7h)Sun 2026-03-08 06:00 EDTadd counts elapsed time across the change
D402:30–04:00 SunWorking time on 2026-03-0803:30–04:00 EDT, 0.5hA start in the gap moves forward by the gap
D501:30–03:00 SunWorking time on 2026-11-0101:30 EDT–03:00 EST, 2.5hA start in the overlap takes the earlier 01:30
D600–00 every day, Europe/Londonbetween(Sun 2026-03-29 00:00 GMT, Mon 00:00 BST)23hA 24-hour shift on a change day follows the clock
D700–00 every day, holiday 2026-03-08between(Sat 2026-03-07 00:00, Tue 2026-03-10 00:00)48hA holiday on a change day removes 23 hours
D802:30–03:00 SunWorking time on 2026-03-08None, 0hA block that resolves to start after its end is empty
D902:00–02:30 SunWorking time on 2026-03-0803:00–03:30 EDT, 0.5hBoth ends in the gap move forward together

See also

Report a problem with this pageAsk a question in Discussions

On this page