# `OpenHours.Offset`
[🔗](https://github.com/hopsor/open_hours/blob/v1.0.1/lib/open_hours/offset.ex#L1)

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.

# `duration`

```elixir
@type duration() :: {integer(), :hour | :minute | :day}
```

# `shift`

```elixir
@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:00

## Examples

    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")

---

*Consult [api-reference.md](api-reference.md) for complete listing*
