Cron (Poolder v0.1.10)

View Source

Cron parses cron expressions and calculates execution timings.

Expressions with 6 and 5 fields are expected. The variant with 6 fields adds a field for seconds at the first position. The variant with 5 fields corresponds to the standard Unix behavior. The field second will be set to 0 when Cron.new/1 gets 5 fields.

Possible values for the fields:

+----------- second (0 - 59)
| +--------- minute (0 - 59)
| | +------- hour (0 - 23)
| | | +----- day of the month (1 - 31)
| | | | +--- month (1 - 12)
| | | | | +- day of the week (0 - 6, SUN - SAT)
| | | | | |
* * * * * *

A field may contain an asterisk *, which means any of the possible values.

A range of values can be specified with an -. Example: 10-15.

A / can be used to setup steps. The value before the / specifies the start value and the value behind the / specifies the step width. Example: */2, 4/2.

Multiple values can be declared with a , separated list without any whitespace. Example: 1,5,10-15,*/2.

The fields month and day_of_week are also accept names. Month accepts Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep. Oct, Nov and Dez. The case does not matter.

day_of_week accepts Sun, Mon, Tue, Wed, Thu, Fri and Sat. The case does not matter. Keep in mind that Sun is equal to 0 and Sat is equal to 6.

All Cron functions are working with NaiveDateTime or DateTime that are using the Calendar.ISO. Any DateTime must have the time zone Etc/UTC.

Examples

A cron expression that triggers daily at 12:00:00

iex> "0 12 * * *"
...> |> Cron.new!()
...> |> Cron.next(~U[2021-12-06 22:11:44Z])
~U[2021-12-07 12:00:00Z]

Cron.stream/2 calculates multiple values. The following cron expression triggers ever 30 minutes from 12 to 14 at the first day in a month.

iex> "0 */30 12-14 1 * *"
...> |> Cron.new!()
...> |> Cron.stream(from: ~U[2021-12-06 11:22:33Z])
...> |> Enum.take(8)
[
  ~N[2022-01-01 12:00:00],
  ~N[2022-01-01 12:30:00],
  ~N[2022-01-01 13:00:00],
  ~N[2022-01-01 13:30:00],
  ~N[2022-01-01 14:00:00],
  ~N[2022-01-01 14:30:00],
  ~N[2022-02-01 12:00:00],
  ~N[2022-02-01 12:30:00]
]

Summary

Functions

Returns true if the given datetime matches the cron.

Returns an :ok tuple with a cron struct for the given expression string. If the expression is invalid an :error will be returned.

Same as new/1, but raises an ArgumentError exception in case of an invalid expression.

Returns the next execution datetime.

Returns an :ok tuple with the next execution datetime for which fun returns a truthy value.

Same as next_while/3, but raises a RuntimeError exception in case no execution datetime can be found.

Returns the previous execution datetime.

Returns an :ok tuple with the previous execution datetime for which fun returns a truthy value.

Same as previous_while/3, but raises a RuntimeError exception in case no execution datetime can be found.

Same as previous/3, but returns the milliseconds since last execution datetime.

Same as previous_while/3, but returns the milliseconds since last execution datetime.

Same as since_while!/3, but raises a RuntimeError exception in case no execution datetime can be found.

Returns a Stream for the given cron.

Same as next/3, but returns the milliseconds until next execution datetime.

Same as next_while/3, but returns the milliseconds until next execution datetime.

Same as until_while!/3, but raises a RuntimeError exception in case no execution datetime can be found.

Types

expression()

@type expression() :: String.t()

millisecond()

@type millisecond() :: pos_integer()

reason()

@type reason() :: atom() | [{atom(), String.t()}]

t()

@type t() :: %Cron{
  day: 1..31 | [1..31, ...] | Range.t(1..31, 1..31),
  day_of_week: 0..6 | [0..6, ...] | Range.t(0..6, 0..6),
  expression: String.t(),
  hour: 0..23 | [0..23, ...] | Range.t(0..23, 0..23),
  minute: 0..59 | [0..59, ...] | Range.t(0..59, 0..59),
  month: 1..12 | [1..13, ...] | Range.t(1..12, 1..12),
  second: 0..59 | [0..59, ...] | Range.t(0..59, 0..59)
}

