Current section
Files
Jump to
Current section
Files
lib/tempus.ex
defmodule Tempus do
@moduledoc """
Documentation for `Tempus`.
"""
use Boundary, exports: [Slot, Slots]
alias Tempus.{Slot, Slots}
@spec free?(slots :: Slots.t(), slot :: Slot.origin()) :: boolean() | no_return
@doc """
Checks whether the slot is disjoined against slots.
### Examples
iex> slots = [
...> Tempus.Slot.wrap(~D|2020-08-07|),
...> Tempus.Slot.wrap(~D|2020-08-10|)
...> ] |> Enum.into(%Tempus.Slots{})
iex> Tempus.free?(slots, ~D|2020-08-07|)
false
iex> Tempus.free?(slots, ~D|2020-08-08|)
true
"""
def free?(%Slots{} = slots, slot) do
Slots.size(Slots.add(slots, Slot.wrap(slot))) == Slots.size(slots) + 1
end
@spec days_ahead(slots :: Slots.t(), origin :: Date.t(), count :: integer()) :: Date.t()
@doc """
Returns the reversed list of free days after origin.
### Examples
iex> slots = [
...> Tempus.Slot.wrap(~D|2020-08-07|),
...> Tempus.Slot.wrap(~D|2020-08-10|)
...> ] |> Enum.into(%Tempus.Slots{})
iex> Tempus.days_ahead(slots, ~D|2020-08-07|, 0)
[~D|2020-08-08|]
iex> Tempus.days_ahead(slots, ~D|2020-08-07|, 3) |> hd()
~D|2020-08-12|
"""
def days_ahead(slots, origin, count) when is_integer(count) and count >= 0 do
origin
|> Stream.iterate(&Date.add(&1, 1))
|> Enum.reduce_while([], fn
_date, acc when length(acc) > count -> {:halt, acc}
date, acc -> {:cont, if(free?(slots, date), do: [date | acc], else: acc)}
end)
end
@spec next_busy(Slots.t(), [{:origin, Slot.origin()} | {:count, pos_integer()}]) ::
[Slot.t()] | Slot.t() | nil | no_return
@doc """
Returns the next **busy** slot from the slots passed as a first argument,
that immediately follows `origin`. IOf slots are overlapped, the overlapped
one gets returned.
### Examples
iex> slots = [
...> Tempus.Slot.wrap(~D|2020-08-07|),
...> Tempus.Slot.wrap(~D|2020-08-10|)
...> ] |> Enum.into(%Tempus.Slots{})
iex> Tempus.next_busy(slots, origin: %Tempus.Slot{from: ~U|2020-08-08 23:00:00Z|, to: ~U|2020-08-09 12:00:00Z|})
#Slot<[from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: %Tempus.Slot{from: ~U|2020-08-07 11:00:00Z|, to: ~U|2020-08-07 12:00:00Z|}, count: 2) |> hd()
#Slot<[from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: %Tempus.Slot{from: ~U|2020-08-07 11:00:00Z|, to: ~U|2020-08-08 12:00:00Z|})
#Slot<[from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: %Tempus.Slot{from: ~U|2020-08-07 11:00:00Z|, to: ~U|2020-08-10 12:00:00Z|})
#Slot<[from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: ~D|2020-08-07|)
#Slot<[from: ~U[2020-08-07 00:00:00.000000Z], to: ~U[2020-08-07 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: ~D|2020-08-08|)
#Slot<[from: ~U[2020-08-10 00:00:00.000000Z], to: ~U[2020-08-10 23:59:59.999999Z]]>
iex> Tempus.next_busy(slots, origin: %Tempus.Slot{from: ~U|2020-08-11 11:00:00Z|, to: ~U|2020-08-11 12:00:00Z|})
nil
iex> Tempus.next_busy(%Tempus.Slots{})
nil
"""
def next_busy(slots, opts \\ [])
def next_busy(%Slots{} = slots, opts) do
origin = Keyword.get(opts, :origin, DateTime.utc_now())
case Keyword.get(opts, :count) do
nil -> slots |> do_next_busy(origin, 1) |> List.first()
i when is_integer(i) and i > 0 -> do_next_busy(slots, origin, i)
other -> raise Tempus.ArgumentError, expected: Integer, passed: other
end
end
def next_busy(%Slot{} = slot, opts),
do: next_busy(Slots.add(%Slots{}, slot), opts)
@spec do_next_busy(Slots.t(), Slot.origin(), pos_integer()) :: [Slot.t()] | no_return
defp do_next_busy(%Slots{} = slots, origin, count) do
origin = Slot.wrap(origin)
slots
|> Enum.drop_while(fn
%Slot{} = slot -> Slot.strict_compare(slot, origin) == :lt
other -> raise Tempus.ArgumentError, expected: Tempus.Slot, passed: other
end)
|> Enum.take(count)
end
@spec next_free(Slots.t(), [{:origin, Slot.origin()} | {:count, pos_integer()}]) ::
Slot.t() | nil | no_return
@doc """
Returns the next **free** slot from the slots passed as a first argument,
that immediately follows `origin`. IOf slots are overlapped, the overlapped
one gets returned.
### Examples
iex> slots = [
...> Tempus.Slot.wrap(~D|2020-08-07|),
...> Tempus.Slot.wrap(~D|2020-08-10|)
...> ] |> Enum.into(%Tempus.Slots{})
iex> Tempus.next_free(slots, origin: %Tempus.Slot{from: ~U|2020-08-08 23:00:00Z|, to: ~U|2020-08-09 12:00:00Z|})
#Slot<[from: ~U[2020-08-08 00:00:00.000000Z], to: ~U[2020-08-09 23:59:59.999999Z]]>
iex> Tempus.next_free(slots, origin: %Tempus.Slot{from: ~U|2020-08-06 11:00:00Z|, to: ~U|2020-08-06 12:00:00Z|})
#Slot<[from: ~U[2020-08-06 12:00:00Z], to: ~U[2020-08-07 00:00:00.000000Z]]>
iex> Tempus.next_free(slots, origin: ~D|2020-08-11|)
#Slot<[from: ~U[2020-08-12 00:00:00.000000Z], to: nil]>
"""
def next_free(slots, opts \\ []) do
origin =
opts
|> Keyword.get(:origin, DateTime.utc_now())
|> Slot.wrap()
|> Slot.shift(to: 1)
first = AVLTree.get_first(slots.slots)
unless is_nil(first) do
slot = %Slot{from: origin.to, to: first.from}
if Slot.valid?(slot),
do: slot,
else: slots |> Slots.inverse() |> next_busy(opts)
end || %Slot{from: origin.to}
end
end