lib/runbox/state_store/schedule_utils.ex

defmodule Runbox.StateStore.ScheduleUtils do
  @moduledoc """
  Module contains functions to compute savepoint timestamps.

  Savepoint timestamps are calculated as multiples of `schedule`.
  """

  @typedoc "Timestamp defined as unix epoch in milliseconds"
  @type epoch_ms :: non_neg_integer

  @typedoc "Difference between two unix epoch timestamps in milliseconds"
  @type epoch_ms_interval :: non_neg_integer

  @typedoc "Interval (in seconds) between savepoint timestamps."
  @type schedule :: epoch_ms_interval | :none

  @doc """
  Returns next savepoint based on given `schedule` and `timestamp`.

  Next savepoint is calculated as next multiple of `schedule` larger or equal to given
  `timestamp`.

  ## Examples
  ```
  iex> Runbox.StateStore.ScheduleUtils.next_savepoint(60_000, 0)
  60_000

  iex> Runbox.StateStore.ScheduleUtils.next_savepoint(60_000, 10_000)
  60_000

  iex> Runbox.StateStore.ScheduleUtils.next_savepoint(60_000, 60_000)
  120_000
  ```
  """
  @spec next_savepoint(schedule(), epoch_ms()) :: epoch_ms()
  def next_savepoint(schedule, timestamp) do
    previous_savepoint(schedule, timestamp) + schedule
  end

  @doc """
  Returns previous savepoint based on given `schedule` and `timestamp`.

  Next savepoint is calculated as previous multiple of `schedule` lower or equal to given
  `timestamp`.

  ## Examples
  ```
  iex> Runbox.StateStore.ScheduleUtils.previous_savepoint(60_000, 30_000)
  0

  iex> Runbox.StateStore.ScheduleUtils.previous_savepoint(60_000, 60_000)
  60_000

  iex> Runbox.StateStore.ScheduleUtils.previous_savepoint(60_000, 90_000)
  60_000

  iex> Runbox.StateStore.ScheduleUtils.previous_savepoint(60_000, 120_000)
  120_000

  iex> Runbox.StateStore.ScheduleUtils.previous_savepoint(60_000, 150_000)
  120_000
  ```
  """
  @spec previous_savepoint(schedule(), epoch_ms()) :: epoch_ms()
  def previous_savepoint(schedule, timestamp) do
    previous_savepoint = div(timestamp, schedule) * schedule

    if previous_savepoint <= 0 do
      0
    else
      previous_savepoint
    end
  end

  @doc """
  Returns count of unreached savepoints.

  ## Examples
  ```
  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints_count(60_000, 0, 120_000)
  2

  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints_count(60_000, 60_000, 120_000)
  1

  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints_count(1, 0, 120_000)
  120_000
  ```
  """
  def unreached_savepoints_count(schedule, from, to) do
    ((to - from) / schedule)
    |> Float.floor(0)
    |> Kernel.round()
  end

  @doc """
  Returns unreached savepoints based on given `schedule` in range of given timestamps `(from; to>`

  ## Examples
  ```
  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints(60_000, 0, 120_000)
  [60_000, 120_000]

  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints(60_000, 60_000, 120_000)
  [120_000]

  iex> Runbox.StateStore.ScheduleUtils.unreached_savepoints(60_000, 60_000, 180_000)
  [120_000, 180_000]
  ```
  """
  @spec unreached_savepoints(schedule(), epoch_ms(), epoch_ms()) :: [
          epoch_ms()
        ]
  def unreached_savepoints(schedule, from, to) do
    get_unreached_savepoints(schedule, from, to, [])
  end

  @spec get_unreached_savepoints(
          schedule(),
          epoch_ms(),
          epoch_ms(),
          [epoch_ms()]
        ) :: [
          epoch_ms()
        ]
  defp get_unreached_savepoints(schedule, from, to, savepoints) do
    from_next = next_savepoint(schedule, from)

    if from_next <= to do
      get_unreached_savepoints(schedule, from_next, to, [from_next | savepoints])
    else
      Enum.reverse(savepoints)
    end
  end
end