Functions

match?(cron, datetime \\ NaiveDateTime.utc_now())

@spec match?(t(), DateTime.t() | NaiveDateTime.t()) :: boolean()

Returns true if the given datetime matches the cron.

The function truncates the precision of the given datetime to seconds.

Examples

iex> cron = Cron.new!("0 * * * *")
iex> Cron.match?(cron, ~U[2021-11-13 06:41:39Z])
false
iex> Cron.match?(cron, ~U[2021-11-13 13:00:00Z])
true
iex> Cron.match?(cron, ~U[2021-11-13 13:00:00.999Z])
true

iex> "* * * * * *" |> Cron.new!() |> Cron.match?()
true

new(string)

@spec new(expression()) :: {:ok, t()} | :error | {:error, reason()}

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"}

new!(string)

@spec new!(expression()) :: t()

Same as new/1, but raises an ArgumentError exception in case of an invalid expression.

next(cron, datetime \\ NaiveDateTime.utc_now())

@spec next(t(), DateTime.t() | NaiveDateTime.t()) :: DateTime.t() | NaiveDateTime.t()

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]

next_while(cron, datetime \\ NaiveDateTime.utc_now(), fun)

@spec next_while(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime.t() -> as_boolean(term()))
) :: {:ok, DateTime.t() | NaiveDateTime.t()} | :error

Returns an :ok tuple with the next execution datetime for which fun returns a truthy value.

If no datetime can be found, an :error will be returned.

The function truncates the precision of the given datetime to seconds.

Examples

iex> {:ok, cron} = Cron.new("0 0 29 2 *")
iex> Cron.next_while(cron, fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, ~N[2044-02-29 00:00:00]}
iex> Cron.next_while(
...>   cron, ~U[2044-02-29 00:00:00Z], fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, ~U[2072-02-29 00:00:00Z]}

iex> {:ok, cron} = Cron.new("0 0 1 1 *")
iex> Cron.next_while(cron, fn _ -> false end)
:error

next_while!(cron, datetime \\ NaiveDateTime.utc_now(), fun)

@spec next_while!(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime.t() -> as_boolean(term()))
) :: DateTime.t() | NaiveDateTime.t()

Same as next_while/3, but raises a RuntimeError exception in case no execution datetime can be found.

previous(cron, datetime \\ NaiveDateTime.utc_now())

@spec previous(t(), DateTime.t() | NaiveDateTime.t()) ::
  DateTime.t() | NaiveDateTime.t()

Returns the previous execution datetime.

If the given datetime matches cron, then also the previous datetime is returning. That means the resulting datetime is always lower 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.previous(cron, ~U[2022-01-01 12:00:00Z])
~U[2022-01-01 00:00:00Z]
iex> Cron.previous(cron, ~U[2022-01-01 00:00:00Z])
~U[2021-12-31 00:00:00Z]
iex> Cron.previous(cron, ~U[2022-01-01 00:00:00.999Z])
~U[2021-12-31 00:00:00Z]

previous_while(cron, datetime \\ NaiveDateTime.utc_now(), fun)

@spec previous_while(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime.t() -> as_boolean(term()))
) :: {:ok, DateTime.t() | NaiveDateTime.t()} | :error

Returns an :ok tuple with the previous execution datetime for which fun returns a truthy value.

If no datetime can be found, an :error will be returned.

The function truncates the precision of the given datetime to seconds.

Examples

iex> {:ok, cron} = Cron.new("0 0 29 2 *")
iex> Cron.previous_while(cron, fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, ~N[2016-02-29 00:00:00]}
iex> Cron.previous_while(
...>   cron, ~U[2016-02-29 00:00:00Z], fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, ~U[1988-02-29 00:00:00Z]}

iex> {:ok, cron} = Cron.new("0 0 1 1 *")
iex> Cron.previous_while(cron, fn _ -> false end)
:error

previous_while!(cron, datetime \\ NaiveDateTime.utc_now(), fun)

@spec previous_while!(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime.t() -> as_boolean(term()))
) :: DateTime.t() | NaiveDateTime.t()

Same as previous_while/3, but raises a RuntimeError exception in case no execution datetime can be found.

since(cron, datetime \\ DateTime.utc_now())

