Packages
oban
2.17.4
2.23.0
2.22.1
2.22.0
2.21.1
2.21.0
2.20.3
2.20.2
2.20.1
2.20.0
2.19.4
2.19.3
2.19.2
2.19.1
2.19.0
2.18.3
2.18.2
2.18.1
2.18.0
2.17.12
2.17.11
2.17.10
2.17.9
2.17.8
2.17.7
2.17.6
2.17.5
2.17.4
2.17.3
2.17.2
2.17.1
2.17.0
2.16.3
2.16.2
2.16.1
2.16.0
2.15.4
2.15.3
2.15.2
2.15.1
2.15.0
2.14.2
2.14.1
2.14.0
2.13.6
2.13.5
2.13.4
2.13.3
2.13.2
2.13.1
2.13.0
2.12.1
2.12.0
2.11.3
2.11.2
2.11.1
2.11.0
2.10.1
2.10.0
retired
2.9.2
2.9.1
2.9.0
2.8.0
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.0
2.4.3
2.4.2
2.4.1
2.4.0
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.0
2.1.0
2.0.0
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.2.0
1.1.0
1.0.0
1.0.0-rc.2
1.0.0-rc.1
0.12.1
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Robust job processing, backed by modern PostgreSQL, SQLite3, and MySQL.
Current section
Files
Jump to
Current section
Files
lib/oban/cron/expression.ex
defmodule Oban.Cron.Expression do
@moduledoc false
@type t :: %__MODULE__{
minutes: MapSet.t(),
hours: MapSet.t(),
days: MapSet.t(),
months: MapSet.t(),
weekdays: MapSet.t(),
reboot?: boolean()
}
defstruct [:minutes, :hours, :days, :months, :weekdays, reboot?: false]
@dow_map %{
"SUN" => "0",
"MON" => "1",
"TUE" => "2",
"WED" => "3",
"THU" => "4",
"FRI" => "5",
"SAT" => "6"
}
@mon_map %{
"JAN" => "1",
"FEB" => "2",
"MAR" => "3",
"APR" => "4",
"MAY" => "5",
"JUN" => "6",
"JUL" => "7",
"AUG" => "8",
"SEP" => "9",
"OCT" => "10",
"NOV" => "11",
"DEC" => "12"
}
@spec now?(cron :: t(), datetime :: DateTime.t()) :: boolean()
def now?(cron, datetime \\ DateTime.utc_now())
def now?(%__MODULE__{reboot?: true}, _datetime), do: true
def now?(%__MODULE__{} = cron, datetime) do
cron
|> Map.from_struct()
|> Enum.all?(&included?(&1, datetime))
end
defp included?({_, :*}, _datetime), do: true
defp included?({:minutes, set}, datetime), do: MapSet.member?(set, datetime.minute)
defp included?({:hours, set}, datetime), do: MapSet.member?(set, datetime.hour)
defp included?({:days, set}, datetime), do: MapSet.member?(set, datetime.day)
defp included?({:months, set}, datetime), do: MapSet.member?(set, datetime.month)
defp included?({:weekdays, set}, datetime), do: MapSet.member?(set, day_of_week(datetime))
defp included?({:reboot?, _}, _datetime), do: true
defp day_of_week(datetime) do
datetime
|> Date.day_of_week()
|> Integer.mod(7)
end
@spec parse(input :: binary()) :: {:ok, t()} | {:error, Exception.t()}
def parse("@annually"), do: parse("0 0 1 1 *")
def parse("@yearly"), do: parse("0 0 1 1 *")
def parse("@monthly"), do: parse("0 0 1 * *")
def parse("@weekly"), do: parse("0 0 * * 0")
def parse("@midnight"), do: parse("0 0 * * *")
def parse("@daily"), do: parse("0 0 * * *")
def parse("@hourly"), do: parse("0 * * * *")
def parse("@reboot"), do: {:ok, %__MODULE__{reboot?: true}}
def parse(input) when is_binary(input) do
case String.split(input, ~r/\s+/, trim: true, parts: 5) do
[mip, hrp, dap, mop, wdp] ->
{:ok,
%__MODULE__{
minutes: parse_field(mip, 0..59),
hours: parse_field(hrp, 0..23),
days: parse_field(dap, 1..31),
months: mop |> trans_field(@mon_map) |> parse_field(1..12),
weekdays: wdp |> trans_field(@dow_map) |> parse_field(0..6)
}}
_parts ->
throw({:error, "incorrect number of fields in expression: #{input}"})
end
catch
{:error, message} -> {:error, %ArgumentError{message: message}}
end
@spec parse!(input :: binary()) :: t()
def parse!(input) when is_binary(input) do
case parse(input) do
{:ok, cron} -> cron
{:error, exception} -> raise(exception)
end
end
defp parse_field(field, range) do
range_set = MapSet.new(range)
parsed =
field
|> String.split(~r/\s*,\s*/)
|> Enum.flat_map(&parse_part(&1, range))
|> MapSet.new()
unless MapSet.subset?(parsed, range_set) do
throw({:error, "expression field #{field} is out of range: #{inspect(range)}"})
end
parsed
end
defp trans_field(field, map) do
Enum.reduce(map, field, fn {val, rep}, acc -> String.replace(acc, val, rep) end)
end
defp parse_part(part, range) do
cond do
part == "*" ->
range
part =~ ~r/^\d+$/ ->
parse_literal(part)
part =~ ~r/^\*\/[1-9]\d?$/ ->
parse_step(part, range)
part =~ ~r/^\d+(\-\d+)?\/[1-9]\d?$/ ->
parse_range_step(part, range)
part =~ ~r/^\d+\-\d+$/ ->
parse_range(part, range)
true ->
throw({:error, "unrecognized cron expression: #{part}"})
end
end
defp parse_literal(part) do
part
|> String.to_integer()
|> List.wrap()
end
defp parse_step(part, range) do
step =
part
|> String.replace_leading("*/", "")
|> String.to_integer()
Enum.take_every(range, step)
end
defp parse_range_step(part, max_range) do
[range, step] = String.split(part, "/")
parse_step(step, parse_range(range, max_range))
end
defp parse_range(part, max_range) do
case String.split(part, "-") do
[rall] ->
String.to_integer(rall)..Enum.max(max_range)
[rmin, rmax] ->
rmin = String.to_integer(rmin)
rmax = String.to_integer(rmax)
if rmin <= rmax do
rmin..rmax
else
throw(
{:error,
"left side (#{rmin}) of a range must be less than or equal to the right side (#{rmax})"}
)
end
end
end
end