This module provides business time arithmetic operations.
It allows shifting a DateTime forward or backward by a given amount of business time, skipping over non-working hours, weekends, holidays, and breaks.
Summary
Functions
Shifts a DateTime forward or backward by the given amount of business time.
Types
@type duration() :: {integer(), :hour | :minute | :day}
Functions
@spec shift(OpenHours.Schedule.t(), DateTime.t(), duration()) :: DateTime.t()
Shifts a DateTime forward or backward by the given amount of business time.
A positive amount shifts forward, a negative amount shifts backward.
Supports :hour, :minute, and :day units.
Hour and minute shifts
For :hour and :minute units, the shift consumes the exact amount of
business time, walking through consecutive time slots and skipping
non-working periods. When the starting DateTime is outside business hours,
it snaps to the next (forward) or previous (backward) business moment
before applying the offset.
Day shifts
For the :day unit, the shift moves to the Nth business day forward or
backward, preserving the time of day when possible. A "business day" is
any day that has at least one time slot in the schedule (not a holiday,
and has configured hours or a shift override).
The current day is never counted — {1, :day} always moves to a
different date.
Time preservation and snapping
The algorithm tries to keep the original time of day on the target date. When the time falls within business hours on the target day, it is preserved exactly. When it does not, the result is the nearest business moment in the direction of the shift — which may be on a different day than the target:
- Forward: finds the next business moment after the target date + time. If the time falls in a gap between slots on the target day, this is the start of the next slot on that day. If the time is past all slots on the target day, this is the start of business on the next business day.
- Backward: finds the previous business moment before the target date + time. If the time falls in a gap, this is the end of the preceding slot. If the time is before all slots, this is the end of business on the previous business day.
This matters when the target day has different hours than the origin day (e.g. a shift override with shorter hours, or a day with breaks that create gaps).
Examples
Given a schedule with Mon–Fri 09:00–14:00 and 15:00–20:00, and Friday overridden with a shift of 10:00–14:00:
# Time preserved: 10:00 exists in both days' hours
shift(schedule, ~N[2019-01-16 10:00:00], {1, :day})
#=> Thursday 10:00
# 15:00 is past Friday's shift (10:00–14:00), no later slot on
# Friday, so it flows forward to the next business day
shift(schedule, ~N[2019-01-17 15:00:00], {1, :day})
#=> Monday 09:00
# 14:30 falls in the gap between 09–14 and 15–20, snaps to
# the start of the next slot on the same day
shift(schedule, ~N[2019-01-14 14:30:00], {1, :day})
#=> Wednesday 15:00Examples
iex> schedule = %OpenHours.Schedule{
...> hours: %{
...> mon: [{~T[09:00:00], ~T[17:00:00]}],
...> tue: [{~T[09:00:00], ~T[17:00:00]}]
...> },
...> time_zone: "Europe/Madrid"
...> }
iex> dt = DateTime.from_naive!(~N[2019-01-14 10:00:00], "Europe/Madrid")
iex> OpenHours.Offset.shift(schedule, dt, {2, :hour})
DateTime.from_naive!(~N[2019-01-14 12:00:00], "Europe/Madrid")