Current section
Files
Jump to
Current section
Files
lib/cron/cron.ex
defmodule Cron do
@moduledoc false
import Kernel, except: [match?: 2]
alias Cron.Calc
alias Cron.Parser
defstruct expression: "0 * * * * *",
second: 0,
minute: 0..59,
hour: 0..23,
day: 1..31,
month: 1..12,
day_of_week: 0..6
@type t :: %Cron{
expression: String.t(),
second: 0..59 | [0..59, ...] | Range.t(0..59, 0..59),
minute: 0..59 | [0..59, ...] | Range.t(0..59, 0..59),
hour: 0..23 | [0..23, ...] | Range.t(0..23, 0..23),
day: 1..31 | [1..31, ...] | Range.t(1..31, 1..31),
month: 1..12 | [1..13, ...] | Range.t(1..12, 1..12),
day_of_week: 0..6 | [0..6, ...] | Range.t(0..6, 0..6)
}
@type expression :: String.t()
@type reason :: atom() | [{atom(), String.t()}]
@type millisecond :: pos_integer()
@doc """
Returns an `:ok` tuple with a cron struct for the given expression string. If
the expression is invalid an `:error` will be returned.
Will accept expression with 6 (including `second`) and 5 (`second: 0`) fields.
## Examples
iex> {:ok, cron} = Cron.new("1 2 3 * *")
iex> cron
#Cron<1 2 3 * *>
iex> {:ok, cron} = Cron.new("0 1 2 3 * *")
iex> cron
#Cron<0 1 2 3 * *>
iex> Cron.new("66 1 2 3 * *")
{:error, second: "66"}
"""
@spec new(expression()) :: {:ok, Cron.t()} | :error | {:error, reason}
def new(string) do
with {:ok, data} <- Parser.run(string) do
{:ok, struct!(Cron, Keyword.put(data, :expression, string))}
end
end
@doc """
Same as `new/1`, but raises an `ArgumentError` exception in case of an invalid
expression.
"""
@spec new!(expression()) :: Cron.t()
def new!(string) do
case new(string) do
{:ok, cron} ->
cron
:error ->
raise ArgumentError, "invalid cron expression: #{inspect(string)}"
{:error, reason} ->
raise ArgumentError, "invalid cron expression: #{inspect(reason)}"
end
end
@doc """
Returns the next execution datetime.
If the given `datetime` matches `cron`, then also the following datetime is
returning. That means the resulting datetime is always greater than the given.
The function truncates the precision of the given `datetime` to seconds.
## Examples
iex> {:ok, cron} = Cron.new("0 0 0 * * *")
iex> Cron.next(cron, ~U[2022-01-01 12:00:00Z])
~U[2022-01-02 00:00:00Z]
iex> Cron.next(cron, ~U[2022-01-02 00:00:00Z])
~U[2022-01-03 00:00:00Z]
iex> Cron.next(cron, ~U[2022-01-02 00:00:00.999Z])
~U[2022-01-03 00:00:00Z]
"""
@spec next(Cron.t(), DateTime.t() | NaiveDateTime.t()) :: DateTime.t() | NaiveDateTime.t()
def next(cron, datetime \\ NaiveDateTime.utc_now())
def next(
%Cron{} = cron,
%DateTime{calendar: Calendar.ISO, time_zone: "Etc/UTC"} = datetime
) do
cron
|> next(DateTime.to_naive(datetime))
|> from_naive!()
end
def next(%Cron{} = cron, %NaiveDateTime{calendar: Calendar.ISO} = datetime) do
datetime
|> NaiveDateTime.truncate(:second)
|> Calc.next(cron)
end
@doc """
Same as `next/3`, but returns the milliseconds until next execution datetime.
## Examples
iex> {:ok, cron} = Cron.new("0 0 0 * * *")
iex> Cron.until(cron, ~U[2022-01-01 12:00:00Z])
43200000
iex> Cron.until(cron, ~U[2022-01-02 00:00:00Z])
86400000
iex> Cron.until(cron, ~U[2022-01-02 00:00:00.999Z])
86399001
"""
@spec until(Cron.t(), DateTime.t() | NaiveDateTime.t()) :: millisecond
def until(%Cron{} = cron, datetime \\ DateTime.utc_now()) do
cron
|> next(datetime)
|> NaiveDateTime.diff(datetime, :millisecond)
end
def from_naive!(%NaiveDateTime{} = datetime) do
datetime
|> DateTime.from_naive!("Etc/UTC")
end
defimpl Inspect do
@spec inspect(Cron.t(), Inspect.Opts.t()) :: String.t()
def inspect(cron, _opts), do: "#Cron<#{cron.expression}>"
end
defimpl String.Chars do
@spec to_string(Cron.t()) :: String.t()
def to_string(%Cron{} = cron), do: cron.expression
end
end