Current section
Files
Jump to
Current section
Files
lib/cron/calc.ex
defmodule Cron.Calc do
@moduledoc false
@second 1
@minute 60 * @second
@hour 60 * @minute
@day 24 * @hour
@spec next(NaiveDateTime.t(), Cron.t()) :: NaiveDateTime.t()
def next(%NaiveDateTime{} = datetime, %Cron{} = cron) do
with ^datetime <- update(datetime, cron, :asc) do
datetime
|> NaiveDateTime.add(@second)
|> update(cron, :asc)
end
end
@spec previous(NaiveDateTime.t(), Cron.t()) :: NaiveDateTime.t()
def previous(%NaiveDateTime{} = datetime, %Cron{} = cron) do
with ^datetime <- update(datetime, cron, :desc) do
datetime
|> NaiveDateTime.add(-@second)
|> update(cron, :desc)
end
end
@spec match?(NaiveDateTime.t(), Cron.t()) :: boolean()
def match?(%NaiveDateTime{} = datetime, %Cron{} = cron) do
datetime == update(datetime, cron, :asc)
end
defp update(datetime, cron, order) do
datetime
|> update(cron, :month, order)
|> update(cron, :day, order)
|> update(cron, :hour, order)
|> update(cron, :minute, order)
|> update(cron, :second, order)
end
defp update(datetime, %Cron{month: 1..12}, :month, _order), do: datetime
defp update(datetime, %Cron{hour: 0..23}, :hour, _order), do: datetime
defp update(datetime, %Cron{minute: 0..59}, :minute, _order), do: datetime
defp update(datetime, %Cron{second: 0..59}, :second, _order), do: datetime
defp update(datetime, cron, :day, order) do
case {cron.day == 1..31, cron.day_of_week == 0..6} do
{true, true} ->
datetime
{false, false} ->
update(datetime, cron, :union, order)
_else ->
new = value(datetime.day, cron, :day, order)
update(datetime, cron, :day, order, {datetime.day, new})
end
end
defp update(datetime, cron, :union, order) do
min_max_datetime(
update(datetime, %{cron | day: 1..31}, order),
update(datetime, %{cron | day_of_week: 0..6}, order),
order
)
end
defp update(datetime, cron, field, order) do
actual = Map.fetch!(datetime, field)
new = value(actual, cron, field, order)
update(datetime, cron, field, order, {actual, new})
end
defp update(datetime, _cron, :month, :asc, {actual, new}) when actual < new do
reset(%{datetime | month: new}, :month, :asc)
end
defp update(%{year: year} = datetime, _cron, :month, :asc, {actual, new}) when actual > new do
reset(%{datetime | year: year + 1, month: new}, :month, :asc)
end
defp update(%{year: year} = datetime, _cron, :month, :desc, {actual, new}) when actual < new do
reset(%{datetime | year: year - 1, month: new}, :month, :desc)
end
defp update(datetime, _cron, :month, :desc, {actual, new}) when actual > new do
reset(%{datetime | month: new}, :month, :desc)
end
defp update(datetime, cron, :day, :asc, {actual, actual}) do
case valid_day?(datetime, cron) do
true ->
datetime
false ->
datetime
|> next_day(cron)
|> reset(:day, :asc)
|> update(cron, :month, :asc)
|> update(cron, :day, :asc)
end
end
defp update(datetime, cron, :day, :asc, {actual, new}) when actual < new do
updated = %{datetime | day: new}
updated =
case valid_day?(updated, cron) do
true -> updated
false -> next_day(datetime, cron)
end
updated
|> reset(:day, :asc)
|> update(cron, :month, :asc)
|> update(cron, :day, :asc)
end
defp update(datetime, cron, :day, :asc, {actual, new}) when actual > new do
datetime
|> next_month()
|> reset(:month, :asc)
|> update(cron, :month, :asc)
|> update(cron, :day, :asc)
end
defp update(datetime, cron, :day, :desc, {actual, actual}) do
case valid_day?(datetime, cron) do
true ->
datetime
false ->
datetime
|> previous_day(cron)
|> reset(:day, :desc)
|> update(cron, :month, :desc)
|> update(cron, :day, :desc)
end
end
defp update(datetime, cron, :day, :desc, {actual, new}) when actual < new do
datetime
|> previous_month()
|> reset(:month, :desc)
|> update(cron, :month, :desc)
|> update(cron, :day, :desc)
end
defp update(datetime, cron, :day, :desc, {actual, new}) when actual > new do
%{datetime | day: new}
|> reset(:day, :desc)
|> update(cron, :month, :desc)
|> update(cron, :day, :desc)
end
defp update(datetime, _cron, _field, _order, {actual, actual}), do: datetime
defp update(datetime, _cron, field, :asc, {actual, new}) when actual < new do
datetime
|> Map.put(field, new)
|> reset(field, :asc)
end
defp update(datetime, cron, field, :asc, {actual, new}) when actual > new do
distance = distance(actual, new, field, :asc)
datetime
|> NaiveDateTime.add(distance)
|> reset(field, :asc)
|> update(cron, :asc)
end
defp update(datetime, _cron, field, :desc, {actual, new}) when actual > new do
datetime
|> Map.put(field, new)
|> reset(field, :desc)
end
defp update(datetime, cron, field, :desc, {actual, new}) when actual < new do
distance = distance(actual, new, field, :desc)
datetime
|> NaiveDateTime.add(distance)
|> reset(field, :desc)
|> update(cron, :desc)
end
defp previous_day(datetime, %{day_of_week: day_of_week}) do
datetime = NaiveDateTime.add(datetime, -@day)
actual = day_of_week(datetime)
value = value(actual, day_of_week, :desc)
distance = distance(actual, value, :day_of_week, :desc)
NaiveDateTime.add(datetime, distance)
end
defp next_day(datetime, %{day_of_week: 0..6}) do
NaiveDateTime.add(datetime, @day)
end
defp next_day(datetime, %{day_of_week: day_of_week}) do
datetime = NaiveDateTime.add(datetime, @day)
actual = day_of_week(datetime)
value = value(actual, day_of_week, :asc)
distance = distance(actual, value, :day_of_week, :asc)
NaiveDateTime.add(datetime, distance)
end
defp previous_month(%{year: year, month: 1} = datetime) do
%{datetime | year: year - 1, month: 12}
end
defp previous_month(%{month: month} = datetime) do
%{datetime | month: month - 1}
end
defp next_month(%{year: year, month: 12} = datetime) do
%{datetime | year: year + 1, month: 1}
end
defp next_month(%{month: month} = datetime) do
%{datetime | month: month + 1}
end
defp valid_day?(datetime, %Cron{day: 1..31} = cron) do
with true <- valid_day?(datetime) do
case Map.fetch!(cron, :day_of_week) do
day when is_integer(day) -> day_of_week(datetime) == day
days -> day_of_week(datetime) in days
end
end
end
defp valid_day?(datetime, _cron) do
valid_day?(datetime)
end
defp valid_day?(%{day: day} = datetime) do
day <= Date.days_in_month(datetime)
end
defp day_of_week(datetime) do
Date.day_of_week(datetime, :sunday) - 1
end
defp value(actual, cron, field, order) do
value(actual, Map.fetch!(cron, field), order)
end
defp value(_actual, value, _order) when is_integer(value), do: value
defp value(actual, %Range{} = range, order) do
cond do
actual in range -> actual
order == :asc -> range.first
order == :desc -> range.last
end
end
defp value(actual, [first | _] = list, :asc) do
Enum.find(list, first, fn value -> value >= actual end)
end
defp value(actual, [_ | _] = list, :desc) do
[first | _] = list = Enum.reverse(list)
Enum.find(list, first, fn value -> value <= actual end)
end
defp distance(actual, new, :second, :asc) when actual > new do
60 - actual + new
end
defp distance(actual, new, :second, :desc) when actual < new do
(60 - new + actual) * -@second
end
defp distance(actual, new, :minute, :asc) when actual > new do
(60 - actual + new) * @minute
end
defp distance(actual, new, :minute, :desc) when actual < new do
(60 - new + actual) * -@minute
end
defp distance(actual, new, :hour, :asc) when actual > new do
(24 - actual + new) * @hour
end
defp distance(actual, new, :hour, :desc) when actual < new do
(24 - new + actual) * -@hour
end
defp distance(actual, new, :day_of_week, :asc) do
cond do
actual == new -> 0
actual < new -> (new - actual) * @day
actual > new -> (6 - actual + new) * @day
end
end
defp distance(actual, new, :day_of_week, :desc) do
cond do
actual == new -> 0
actual > new -> (actual - new) * -@day
actual < new -> (6 - new + actual) * -@day
end
end
defp reset(datetime, :second, _order), do: datetime
defp reset(datetime, field, :asc) do
case field do
:month ->
%{datetime | day: 1, hour: 0, minute: 0, second: 0}
:day ->
%{datetime | hour: 0, minute: 0, second: 0}
:hour ->
%{datetime | minute: 0, second: 0}
:minute ->
%{datetime | second: 0}
end
end
defp reset(datetime, field, :desc) do
case field do
:month ->
%{datetime | day: Date.days_in_month(datetime), hour: 23, minute: 59, second: 59}
:day ->
%{datetime | hour: 23, minute: 59, second: 59}
:hour ->
%{datetime | minute: 59, second: 59}
:minute ->
%{datetime | second: 59}
end
end
defp min_max_datetime(datetime1, datetime2, :asc) do
case NaiveDateTime.compare(datetime1, datetime2) do
:gt -> datetime2
_else -> datetime1
end
end
defp min_max_datetime(datetime1, datetime2, :desc) do
case NaiveDateTime.compare(datetime1, datetime2) do
:lt -> datetime2
_else -> datetime1
end
end
end