Shipmind Labs

A shift pattern is wall-clock time. A shift is an instant. If your model lets those two share a type, DST will eventually hand you a scheduling bug you cannot reproduce.

That is the rule we ended up encoding in our open-source scheduling library, shift-planner, and it reshaped the whole API.

The pattern says "nurses start at 22:00". That is an intent. It is not a moment in time until you pick a day and a zone. So the pattern object refuses a start time carrying tzinfo at construction: the plan already owns the timezone, and a pattern that smuggles in its own gives you two sources of truth that disagree twice a year. The instant gets built once, in the one place where both halves are present: combine the day, the wall-clock time, and the plan's zone.

The other direction matters just as much. When a plan decides which existing shifts belong to it, it draws day boundaries by converting each shift's start back into the plan's zone and taking that date. Not UTC. A shift starting at 22:00 local belongs to that local day, even though in UTC it has already rolled over.

Then the consequence that surprises people. Shift end is start plus length in wall-clock arithmetic, while coverage and overlap checks compare instants. Across a spring-forward gap, a two-hour shift is one hour of real duty. That is not a bug to paper over. It is what happens to the person working it, and payroll and coverage rules need to agree on which of the two numbers they mean. We keep a test file whose only job is to pin that behaviour down, because it is exactly the kind of thing a well-meaning refactor quietly normalises away.

Three smaller decisions fell out of the same principle:

Shifts, jobs and cooldown locks all reject a naive datetime at construction. A timezone-less timestamp is not a moment; it is a note to yourself.

Every interval is half-open. Back-to-back shifts must not count as overlapping, and that is a one-line decision you only get to make once.

No source file calls datetime.now(). Every check takes the moment as an argument. That is what makes DST testable at all, and it means an operator can ask "is this roster legal as of Sunday 02:30" without touching the system clock.

One last property worth copying: applying a plan keeps every shift the plan does not claim, then appends the shifts it generates. Redrawing a plan is idempotent. Reruns are boring, and boring is what we were after.

Most scheduling bugs we have been called in to look at are not scheduling bugs. They are one timestamp that lost its zone, or a duration that was assumed to equal elapsed time.

If you run shift-based operations, it is probably worth working out where your wall-clock intent and your instants currently meet, and whether that is one place or several.

Was this useful?

Building something similar?

or email hello@shipmindlabs.com