defmodule Plausible.Stats.DateTimeRange do @moduledoc """ Defines a struct similar `Date.Range`, but with `DateTime` instead of `Date`. The structs should be created with the `new!/2` function. """ @enforce_keys [:first, :last] defstruct [:first, :last] @type t() :: %__MODULE__{ first: %DateTime{}, last: %DateTime{} } @doc """ Creates a `DateTimeRange` struct from the given `%Date{}` structs. The first datetime will become the first date at 00:00:00, and the last datetime will become the last date at 23:59:59. Both dates will be turned into `%DateTime{}` structs in the given timezone. """ def new!(%Date{} = first, last, timezone) do first = case DateTime.new(first, ~T[00:00:00], timezone) do {:ok, datetime} -> datetime {:gap, _just_before, just_after} -> just_after {:ambiguous, _first_datetime, second_datetime} -> second_datetime end new!(first, last, timezone) end def new!(first, %Date{} = last, timezone) do last = case DateTime.new(last, ~T[23:59:59], timezone) do {:ok, datetime} -> datetime {:gap, just_before, _just_after} -> just_before {:ambiguous, first_datetime, _second_datetime} -> first_datetime end new!(first, last, timezone) end def new!(%DateTime{} = first, %DateTime{} = last, _timezone) do new!(first, last) end def new!(%DateTime{} = first, %DateTime{} = last) do first = DateTime.truncate(first, :second) last = DateTime.truncate(last, :second) if DateTime.before?(first, last) do %__MODULE__{first: first, last: last} else %__MODULE__{first: last, last: first} end end def to_timezone(%__MODULE__{first: first, last: last}, timezone) do first = DateTime.shift_zone!(first, timezone) last = DateTime.shift_zone!(last, timezone) %__MODULE__{first: first, last: last} end def to_date_range(datetime_range, timezone) do %__MODULE__{first: first, last: last} = to_timezone(datetime_range, timezone) first = DateTime.to_date(first) last = DateTime.to_date(last) Date.range(first, last) end def length(%__MODULE__{first: first, last: last}, unit) do DateTime.diff(last, first, unit) end end