# `GanttEx.Calendar`
[🔗](https://github.com/rocket4ce/gantt_ex/blob/v0.1.0/lib/gantt_ex/calendar.ex#L1)

Working-time calendars.

A calendar defines the working days of the week, the working hours of a
day and per-date exceptions (holidays or extra working days). When a chart
uses working time, durations only count working time:

  * with the `:day` unit, a duration is a number of working days, and a
    task that starts on Friday with a duration of 2 ends on Tuesday
  * with the `:hour` and `:minute` units, only working hours count
  * larger units (`:week`, `:month`, ...) use plain calendar time

## Examples

    iex> calendar = GanttEx.Calendar.new!(holidays: [~D[2025-01-01]])
    iex> GanttEx.Calendar.working_day?(calendar, ~D[2025-01-01])
    false
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-03 00:00:00], 2, :day)
    ~N[2025-01-07 00:00:00]
    iex> GanttEx.Calendar.diff(calendar, ~N[2025-01-03 00:00:00], ~N[2025-01-07 00:00:00], :day)
    2
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-03 16:00:00], 3, :hour)
    ~N[2025-01-06 10:00:00]

Calendars are given to `GanttEx.Live.Gantt` with the `calendar` attribute
(the global calendar) and the `calendars` attribute (calendars that tasks
reference with their `:calendar_id`), together with `work_time`.

# `range`

```elixir
@type range() :: {non_neg_integer(), pos_integer()}
```

A working period of a day, in minutes from midnight: `{start, finish}`.

# `t`

```elixir
@type t() :: %GanttEx.Calendar{
  dates: %{required(Date.t()) =&gt; [range()]},
  hours: [range()],
  id: term(),
  work_days: [1..7]
}
```

# `add`

```elixir
@spec add(t(), NaiveDateTime.t(), number(), GanttEx.Dates.unit()) :: NaiveDateTime.t()
```

Adds `amount` units of working time. Negative amounts move backwards: that
is how a start date is computed from an end date.

    iex> calendar = GanttEx.Calendar.new!()
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-10 00:00:00], 1, :day)
    ~N[2025-01-11 00:00:00]
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-14 00:00:00], -2, :day)
    ~N[2025-01-10 00:00:00]
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-06 11:00:00], 2, :hour)
    ~N[2025-01-06 14:00:00]
    iex> GanttEx.Calendar.add(calendar, ~N[2025-01-06 09:00:00], 1, :month)
    ~N[2025-02-06 09:00:00]

# `diff`

```elixir
@spec diff(t(), NaiveDateTime.t(), NaiveDateTime.t(), GanttEx.Dates.unit()) ::
  integer()
```

Returns the working time between two datetimes in `unit`, rounded.

    iex> calendar = GanttEx.Calendar.new!()
    iex> GanttEx.Calendar.diff(calendar, ~N[2025-01-06 00:00:00], ~N[2025-01-13 00:00:00], :day)
    5
    iex> GanttEx.Calendar.diff(calendar, ~N[2025-01-06 10:00:00], ~N[2025-01-07 10:00:00], :hour)
    8
    iex> GanttEx.Calendar.diff(calendar, ~N[2025-01-13 00:00:00], ~N[2025-01-06 00:00:00], :day)
    -5

# `hours_for`

```elixir
@spec hours_for(t(), Date.t() | NaiveDateTime.t()) :: [range()]
```

Returns the working periods of a date.

    iex> calendar = GanttEx.Calendar.new!()
    iex> GanttEx.Calendar.hours_for(calendar, ~D[2025-01-06])
    [{480, 720}, {780, 1020}]
    iex> GanttEx.Calendar.hours_for(calendar, ~D[2025-01-05])
    []

# `new`

```elixir
@spec new(keyword() | map() | t()) :: {:ok, t()} | {:error, {:invalid, atom()}}
```

Builds a calendar.

## Options

  * `:id` - identifier referenced by tasks (`:calendar_id`). Defaults to `"global"`.
  * `:work_days` - ISO days of the week that are worked (`1` = Monday).
    Defaults to Monday to Friday.
  * `:hours` - working periods of a working day, as `{8, 12}` (hours),
    `{"08:30", "12:00"}`, `"08:30-12:00"` or `{~T[08:30:00], ~T[12:00:00]}`.
    Defaults to 08:00-12:00 and 13:00-17:00.
  * `:holidays` - dates that are not worked.
  * `:working_dates` - dates that are worked even if their weekday is not.
    A map of `date => hours` sets custom hours for those dates.

## Examples

    iex> {:ok, calendar} = GanttEx.Calendar.new(id: "night", work_days: [1, 2, 3, 4], hours: ["22:00-23:30"])
    iex> {calendar.work_days, calendar.hours}
    {[1, 2, 3, 4], [{1320, 1410}]}

    iex> GanttEx.Calendar.new(hours: ["9 to 5"])
    {:error, {:invalid, :hours}}

# `new!`

```elixir
@spec new!(keyword() | map() | t()) :: t()
```

Like `new/1` but raises `ArgumentError`.

# `next_working`

```elixir
@spec next_working(t(), NaiveDateTime.t(), GanttEx.Dates.unit()) :: NaiveDateTime.t()
```

Moves `datetime` forward to the next working time: the start of the next
working day for the `:day` unit, the next working minute otherwise.

    iex> calendar = GanttEx.Calendar.new!()
    iex> GanttEx.Calendar.next_working(calendar, ~N[2025-01-04 10:00:00], :day)
    ~N[2025-01-06 00:00:00]
    iex> GanttEx.Calendar.next_working(calendar, ~N[2025-01-06 12:15:00], :hour)
    ~N[2025-01-06 13:00:00]

# `non_working_dates`

```elixir
@spec non_working_dates(t(), NaiveDateTime.t(), NaiveDateTime.t()) :: [Date.t()]
```

Returns the non-working dates between two datetimes (useful to highlight them).

    iex> calendar = GanttEx.Calendar.new!(holidays: [~D[2025-01-08]])
    iex> GanttEx.Calendar.non_working_dates(calendar, ~N[2025-01-06 00:00:00], ~N[2025-01-13 00:00:00])
    [~D[2025-01-08], ~D[2025-01-11], ~D[2025-01-12]]

# `working_day?`

```elixir
@spec working_day?(t(), Date.t() | NaiveDateTime.t()) :: boolean()
```

Returns `true` if any time of the date is worked.

    iex> GanttEx.Calendar.working_day?(GanttEx.Calendar.new!(), ~D[2025-01-04])
    false

# `working_minutes`

```elixir
@spec working_minutes(t(), Date.t()) :: non_neg_integer()
```

Returns the number of working minutes of a date.

    iex> GanttEx.Calendar.working_minutes(GanttEx.Calendar.new!(), ~D[2025-01-06])
    480

# `working_time?`

```elixir
@spec working_time?(t(), NaiveDateTime.t()) :: boolean()
```

Returns `true` if `datetime` falls inside working time.

    iex> calendar = GanttEx.Calendar.new!()
    iex> GanttEx.Calendar.working_time?(calendar, ~N[2025-01-06 12:30:00])
    false
    iex> GanttEx.Calendar.working_time?(calendar, ~N[2025-01-06 13:00:00])
    true

---

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