lib/ex_teal/metric/ranges.ex

defmodule ExTeal.Metric.Ranges do
  @moduledoc """
  Helper functions for building time intervals and
  queries based on the params of a request.
  """

  use Timex
  import Ecto.Query
  alias ExTeal.Metric.Request

  @doc """
  Parse the start and end params
  """
  @spec get_aggregate_datetimes(request :: Request.t()) ::
          {start_dt :: DateTime.t(), end_dt :: DateTime.t()}
  def get_aggregate_datetimes(%Request{unit: unit} = request) do
    request
    |> to_datetimes()
    |> aggregate_as(unit)
  end

  defp to_datetimes(%Request{start_at: start_param, end_at: end_param}) do
    {
      Timex.parse!(start_param, "{ISO:Extended}"),
      Timex.parse!(end_param, "{ISO:Extended}")
    }
  end

  defp aggregate_as({start_at, end_at}, "year") do
    {
      Timex.beginning_of_year(start_at),
      Timex.end_of_year(end_at)
    }
  end

  defp aggregate_as({start_at, end_at}, "month") do
    {
      Timex.beginning_of_month(start_at),
      Timex.end_of_month(end_at)
    }
  end

  defp aggregate_as({start_at, end_at}, "week") do
    {
      Timex.beginning_of_week(start_at),
      Timex.end_of_week(end_at)
    }
  end

  defp aggregate_as({start_at, end_at}, "day") do
    {
      Timex.beginning_of_day(start_at),
      Timex.end_of_day(end_at)
    }
  end

  defp aggregate_as({start_at, end_at}, "hour") do
    {
      beginning_of_hour(start_at),
      end_of_hour(end_at)
    }
  end

  defp aggregate_as({start_at, end_at}, "minute") do
    {
      beginning_of_minute(start_at),
      end_of_minute(end_at)
    }
  end

  @type between_options :: [start_dt: DateTime.t(), end_dt: DateTime.t(), metric: module()]

  @doc """
  Set the boundaries of an aggregate queries
  """
  @spec between(Ecto.Queryable.t(), between_options()) :: Ecto.Queryable.t()
  def between(query, start_dt: start, end_dt: end_dt, metric: metric) do
    dt_field = metric.date_field()

    field_type = metric.date_field_type()
    start_time = to_dt_field_type(start, field_type)
    end_time = to_dt_field_type(end_dt, field_type)

    query
    |> where([q], field(q, ^dt_field) >= ^start_time)
    |> where([q], field(q, ^dt_field) <= ^end_time)
  end

  @spec parse_dt(String.t(), String.t(), String.t()) :: DateTime.t()
  def parse_dt(param, format, timezone) do
    param
    |> Timex.parse!(format)
    |> DateTime.from_naive!(timezone)
  end

  defp beginning_of_hour(datetime) do
    {date, {h, _m, _s}} = Timex.to_erl(datetime)
    Timex.to_datetime({date, {h, 0, 0}}, datetime.time_zone)
  end

  defp end_of_hour(datetime) do
    {date, {h, _m, _s}} = Timex.to_erl(datetime)
    Timex.to_datetime({date, {h, 59, 59}}, datetime.time_zone)
  end

  defp beginning_of_minute(datetime) do
    {date, {h, m, _s}} = Timex.to_erl(datetime)
    Timex.to_datetime({date, {h, m, 0}}, datetime.time_zone)
  end

  defp end_of_minute(datetime) do
    {date, {h, m, _s}} = Timex.to_erl(datetime)
    Timex.to_datetime({date, {h, m, 59}}, datetime.time_zone)
  end

  defp to_dt_field_type(value, :naive_datetime) do
    utc = Timezone.get("Etc/UTC", value)
    Timezone.convert(value, utc)
  end

  defp to_dt_field_type(value, :utc_datetime) do
    value
  end
end