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
:dayunit, 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
:hourand:minuteunits, 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
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.
Like new/1 but raises ArgumentError.
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
@type range() :: {non_neg_integer(), pos_integer()}
A working period of a day, in minutes from midnight: {start, finish}.
Functions
@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]
@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
@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])
[]
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 ofdate => hourssets 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}}
Like new/1 but raises ArgumentError.
@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]
@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]]
@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
@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
@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