Packages
oban
2.7.2
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()
}
defstruct [:minutes, :hours, :days, :months, :weekdays]
@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"
}
@doc """
Evaluate whether a cron struct overlaps with the current date time.
"""
@spec now?(cron :: t(), datetime :: DateTime.t()) :: boolean()
def now?(%__MODULE__{} = cron, datetime \\ DateTime.utc_now()) 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 day_of_week(datetime) do
datetime
|> Date.day_of_week()
|> Integer.mod(7)
end
@doc """
Parses a crontab expression into a %Cron{} struct.
The parser can handle common expressions that use minutes, hours, days, months and weekdays,
along with ranges and steps. It also supports common extensions, also called nicknames.
Raises an `ArgumentError` if the expression cannot be parsed.
## Nicknames
- @yearly: Run once a year, "0 0 1 1 *".
- @annually: same as @yearly
- @monthly: Run once a month, "0 0 1 * *".
- @weekly: Run once a week, "0 0 * * 0".
- @daily: Run once a day, "0 0 * * *".
- @midnight: same as @daily
- @hourly: Run once an hour, "0 * * * *".
- @reboot: Run once at boot
## Examples
iex> parse!("@hourly")
%Cron{}
iex> parse!("0 * * * *")
%Cron{}
iex> parse!("60 * * * *")
** (ArgumentError)
"""
@spec parse!(input :: binary()) :: 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
now = DateTime.utc_now()
[now.minute, now.hour, now.day, now.month, day_of_week(now)]
|> Enum.join(" ")
|> parse!()
end
def parse!(input) when is_binary(input) do
[mip, hrp, dap, mop, wdp] =
input
|> String.trim()
|> String.split(~r/\s+/, parts: 5)
%__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)
}
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
raise ArgumentError, "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)
part =~ ~r/^\d+\-\d+$/ -> parse_range(part)
true -> raise ArgumentError, "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.filter(range, &(rem(&1, step) == 0))
end
defp parse_range(part) do
[rmin, rmax] = String.split(part, "-", parts: 2)
String.to_integer(rmin)..String.to_integer(rmax)
end
defp parse_range_step(part) do
[range, step] = String.split(part, "/")
parse_step(step, parse_range(range))
end
end