defmodule Tempus.Slot do @moduledoc """ Declares a time slot interval and provides functions for interval arithmetic, coverage checks, boundary comparisons, and transformations. Slots in `Tempus` are modeled as mathematical intervals with configurable boundary openness: - **Half-Open Intervals**: Standard slots default to $[from, to)$ (`from_open: false`, `to_open: true`). For example, a date `~D[2023-04-12]` wraps into `%Slot{from: ~U[2023-04-12 00:00:00Z], to: ~U[2023-04-13 00:00:00Z], from_open: false, to_open: true}`. - **Infinite / Unbound Limits**: A `nil` boundary represents infinity ($-\infty$ or $+\infty$) and is always treated as **open** (`from_open: true` when `from: nil`, `to_open: true` when `to: nil`). - **Void / Identity Slot**: Represented by `%Slot{from: nil, to: nil, from_open: true, to_open: true}` (`Slot.id/0`). ### Key Functions - `Slot.wrap/1`: Converts a `Date`, `DateTime`, or `Time` into a `%Tempus.Slot{}`. - `Slot.join/2`: Merges two overlapping or contiguous slots into a single maximal slot. - `Slot.intersect/2`: Calculates the overlapping intersection of slots. - `Slot.xor/2`: Returns the symmetric difference between two slots. - `Slot.gap/1`: Finds the uncovered gap between slots. - `Slot.cover?/2`: Checks if a `DateTime` or `Date` falls within the slot interval. - `Slot.disjoint?/2` & `Slot.neighbour?/2`: Checks if slots are non-overlapping or adjacent/touching. """ alias __MODULE__ import Tempus.Guards @typedoc "A timeslot to be used in `Tempus`" @type t :: %__MODULE__{ from: nil | DateTime.t(), to: nil | DateTime.t(), from_open: boolean(), to_open: boolean() } @typedoc "The origin used in comparisons and calculations" @type origin :: Slot.t() | Date.t() | DateTime.t() | nil defstruct [:from, :to, from_open: false, to_open: true] @spec new([{:from, origin()} | {:to, origin()}]) :: {:ok, t()} | {:error, any()} @doc """ Creates new slot using `arg[:from]` as a starting origin and `arg[:to]` and an ending origin. ## Examples iex> Tempus.Slot.new(from: ~U|2015-09-30 00:00:00Z|, to: ~U|2015-10-01 01:00:00Z|) {:ok, %Tempus.Slot{from: ~U|2015-09-30 00:00:00Z|, to: ~U|2015-10-01 01:00:00Z|, from_open: false, to_open: false}} iex> Tempus.Slot.new(%{from: ~D|2015-09-30|, to: ~U|2015-10-01T12:00:00Z|}) {:ok, %Tempus.Slot{from: ~U|2015-09-30 00:00:00.000000Z|, to: ~U|2015-10-01 12:00:00Z|, from_open: false, to_open: false}} """ def new(from_to), do: new(from_to[:from], from_to[:to]) @spec new(from :: origin(), to :: origin()) :: {:ok, t()} | {:error, any()} @doc """ Creates new slot using `from` as a starting origin and `to` and an ending origin. See `new/1` for more readable implementation. ## Examples iex> import Tempus.Sigils iex> Tempus.Slot.new(~U|2015-09-30 00:00:00Z|, ~U|2015-10-01 01:00:00Z|) {:ok, %Tempus.Slot{from: ~U|2015-09-30 00:00:00Z|, to: ~U|2015-10-01 01:00:00Z|, from_open: false, to_open: false}} iex> Tempus.Slot.new(~D|2015-09-30|, ~U|2015-10-01T12:00:00Z|) {:ok, %Tempus.Slot{from: ~U|2015-09-30 00:00:00.000000Z|, to: ~U|2015-10-01 12:00:00Z|, from_open: false, to_open: false}} iex> Tempus.Slot.new(nil, nil) {:ok, Tempus.Slot.id()} iex> Tempus.Slot.new(~D|2015-09-30|, nil) {:ok, %Tempus.Slot{from: ~U[2015-09-30 00:00:00.000000Z], to: nil, from_open: false, to_open: true}} iex> Tempus.Slot.new(nil, ~D|2015-09-30|) {:ok, %Tempus.Slot{from: nil, to: ~U[2015-10-01 00:00:00.000000Z], from_open: true, to_open: true}} iex> Tempus.Slot.new(:ok, :ok) {:error, :invalid_input} """ def new(from, to) when not is_origin(from) when not is_origin(to), do: {:error, :invalid_input} def new(nil, nil), do: {:ok, %Tempus.Slot{from: nil, to: nil, from_open: true, to_open: true}} def new(from, nil) do w = wrap(from) {:ok, %Tempus.Slot{from: w.from, to: nil, from_open: w.from_open, to_open: true}} end def new(nil, to) do w = wrap(to) {:ok, %Tempus.Slot{from: nil, to: w.to, from_open: true, to_open: w.to_open}} end def new(from, to), do: {:ok, [from, to] |> Enum.map(&wrap/1) |> join()} @spec new!(from :: origin(), to :: origin()) :: t() | no_return @doc """ Creates new slot using `from` as a starting origin and `to` and an ending origin. Unlike `new/1`, this function raises on malformed input. ## Examples iex> Tempus.Slot.new!(~U|2015-09-30 00:00:00Z|, ~U|2015-10-01 01:00:00Z|) %Tempus.Slot{from: ~U|2015-09-30 00:00:00Z|, to: ~U|2015-10-01 01:00:00Z|, from_open: false, to_open: false} iex> Tempus.Slot.new!(~D|2015-09-30|, ~U|2015-10-01T12:00:00Z|) %Tempus.Slot{from: ~U|2015-09-30 00:00:00.000000Z|, to: ~U|2015-10-01 12:00:00Z|, from_open: false, to_open: false} iex> Tempus.Slot.new!(:ok, :ok) ** (ArgumentError) malformed from/to argument, expected `origin` """ def new!(from, to) do case new(from, to) do {:ok, slot} -> slot {:error, :invalid_input} -> raise ArgumentError, message: "malformed from/to argument, expected `origin`" end end @doc """ Helper macro to pattern-match void slots. """ defmacro void do quote do %Slot{from: nil, to: nil, from_open: true, to_open: true} end end @doc "Identity element, void slot `~I(nil → nil)`" @spec id :: %{__struct__: Slot, from: nil, to: nil, from_open: true, to_open: true} def id, do: %Slot{from: nil, to: nil, from_open: true, to_open: true} @spec valid?(slot :: Slot.t()) :: boolean() @doc """ Checks whether the `Slot` is valid (to > from, or to == from if closed point) or not. ## Examples iex> slot = %Tempus.Slot{from: ~U|2015-09-30 00:00:00Z|, to: ~U|2015-10-01 01:00:00Z|} iex> Tempus.Slot.valid?(slot) true iex> Tempus.Slot.valid?(%Tempus.Slot{from: slot.to, to: slot.from}) false iex> slot = %Tempus.Slot{from: nil, to: ~U|2015-10-01 01:00:00Z|} ...> Tempus.Slot.valid?(slot) true iex> slot = %Tempus.Slot{from: ~U|2015-09-30 00:00:00Z|, to: nil} ...> Tempus.Slot.valid?(slot) true iex> Tempus.Slot.valid?(:ok) false """ def valid?(%Slot{from: nil, to: nil}), do: true def valid?(%Slot{from: nil, to: %DateTime{}}), do: true def valid?(%Slot{from: %DateTime{}, to: nil}), do: true def valid?(%Slot{from: %DateTime{} = from, to: %DateTime{} = to, from_open: fo, to_open: to_op}) do case DateTime.compare(from, to) do :lt -> true :eq -> not fo and not to_op :gt -> false end end def valid?(_), do: false @doc """ Splits the slot given as a first argument to two on borders given as a second slot. ## Examples iex> outer = Tempus.Slot.wrap(~D[2023-04-12]) ...> {:ok, inner} = Tempus.Slot.new(~U[2023-04-12 12:00:00Z], ~U[2023-04-12 13:00:00Z]) iex> Tempus.Slot.xor(outer, inner) [%Tempus.Slot{from: ~U[2023-04-12 00:00:00.000000Z], to: ~U[2023-04-12 12:00:00Z], from_open: false, to_open: true}, %Tempus.Slot{from: ~U[2023-04-12 13:00:00Z], to: ~U[2023-04-13 00:00:00.000000Z], from_open: true, to_open: true}] iex> Tempus.Slot.xor(outer, inner) == Tempus.Slot.xor(inner, outer) false iex> {:ok, past} = Tempus.Slot.new(~U[2020-04-12 12:00:00Z], ~U[2020-04-12 13:00:00Z]) ...> Tempus.Slot.xor(past, inner) [past, inner] ...> Tempus.Slot.xor(inner, past) [past, inner] iex> {:ok, border} = Tempus.Slot.new(~U[2023-04-12 11:00:00Z], ~U[2023-04-12 12:00:00Z]) ...> Tempus.Slot.xor(border, inner) [Tempus.Slot.new!(~U[2023-04-12 11:00:00Z], ~U[2023-04-12 13:00:00Z])] """ @spec xor(outer :: Slot.t(), inner :: Slot.t()) :: [Slot.t()] def xor(outer, inner) when is_slot_coming_before(outer, inner), do: [outer, inner] def xor(outer, inner) when is_slot_coming_before(inner, outer), do: [inner, outer] def xor(outer, inner) when is_slot_border(inner.from, outer) or is_slot_border(inner.to, outer), do: [Slot.join(inner, outer)] def xor(outer, inner) do s1 = %Slot{ from: outer.from, to: inner.from, from_open: outer.from_open, to_open: not inner.from_open } s2 = %Slot{ from: inner.to, to: outer.to, from_open: not inner.to_open, to_open: outer.to_open } Enum.filter([s1, s2], &valid?/1) end @spec cover?(slot :: Slot.t(), dt :: origin(), strict? :: boolean()) :: boolean() @doc """ Checks whether the `Slot` covers the date/datetime passed as a second argument. ## Examples iex> dt_between = ~U|2015-09-30 01:00:00Z| ...> dt_from = ~U|2015-09-30 00:00:00Z| ...> dt_to = ~U|2015-10-01 01:00:00Z| ...> d_from = Date.from_iso8601!("2015-09-30") ...> d_to = Date.from_iso8601!("2015-10-01") iex> slot = %Tempus.Slot{from: dt_from, to: dt_to} iex> Tempus.Slot.cover?(slot, dt_between) true iex> Tempus.Slot.cover?(slot, dt_to) false iex> Tempus.Slot.cover?(slot, dt_from) true iex> Tempus.Slot.cover?(slot, d_from) true iex> Tempus.Slot.cover?(slot, ~U|2000-01-01 00:00:00Z|) false iex> Tempus.Slot.cover?(slot, d_to) false """ def cover?(slot, dt, strict? \\ false) def cover?(%Slot{} = slot, %DateTime{} = dt, _) when not is_datetime_covered(dt, slot), do: false def cover?(%Slot{} = slot, %DateTime{} = dt, true) when is_slot_border(dt, slot), do: false def cover?(%Slot{}, %DateTime{}, _), do: true def cover?(%Slot{} = slot, %Slot{} = dt, _) when not is_slot_covered(dt, slot), do: false def cover?(%Slot{} = slot, %Slot{from: from, to: to}, true) when is_slot_border(from, slot) or is_slot_border(to, slot), do: false def cover?(%Slot{}, %Slot{}, _), do: true def cover?(%Slot{} = slot, origin, strict?), do: cover?(slot, wrap(origin), strict?) @spec disjoint?(s1 :: origin(), s2 :: origin()) :: boolean() @doc """ Returns `true` if two slots are disjoined, `false` otherwise. ## Examples iex> slot = %Tempus.Slot{from: ~U|2015-09-01 00:00:00Z|, to: ~U|2015-10-01 00:00:00Z|} iex> inner = %Tempus.Slot{from: ~U|2015-09-01 00:00:00Z|, to: ~U|2015-09-01 01:00:00Z|} iex> Tempus.Slot.disjoint?(slot, inner) false iex> outer = %Tempus.Slot{from: ~U|2015-10-01 00:00:01Z|, to: ~U|2015-10-01 01:00:00Z|} iex> Tempus.Slot.disjoint?(slot, outer) true iex> Tempus.Slot.disjoint?(~D|2000-01-01|, ~U|2015-10-01 00:00:01Z|) true """ def disjoint?(%Slot{} = s1, %Slot{} = s2) when is_joint(s1, s2), do: false def disjoint?(%Slot{}, %Slot{}), do: true def disjoint?(s1, s2), do: disjoint?(wrap(s1), wrap(s2)) @doc """ Returns `true` if two slots are neighbours (touch at a boundary), `false` otherwise. ## Examples iex> slot = Tempus.Slot.wrap(~D|2015-09-01|) iex> Tempus.Slot.neighbour?(slot, Tempus.Slot.wrap(~D|2015-09-02|)) true iex> Tempus.Slot.neighbour?(slot, Tempus.Slot.wrap(~D|2015-08-31|)) true iex> Tempus.Slot.neighbour?(slot, Tempus.Slot.wrap(~D|2015-09-03|)) false """ @spec neighbour?(s1 :: origin(), s2 :: origin()) :: boolean() def neighbour?(s1, s2) do s1 = wrap(s1) s2 = wrap(s2) (not is_nil(s1.to) and not is_nil(s2.from) and is_datetime_equal(s1.to, s2.from)) or (not is_nil(s2.to) and not is_nil(s1.from) and is_datetime_equal(s2.to, s1.from)) end @spec intersect(slots :: Enum.t()) :: Slot.t() | nil @doc """ Intersects slots to the minimal covered timeslice. ### Example iex> Tempus.Slot.intersect([Tempus.Slot.id(), Tempus.Slot.id()]) Tempus.Slot.id() iex> Tempus.Slot.intersect([%Tempus.Slot{from: nil, to: ~U[2020-09-30 23:00:00Z]}, ...> %Tempus.Slot{from: nil, to: ~U[2020-09-30 23:00:00Z]}]) %Tempus.Slot{from: nil, to: ~U[2020-09-30 23:00:00Z], from_open: true, to_open: true} iex> Tempus.Slot.intersect([~D|2020-09-30|, Tempus.Slot.id()]) %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: ~U[2020-10-01 00:00:00.000000Z], from_open: false, to_open: true} """ def intersect(slots) do Enum.reduce(slots, fn _slot, nil -> nil slot, void() -> wrap(slot) void(), slot -> wrap(slot) slot, acc -> slot = wrap(slot) acc = wrap(acc) if disjoint?(acc, slot) do nil else {from, fo} = intersect_from(slot, acc) {to, to_op} = intersect_to(slot, acc) %Slot{from: from, to: to, from_open: fo, to_open: to_op} end end) end @spec intersect_from(Slot.t(), Slot.t()) :: {DateTime.t() | nil, boolean()} defp intersect_from(%Slot{from: nil}, %Slot{from: nil}), do: {nil, true} defp intersect_from(%Slot{from: f1, from_open: fo1}, %Slot{from: nil}), do: {f1, fo1} defp intersect_from(%Slot{from: nil}, %Slot{from: f2, from_open: fo2}), do: {f2, fo2} defp intersect_from(%Slot{from: f1, from_open: fo1}, %Slot{from: f2, from_open: fo2}) do case DateTime.compare(f1, f2) do :gt -> {f1, fo1} :lt -> {f2, fo2} :eq -> {f1, fo1 or fo2} end end @spec intersect_to(Slot.t(), Slot.t()) :: {DateTime.t() | nil, boolean()} defp intersect_to(%Slot{to: nil}, %Slot{to: nil}), do: {nil, true} defp intersect_to(%Slot{to: t1, to_open: to1}, %Slot{to: nil}), do: {t1, to1} defp intersect_to(%Slot{to: nil}, %Slot{to: t2, to_open: to2}), do: {t2, to2} defp intersect_to(%Slot{to: t1, to_open: to1}, %Slot{to: t2, to_open: to2}) do case DateTime.compare(t1, t2) do :lt -> {t1, to1} :gt -> {t2, to2} :eq -> {t1, to1 or to2} end end @spec join(slots :: Enum.t()) :: Slot.t() @doc """ Joins slots to the maximal covered timeslice. ### Example iex> Tempus.Slot.join([]) Tempus.Slot.id() iex> Tempus.Slot.join([Tempus.Slot.wrap(~D|2020-09-30|), Tempus.Slot.wrap(~D|2020-10-02|)]) %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: ~U[2020-10-03 00:00:00.000000Z], from_open: false, to_open: true} iex> Tempus.Slot.join([~D|2020-09-30|, ~D|2020-10-02|]) %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: ~U[2020-10-03 00:00:00.000000Z], from_open: false, to_open: true} """ def join([]), do: void() def join([slot | slots]), do: do_join(slots, wrap(slot)) defp do_join([], acc), do: acc defp do_join(any, void()), do: join(any) defp do_join([void() | slots], acc), do: do_join(slots, acc) defp do_join([slot | slots], acc) do slot = wrap(slot) {from, from_open} = cond do is_nil(slot.from) or is_nil(acc.from) -> {nil, true} DateTime.compare(slot.from, acc.from) == :lt -> {slot.from, slot.from_open} DateTime.compare(slot.from, acc.from) == :gt -> {acc.from, acc.from_open} true -> {slot.from, slot.from_open and acc.from_open} end {to, to_open} = cond do is_nil(slot.to) or is_nil(acc.to) -> {nil, true} DateTime.compare(slot.to, acc.to) == :gt -> {slot.to, slot.to_open} DateTime.compare(slot.to, acc.to) == :lt -> {acc.to, acc.to_open} true -> {slot.to, slot.to_open and acc.to_open} end do_join(slots, %Slot{from: from, to: to, from_open: from_open, to_open: to_open}) end @spec join(Slot.t(), Slot.t()) :: Slot.t() @doc """ Joins two slots to the maximal covered timeslice. ### Example iex> Tempus.Slot.join(Tempus.Slot.wrap(~D|2020-09-30|), Tempus.Slot.wrap(~D|2020-10-02|)) %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: ~U[2020-10-03 00:00:00.000000Z], from_open: false, to_open: true} iex> Tempus.Slot.join(~D|2020-09-30|, ~D|2020-10-02|) %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: ~U[2020-10-03 00:00:00.000000Z], from_open: false, to_open: true} """ def join(s1, s2), do: join([s1, s2]) @spec duration(slot :: Slot.origin(), unit :: System.time_unit()) :: non_neg_integer() | :infinity @doc """ Calculates the duration of a slot in units given as a second parameter (default: `:second`.) ### Example iex> Tempus.Slot.duration(~D|2020-09-03|) 86400 iex> Tempus.Slot.duration(Tempus.Slot.id()) 0 iex> Tempus.Slot.duration(%Tempus.Slot{from: nil, to: DateTime.utc_now()}) :infinity iex> Tempus.Slot.duration(%Tempus.Slot{from: DateTime.utc_now(), to: nil}) :infinity """ def duration(slot, unit \\ :second) def duration(slot, unit) when not is_struct(slot, Slot), do: slot |> wrap() |> duration(unit) def duration(void(), _), do: 0 def duration(%Slot{from: nil, to: %DateTime{}}, _), do: :infinity def duration(%Slot{from: %DateTime{}, to: nil}, _), do: :infinity def duration(%Slot{from: %DateTime{} = from, to: %DateTime{} = to}, unit), do: DateTime.diff(to, from, unit) @spec compare(s1 :: origin(), s2 :: origin(), strict :: boolean()) :: :lt | :gt | :eq | :joint @doc """ Compares two slot structs. Returns `:gt` if first slot is strictly later than the second and `:lt` for vice versa. **NB** `:eq` is returned not only if slots are equal, but also when they are overlapped. Might be used in `Enum.sort/2`. ### Examples iex> slot = Tempus.Slot.wrap(~D[2020-09-30]) iex> slot1 = %Tempus.Slot{from: nil, to: ~U[2020-09-30 00:00:00.000000Z]} iex> slot2 = %Tempus.Slot{from: nil, to: DateTime.utc_now()} iex> slot3 = %Tempus.Slot{from: ~U[2020-09-30 00:00:00.000000Z], to: nil} iex> slot4 = %Tempus.Slot{from: DateTime.utc_now(), to: nil} iex> Tempus.Slot.compare(Tempus.Slot.id(), Tempus.Slot.id(), true) :eq iex> Tempus.Slot.compare(slot1, slot2, false) :eq iex> Tempus.Slot.compare(slot1, slot2, true) :joint iex> Tempus.Slot.compare(slot3, slot4, false) :eq iex> Tempus.Slot.compare(slot3, slot4, true) :joint iex> Tempus.Slot.compare(slot, slot, true) :eq iex> Tempus.Slot.compare(slot, DateTime.utc_now(), true) :lt iex> Tempus.Slot.compare(slot, ~D|2000-01-01|, true) :gt iex> Tempus.Slot.compare(slot, slot.from, true) :joint """ def compare(s1, s2, strict \\ false) def compare(value, value, _), do: :eq def compare(_, void(), false), do: :eq def compare(_, void(), true), do: :joint def compare(void(), _, false), do: :eq def compare(void(), _, true), do: :joint def compare(nil, _, _), do: :lt def compare(_, nil, _), do: :lt def compare(%Slot{} = s1, %Slot{} = s2, _) when is_slot_coming_before(s1, s2), do: :lt def compare(%Slot{} = s1, %Slot{} = s2, _) when is_slot_coming_before(s2, s1), do: :gt def compare(%Date{} = d, %DateTime{} = dt, strict), do: compare(Slot.wrap(d), Slot.wrap(dt), strict) def compare(%DateTime{} = dt, %Date{} = d, strict), do: compare(Slot.wrap(dt), Slot.wrap(d), strict) def compare(%Date{} = d, %Slot{} = s, strict), do: compare(Slot.wrap(d), s, strict) def compare(%Slot{} = s, %Date{} = d, strict), do: compare(s, Slot.wrap(d), strict) def compare(%DateTime{} = dt, %Slot{} = s, strict), do: compare(Slot.wrap(dt), s, strict) def compare(%Slot{} = s, %DateTime{} = dt, strict), do: compare(s, Slot.wrap(dt), strict) def compare(%Slot{from: nil, to: %DateTime{}}, %Slot{from: nil, to: %DateTime{}}, false), do: :eq def compare( %Slot{from: nil, to: %DateTime{} = t1}, %Slot{from: nil, to: %DateTime{} = t2}, true ), do: if(DateTime.compare(t1, t2) == :eq, do: :eq, else: :joint) def compare(%Slot{from: %DateTime{}, to: nil}, %Slot{from: %DateTime{}, to: nil}, false), do: :eq def compare( %Slot{from: %DateTime{} = f1, to: nil}, %Slot{from: %DateTime{} = f2, to: nil}, true ), do: if(DateTime.compare(f1, f2) == :eq, do: :eq, else: :joint) def compare(%Slot{from: f1, to: t1}, %Slot{from: f2, to: t2}, strict) do f2l = t1 && f2 && DateTime.compare(t1, f2) l2f = f1 && t2 && DateTime.compare(f1, t2) case {strict, f2l, l2f} do {_, :lt, _} -> :lt {_, _, :gt} -> :gt {false, _, _} -> :eq {true, nil, _} -> :joint {true, _, nil} -> :joint {true, _, _} -> if DateTime.compare(f1, f2) == :eq && DateTime.compare(t1, t2) == :eq, do: :eq, else: :joint end end def compare(s1, s2, strict), do: compare(wrap(s1), wrap(s2), strict) @spec strict_compare(s1 :: Slot.origin(), s2 :: Slot.origin()) :: :eq | :lt | :gt | :joint @doc """ Compares two slot structs. The same as `compare/2`, but returns `:joint` if the slots are overlapped. ### Examples iex> Tempus.Slot.strict_compare(~D|2020-01-01|, DateTime.utc_now()) :lt """ def strict_compare(s1, s2) when is_origin(s1) and is_origin(s2), do: compare(s1, s2, true) @spec wrap(origin(), DateTime.t()) :: Slot.t() @doc """ Wraps the argument into a slot. For `DateTime` it'd be a point slot. For a `Date`, it would be the whole day $[00:00:00.000000 \to 00:00:00.000000\text{ next day})$. ## Examples iex> Tempus.Slot.wrap(~D|2020-08-06|) %Tempus.Slot{from: ~U[2020-08-06 00:00:00.000000Z], to: ~U[2020-08-07 00:00:00.000000Z], from_open: false, to_open: true} iex> Tempus.Slot.wrap(:ok) Tempus.Slot.id() """ def wrap(moment \\ nil, origin \\ DateTime.utc_now()) def wrap(nil, origin), do: wrap(DateTime.utc_now(), origin) def wrap(%Slot{} = slot, _), do: slot def wrap(%DateTime{} = dt, _), do: %Slot{from: dt, to: dt, from_open: false, to_open: false} def wrap( %Time{ calendar: calendar, hour: hour, microsecond: microsecond, minute: minute, second: second }, origin ) do wrap(%DateTime{ calendar: calendar, day: origin.day, hour: hour, microsecond: microsecond, minute: minute, month: origin.month, second: second, std_offset: origin.std_offset, time_zone: origin.time_zone, utc_offset: origin.utc_offset, year: origin.year, zone_abbr: origin.zone_abbr }) end def wrap(%Date{calendar: calendar, day: day, month: month, year: year} = date, origin) do next_date = Date.add(date, 1) %Slot{ from: %DateTime{ calendar: calendar, day: day, hour: 0, microsecond: {0, 6}, minute: 0, month: month, second: 0, std_offset: origin.std_offset, time_zone: origin.time_zone, utc_offset: origin.utc_offset, year: year, zone_abbr: origin.zone_abbr }, to: %DateTime{ calendar: calendar, day: next_date.day, hour: 0, microsecond: {0, 6}, minute: 0, month: next_date.month, second: 0, std_offset: origin.std_offset, time_zone: origin.time_zone, utc_offset: origin.utc_offset, year: next_date.year, zone_abbr: origin.zone_abbr }, from_open: false, to_open: true } end def wrap(_, _), do: void() @doc false @spec shift( slot :: t(), action :: [ {:to, integer()} | {:from, integer()} | {:by, integer()} | {:unit, System.time_unit()} ] ) :: Slot.t() def shift(%Slot{from: from, to: to, from_open: fo, to_open: to_op}, action \\ []) do {multiplier, unit} = case Keyword.get(action, :unit, :microsecond) do :day -> {60 * 60 * 24 * 1_000_000, :microsecond} :hour -> {60 * 60 * 1_000_000, :microsecond} :minute -> {60 * 1_000_000, :microsecond} other -> {1, other} end [by_from, by_to] = action |> Keyword.get(:by) |> case do nil -> Enum.map([:from, :to], &Keyword.get(action, &1, 0)) value -> [value, value] end |> Enum.map(&(&1 * multiplier)) check_shifted(do_shift(from, by_from, unit), do_shift(to, by_to, unit), fo, to_op) end @spec check_shifted(maybe_datetime, maybe_datetime, boolean(), boolean()) :: Slot.t() when maybe_datetime: nil | DateTime.t() defp check_shifted(nil, nil, _fo, _to_op), do: %Slot{from: nil, to: nil, from_open: true, to_open: true} defp check_shifted(nil, to, _fo, to_op), do: %Slot{from: nil, to: to, from_open: true, to_open: to_op} defp check_shifted(from, nil, fo, _to_op), do: %Slot{from: from, to: nil, from_open: fo, to_open: true} defp check_shifted(%DateTime{} = from, %DateTime{} = to, fo, to_op) do if DateTime.compare(to, from) == :lt do void() else %Slot{from: from, to: to, from_open: fo, to_open: to_op} end end @spec do_shift(maybe_datetime, integer(), System.time_unit()) :: maybe_datetime when maybe_datetime: nil | DateTime.t() defp do_shift(nil, _, _), do: nil defp do_shift(%DateTime{microsecond: {_, 0}} = dt, count, unit), do: %DateTime{dt | microsecond: {0, 6}} |> DateTime.truncate(unit) |> DateTime.add(count, unit) defp do_shift(%DateTime{microsecond: {value, n}} = dt, count, unit), do: %DateTime{dt | microsecond: {:erlang.rem(value, round(:math.pow(10, n))), Enum.max([6, 6])}} |> DateTime.truncate(unit) |> DateTime.add(count, unit) @spec shift_tz( slot :: Slot.t(), tz :: Calendar.time_zone(), tz_db :: Calendar.time_zone_database() ) :: Slot.t() @doc """ Shifts both `from` and `to` values to `UTC` zone. ### Examples ```elixir slot = %Tempus.Slot{ from: DateTime.from_naive!(~N|2018-01-05 21:00:00|, "America/New_York"), to: DateTime.from_naive!(~N|2018-01-08 08:59:59|, "Australia/Sydney") } #⇒ %Tempus.Slot{from: ~U[2018-01-06 02:00:00Z], to: ~U[2018-01-07 21:59:59Z], from_open: false, to_open: true} ``` """ def shift_tz( %Slot{from: from, to: to, from_open: fo, to_open: to_op}, tz \\ "Etc/UTC", tz_db \\ Calendar.get_time_zone_database() ) do %Slot{ from: from && DateTime.shift_zone!(from, tz, tz_db), to: to && DateTime.shift_zone!(to, tz, tz_db), from_open: fo, to_open: to_op } end @spec gap([t()]) :: t() @doc false def gap([ %Slot{to: from, to_open: from_open} = prev, %Slot{from: to, from_open: to_open} = next ]) when is_slot_coming_before(prev, next) do %Slot{from: from, to: to, from_open: not from_open, to_open: not to_open} end def gap([%Slot{} = prev, %Slot{} = next]) when is_slot_coming_before(next, prev), do: gap([next, prev]) def gap([%Slot{from: nil, to: from, to_open: from_open}]), do: %Slot{from: from, to: nil, from_open: not from_open, to_open: true} def gap([%Slot{from: to, from_open: to_open, to: nil}]), do: %Slot{from: nil, to: to, from_open: true, to_open: not to_open} def gap(_), do: void() defimpl Inspect do @moduledoc false import Inspect.Algebra @fancy_inspect Application.compile_env(:tempus, :inspect, :sigil) defp open_bracket(true), do: "(" defp open_bracket(false), do: "[" defp close_bracket(true), do: ")" defp close_bracket(false), do: "]" defp value(from, to, _opts) do Enum.map_join([from, to], " → ", fn nil -> "∞" dt -> DateTime.to_iso8601(dt) end) end def inspect( %Tempus.Slot{from: from, to: to, from_open: fo, to_open: to_op}, %Inspect.Opts{custom_options: [_ | _]} = opts ) do opts.custom_options |> Keyword.get(:fancy, @fancy_inspect) |> case do truthy when truthy in [:emoji, true] -> tag = case truthy do :emoji -> "⌚" true -> "𝕥" end concat([tag, open_bracket(fo), value(from, to, opts), close_bracket(to_op)]) false -> concat([ "#Slot<", to_doc([from: from, to: to, from_open: fo, to_open: to_op], opts), ">" ]) :sigil -> case {from, to} do {nil, nil} -> "%Tempus.Slot{}" {from, nil} -> "%Tempus.Slot{from: " <> inspect(from) <> "}" {nil, to} -> "%Tempus.Slot{to: " <> inspect(to) <> "}" {from, to} -> "~I" <> open_bracket(fo) <> "#{from}|#{to}" <> close_bracket(to_op) end end end def inspect(%Tempus.Slot{from: from, to: to, from_open: fo, to_open: to_op}, opts) do concat(["~I", open_bracket(fo), value(from, to, opts), close_bracket(to_op)]) end end end