lib/tox.ex

defmodule Tox do
  @moduledoc """
  Some structs and functions to work with dates, times, durations, periods, and
  intervals.
  """

  @typedoc """
  Units related to dates and times.
  """
  @type unit ::
          :year
          | :month
          | :week
          | :day
          | :hour
          | :minute
          | :second
          | :microsecond

  @typedoc """
  An amount of time with a specified unit e.g. `{second: 500}`.
  """
  @type duration :: {unit(), integer()}

  @typedoc """
  Boundaries specifies whether the start and end of an interval are included or
  excluded.

  * `:open`: start and end are excluded
  * `:closed`: start and end are included
  * `:left_open`: start is excluded and end is included
  * `:right_open`: start is included and end is excluded
  """
  @type boundaries :: :closed | :left_open | :right_open | :open

  @doc """
  Shift the `DateTime`, `NaiveDateTime`, `Date` or `Time` by the given duration.
  """
  @spec shift(date_or_time, [duration()]) :: date_or_time
        when date_or_time: DateTime.t() | NaiveDateTime.t() | Date.t() | Time.t()
  def shift(%DateTime{} = datetime, duration), do: Tox.DateTime.shift(datetime, duration)
  def shift(%NaiveDateTime{} = naive, duration), do: Tox.NaiveDateTime.shift(naive, duration)
  def shift(%Date{} = date, duration), do: Tox.Date.shift(date, duration)
  def shift(%Time{} = time, duration), do: Tox.Time.shift(time, duration)

  @doc false
  @spec days_per_week :: integer()
  def days_per_week, do: 7

  @doc false
  @spec week(Calendar.date()) :: {Calendar.year(), non_neg_integer()}
  def week(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
    :calendar.iso_week_number({year, month, day})
  end

  Code.ensure_loaded(Date)

  if function_exported?(Date, :beginning_of_week, 2) do
    @doc false
    @spec day_of_week(Calendar.calendar(), integer(), non_neg_integer, non_neg_integer) :: 1..7
    def day_of_week(calendar, year, month, day) do
      {day, _epoch_day_of_week, _last_day_of_week} =
        calendar.day_of_week(year, month, day, :default)

      day
    end
  else
    @doc false
    @spec day_of_week(Calendar.calendar(), integer(), non_neg_integer, non_neg_integer) :: 1..7
    def day_of_week(calendar, year, month, day) do
      calendar.day_of_week(year, month, day)
    end
  end
end