@spec since(t(), DateTime.t() | NaiveDateTime.t()) :: millisecond()

Same as previous/3, but returns the milliseconds since last execution datetime.

Examples

iex> {:ok, cron} = Cron.new("0 0 0 * * *")
iex> Cron.since(cron, ~U[2022-01-02 00:00:00Z])
86_400_000
iex> Cron.since(cron, ~U[2022-01-02 00:01:00Z])
60_000
iex> Cron.since(cron, ~U[2022-01-02 00:01:00.999Z])
60_999
iex> Cron.since(cron, ~U[2022-01-02 00:00:00.999Z])
86_400_999

since_while(cron, datetime \\ DateTime.utc_now(), fun)

@spec since_while(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime -> as_boolean(term()))
) :: {:ok, millisecond()} | :error

Same as previous_while/3, but returns the milliseconds since last execution datetime.

Examples

iex> {:ok, cron} = Cron.new("0 0 29 2 *")
iex> Cron.since_while(
...>   cron,
...>   ~U[2022-01-01 00:00:00Z],
...>   fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, 184_291_200_000}
iex> Cron.since_while(
...>   cron,
...>   ~U[2016-02-29 00:00:01.999Z],
...>   fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, 1_999}

iex> {:ok, cron} = Cron.new("0 0 1 1 *")
iex> Cron.since_while(cron, fn _ -> false end)
:error

since_while!(cron, datetime \\ DateTime.utc_now(), fun)

@spec since_while!(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime -> as_boolean(term()))
) :: millisecond()

Same as since_while!/3, but raises a RuntimeError exception in case no execution datetime can be found.

stream(cron, opts \\ [])

@spec stream(
  t(),
  keyword()
) :: Enumerable.t()

Returns a Stream for the given cron.

The stream ends after the last execution datetime in the year 9000 or -9000.

Options:

  • :from - the start datetime. Defaults to NaiveDateTime.utc_now("Etc/UTC").

  • :oder - :asc or :desc to get execution datetimes before or after start datetime. Defaults to :asc.

Examples

iex> cron = Cron.new!("0 0 12 1 * *")
iex> stream = Cron.stream(cron, from: ~U[2022-06-05 00:00:00Z])
iex> Enum.take(stream, 3)
[
  ~N[2022-07-01 12:00:00],
  ~N[2022-08-01 12:00:00],
  ~N[2022-09-01 12:00:00],
]
iex> stream = Cron.stream(cron, from: ~U[2022-06-05 00:00:00Z], order: :desc)
iex> Enum.take(stream, 3)
[
  ~N[2022-06-01 12:00:00],
  ~N[2022-05-01 12:00:00],
  ~N[2022-04-01 12:00:00],
]
iex> stream = Cron.stream(cron, from: ~U[9000-10-05 00:00:00Z])
iex> Enum.take(stream, 3)
[
  ~N[9000-11-01 12:00:00],
  ~N[9000-12-01 12:00:00]
]

until(cron, datetime \\ DateTime.utc_now())

@spec until(t(), DateTime.t() | NaiveDateTime.t()) :: millisecond()

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

until_while(cron, datetime \\ DateTime.utc_now(), fun)

@spec until_while(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime -> as_boolean(term()))
) :: {:ok, millisecond()} | :error

Same as next_while/3, but returns the milliseconds until next execution datetime.

Examples

iex> {:ok, cron} = Cron.new("0 0 29 2 *")
iex> Cron.until_while(
...>   cron,
...>   ~U[2022-01-01 00:00:00Z],
...>   fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, 699_321_600_000}
iex> Cron.until_while(
...>   cron,
...>   ~U[2044-02-28 23:59:59.100Z],
...>   fn datetime -> Date.day_of_week(datetime) == 1 end)
{:ok, 900}

iex> {:ok, cron} = Cron.new("0 0 1 1 *")
iex> Cron.until_while(cron, fn _ -> false end)
:error

until_while!(cron, datetime \\ DateTime.utc_now(), fun)

@spec until_while!(
  t(),
  DateTime.t() | NaiveDateTime.t(),
  (NaiveDateTime -> as_boolean(term()))
) :: millisecond()

Same as until_while!/3, but raises a RuntimeError exception in case no execution datetime can be found.