lib/human_time.ex

defmodule HumanTime do
  @moduledoc """
  Human Time is a function to convert a string such as "every other tuesday", "every weekday" or "next friday at 2pm" and convert it into a one or a sequence of date times as allowed by the string.
  """

  # TODO: Add in cron-tab formatting (for repeating stuff)
  # Some specific examples that demonstrate the full range of expressions:

  #   0 * * * * — The first minute of every hour
  #   */15 9-17 * * * — Every fifteen minutes during standard business hours
  #   0 0 * DEC * — Once a day at midnight during december
  #   0 7-9,4-6 13 * FRI — Once an hour during both rush hours on Friday the 13th

  alias HumanTime.Repeating
  alias HumanTime.Relative
  require Logger

  @doc """
  Generates a stream of datetimes for the string given.

  ## Options
  `from` The datetime from when the sequence will be generated, defaults to the current time.

  `until` The datetime when the sequence will be terminated, defaults to nil. When nil the sequence will never be terminated.

  ## Example
      HumanTime.repeating("Every wednesday at 1530")
      |> Stream.take(3)
      |> Enum.to_list

      #=> [
      #=>   #DateTime<2018-08-15 15:30:00.848218Z>,
      #=>   #DateTime<2018-08-22 15:30:00.848218Z>,
      #=>   #DateTime<2018-08-29 15:30:00.848218Z>
      #=> ]

  ## Error result is:
    {:error, message}
  """
  @spec repeating(String.t(), [term]) :: {:ok, Enumerable.t()} | {:error, String.t()}
  def repeating(timestring, opts \\ []) do
    # Logger.debug "Calling repeating with timestring: #{timestring} and opts: #{Kernel.inspect opts}"

    from = opts[:from] || Timex.now()
    until = opts[:until]

    while_function = Repeating.Generators.while_function(until)
    result = Repeating.Parser.build_functions(timestring)

    case result do
      {:error, msg} ->
        {:error, msg}

      {:ok, {generator_function, filter_function, mapper_function}} ->
        {:ok,
         from
         |> Stream.iterate(generator_function)
         |> Stream.take_while(while_function)
         |> Stream.filter(filter_function)
         |> Stream.map(mapper_function)
         |> Stream.filter(&(&1 != nil))}
    end
  end

  @doc """
  Repeats the time string and raises an exception in case of errors
  """
  @spec repeating!(String.t(), [term]) :: Enumerable.t()
  def repeating!(timestring, opts \\ []) do
    case repeating(timestring, opts) do
      {:ok, enumerable} -> enumerable
      {:error, msg} -> raise msg
    end
  end

  @doc """
  Generates a single datetime for the string given.

  ## Options
  `from` The datetime from when the sequence will be generated, defaults to the current time.

  ## Example
      HumanTime.relative("Next wednesday at 1530")

      #=> {:ok, #DateTime<2018-08-15 15:30:00.848218Z>}

  ## Error result is:
      #=> {:error, message}
  """
  @spec relative(String.t(), [term]) :: {:ok, DateTime.t() | nil} | {:error, String.t()}
  def relative(timestring, opts \\ []) do
    from = opts[:from] || Timex.now()

    Relative.Parser.parse(timestring, from)
  end

  @doc """
  Creates the time string and raises an exception in case of errors
  """
  @spec relative!(String.t(), [term]) :: DateTime.t() | nil
  def relative!(timestring, opts \\ []) do
    case relative(timestring, opts) do
      {:ok, the_datetime} -> the_datetime
      {:error, msg} -> raise msg
    end
  end
end