GanttEx.Calendar (GanttEx v0.1.0)

Copy Markdown View Source

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.

Summary

Types

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

t()

Functions

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

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

Returns the working periods of a date.

Builds a calendar.

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

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

Returns true if any time of the date is worked.

Returns the number of working minutes of a date.

Returns true if datetime falls inside working time.

Types

range()

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

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

t()

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

Functions

add(calendar, datetime, amount, unit)

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(calendar, from, to, unit)

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(calendar, datetime)

@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(calendar)

@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!(opts \\ [])

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

Like new/1 but raises ArgumentError.

next_working(calendar, datetime, unit)

@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(calendar, from, to)

@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?(calendar, date)

@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(calendar, date)

@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?(calendar, datetime)

@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