Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date helper datetime support #129

Merged
merged 15 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Config

config :elixir, :time_zone_database, Tz.TimeZoneDatabase
8 changes: 6 additions & 2 deletions lib/crontab/date_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ defmodule Crontab.DateChecker do
true

"""
@spec matches_date?(cron_expression :: CronExpression.t(), date :: date) :: boolean | no_return
@spec matches_date?(cron_expression :: CronExpression.t(), date :: date) ::
boolean | no_return
def matches_date?(cron_expression_or_condition_list, date)

def matches_date?(%CronExpression{reboot: true}, _),
Expand All @@ -39,7 +40,10 @@ defmodule Crontab.DateChecker do
|> matches_date?(execution_date)
end

@spec matches_date?(condition_list :: CronExpression.condition_list(), date :: date) :: boolean
@spec matches_date?(
condition_list :: CronExpression.condition_list(),
date :: date
) :: boolean
def matches_date?([], _), do: true

def matches_date?([{interval, conditions} | tail], execution_date) do
Expand Down
258 changes: 176 additions & 82 deletions lib/crontab/date_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ defmodule Crontab.DateHelper do

@type unit :: :year | :month | :day | :hour | :minute | :second | :microsecond

@type date :: NaiveDateTime.t() | DateTime.t()

@units [
{:year, {nil, nil}},
{:month, {1, 12}},
{:day, {1, :end_onf_month}},
{:day, {1, :end_of_month}},
{:hour, {0, 23}},
{:minute, {0, 59}},
{:second, {0, 59}},
Expand All @@ -21,8 +23,11 @@ defmodule Crontab.DateHelper do
iex> Crontab.DateHelper.beginning_of(~N[2016-03-14 01:45:45.123], :year)
~N[2016-01-01 00:00:00]

iex> Crontab.DateHelper.beginning_of(~U[2016-03-14 01:45:45.123Z], :year)
~U[2016-01-01 00:00:00Z]

"""
@spec beginning_of(NaiveDateTime.t(), unit) :: NaiveDateTime.t()
@spec beginning_of(date, unit :: unit) :: date when date: date
def beginning_of(date, unit) do
_beginning_of(date, proceeding_units(unit))
end
Expand All @@ -35,120 +40,194 @@ defmodule Crontab.DateHelper do
iex> Crontab.DateHelper.end_of(~N[2016-03-14 01:45:45.123], :year)
~N[2016-12-31 23:59:59.999999]

iex> Crontab.DateHelper.end_of(~U[2016-03-14 01:45:45.123Z], :year)
~U[2016-12-31 23:59:59.999999Z]

"""
@spec end_of(NaiveDateTime.t(), unit) :: NaiveDateTime.t()
@spec end_of(date, unit :: unit) :: date when date: date
def end_of(date, unit) do
_end_of(date, proceeding_units(unit))
end

@doc """
Find the last occurrence of weekday in month.
Find last occurrence of weekday in month

### Examples:

iex> Crontab.DateHelper.last_weekday(~N[2016-03-14 01:45:45.123], 6)
26

iex> Crontab.DateHelper.last_weekday(~U[2016-03-14 01:45:45.123Z], 6)
26

"""
@spec last_weekday(NaiveDateTime.t(), Calendar.day_of_week()) :: Calendar.day()
@spec last_weekday(date :: date, day_of_week :: Calendar.day_of_week()) :: Calendar.day()
def last_weekday(date, weekday) do
date
|> end_of(:month)
|> last_weekday(weekday, :end)
end

@doc """
Find the nth weekday of month.
Find nth weekday of month

### Examples:

iex> Crontab.DateHelper.nth_weekday(~N[2016-03-14 01:45:45.123], 6, 2)
12

iex> Crontab.DateHelper.nth_weekday(~U[2016-03-14 01:45:45.123Z], 6, 2)
12

"""
@spec nth_weekday(NaiveDateTime.t(), Calendar.day_of_week(), integer) :: Calendar.day()
@spec nth_weekday(date :: date, weekday :: Calendar.day_of_week(), n :: pos_integer) ::
Calendar.day()
def nth_weekday(date, weekday, n) do
date
|> beginning_of(:month)
|> nth_weekday(weekday, n, :start)
end

@doc """
Find the last occurrence of weekday in month.
Find last occurrence of weekday in month

### Examples:

iex> Crontab.DateHelper.last_weekday_of_month(~N[2016-03-14 01:45:45.123])
31

iex> Crontab.DateHelper.last_weekday_of_month(~U[2016-03-14 01:45:45.123Z])
31

"""
@spec last_weekday_of_month(NaiveDateTime.t()) :: Calendar.day()
def last_weekday_of_month(date) do
last_weekday_of_month(end_of(date, :month), :end)
end
@spec last_weekday_of_month(date :: date()) :: Calendar.day()
def last_weekday_of_month(date), do: last_weekday_of_month(end_of(date, :month), :end)

@doc """
Find the next occurrence of weekday relative to date.
Find next occurrence of weekday relative to date

### Examples:

iex> Crontab.DateHelper.next_weekday_to(~N[2016-03-14 01:45:45.123])
14

iex> Crontab.DateHelper.next_weekday_to(~U[2016-03-14 01:45:45.123Z])
14

"""
@spec next_weekday_to(NaiveDateTime.t()) :: Calendar.day()
def next_weekday_to(date = %NaiveDateTime{year: year, month: month, day: day}) do
weekday = :calendar.day_of_the_week(year, month, day)
next_day = NaiveDateTime.add(date, 1, :day)
previous_day = NaiveDateTime.add(date, -1, :day)
@spec next_weekday_to(date :: date) :: Calendar.day()
def next_weekday_to(date) do
weekday = Date.day_of_week(date)
next_day = add(date, 1, :day)
previous_day = add(date, -1, :day)

cond do
weekday == 7 && next_day.month == date.month -> next_day.day
weekday == 7 -> NaiveDateTime.add(date, -2, :day).day
weekday == 7 -> add(date, -2, :day).day
weekday == 6 && previous_day.month == date.month -> previous_day.day
weekday == 6 -> NaiveDateTime.add(date, 2, :day).day
weekday == 6 -> add(date, 2, :day).day
true -> date.day
end
end

@spec inc_year(NaiveDateTime.t()) :: NaiveDateTime.t()
def inc_year(date) do
leap_year? =
date
|> NaiveDateTime.to_date()
|> Date.leap_year?()
@doc """
Increment Year

if leap_year? do
NaiveDateTime.add(date, 366, :day)
else
NaiveDateTime.add(date, 365, :day)
end
### Examples:

iex> Crontab.DateHelper.inc_year(~N[2016-03-14 01:45:45.123])
~N[2017-03-14 01:45:45.123]

iex> Crontab.DateHelper.inc_year(~U[2016-03-14 01:45:45.123Z])
~U[2017-03-14 01:45:45.123Z]

"""
@spec inc_year(date) :: date when date: date
def inc_year(date = %{month: 2, day: 29}), do: add(date, 365, :day)

def inc_year(date = %{month: month}) do
candidate = add(date, 365, :day)
date_leap_year_before_mar? = Date.leap_year?(date) and month < 3
candidate_leap_year_after_feb? = Date.leap_year?(candidate) and month > 2
adjustment = if candidate_leap_year_after_feb? or date_leap_year_before_mar?, do: 1, else: 0
add(candidate, adjustment, :day)
end

@spec dec_year(NaiveDateTime.t()) :: NaiveDateTime.t()
def dec_year(date) do
leap_year? =
date
|> NaiveDateTime.to_date()
|> Date.leap_year?()
@doc """
Decrement Year

if leap_year? do
NaiveDateTime.add(date, -366, :day)
else
NaiveDateTime.add(date, -365, :day)
end
### Examples:

iex> Crontab.DateHelper.dec_year(~N[2016-03-14 01:45:45.123])
~N[2015-03-14 01:45:45.123]

iex> Crontab.DateHelper.dec_year(~U[2016-03-14 01:45:45.123Z])
~U[2015-03-14 01:45:45.123Z]

"""
@spec dec_year(date) :: date when date: date
def dec_year(date = %{month: 2, day: 29}), do: add(date, -366, :day)

def dec_year(date = %{month: month}) do
candidate = add(date, -365, :day)
date_leap_year_after_mar? = Date.leap_year?(date) and month > 2
candidate_leap_year_before_feb? = Date.leap_year?(candidate) and month < 3
adjustment = if date_leap_year_after_mar? or candidate_leap_year_before_feb?, do: -1, else: 0
add(candidate, adjustment, :day)
end

@spec inc_month(NaiveDateTime.t()) :: NaiveDateTime.t()
def inc_month(date = %NaiveDateTime{day: day}) do
@doc """
Increment Month

### Examples:

iex> Crontab.DateHelper.inc_month(~N[2016-03-14 01:45:45.123])
~N[2016-04-01 01:45:45.123]

iex> Crontab.DateHelper.inc_month(~U[2016-03-14 01:45:45.123Z])
~U[2016-04-01 01:45:45.123Z]

"""
@spec inc_month(date) :: date when date: date
def inc_month(date = %{year: year, month: month, day: day}) do
days =
date
|> NaiveDateTime.to_date()
Date.new!(year, month, day)
maennchen marked this conversation as resolved.
Show resolved Hide resolved
|> Date.days_in_month()

NaiveDateTime.add(date, days + 1 - day, :day)
add(date, days + 1 - day, :day)
end

@spec dec_month(NaiveDateTime.t()) :: NaiveDateTime.t()
def dec_month(date) do
days =
date
|> NaiveDateTime.to_date()
|> Date.days_in_month()
@doc """
Decrement Month

### Examples:

iex> Crontab.DateHelper.dec_month(~N[2016-03-14 01:45:45.123])
~N[2016-02-14 01:45:45.123]

NaiveDateTime.add(date, -days, :day)
iex> Crontab.DateHelper.dec_month(~U[2016-03-14 01:45:45.123Z])
~U[2016-02-14 01:45:45.123Z]

iex> Crontab.DateHelper.dec_month(~N[2011-05-31 23:59:59])
~N[2011-04-30 23:59:59]

"""
@spec dec_month(date) :: date when date: date
def dec_month(date = %{year: year, month: month, day: day}) do
days_in_last_month = Date.new!(year, month, 1) |> Date.add(-1) |> Date.days_in_month()
add(date, -(day + max(days_in_last_month - day, 0)), :day)
end

@spec _beginning_of(NaiveDateTime.t(), [{unit, {any, any}}]) :: NaiveDateTime.t()
@spec _beginning_of(date, [{unit, {any, any}}]) :: date when date: date
defp _beginning_of(date, [{unit, {lower, _}} | tail]) do
_beginning_of(Map.put(date, unit, lower), tail)
end

defp _beginning_of(date, []), do: date

@spec _end_of(NaiveDateTime.t(), [{unit, {any, any}}]) :: NaiveDateTime.t()
defp _end_of(date, [{unit, {_, :end_onf_month}} | tail]) do
upper =
date
|> NaiveDateTime.to_date()
|> Date.days_in_month()

@spec _end_of(date, [{unit, {any, any}}]) :: date when date: date
defp _end_of(date, [{unit, {_, :end_of_month}} | tail]) do
upper = Date.days_in_month(date)
_end_of(Map.put(date, unit, upper), tail)
end

Expand All @@ -165,7 +244,7 @@ defmodule Crontab.DateHelper do
|> Enum.reduce([], fn {key, value}, acc ->
cond do
Enum.count(acc) > 0 ->
Enum.concat(acc, [{key, value}])
[{key, value} | acc]

key == unit ->
[{key, value}]
Expand All @@ -174,39 +253,54 @@ defmodule Crontab.DateHelper do
[]
end
end)
|> Enum.reverse()

units
end

@spec nth_weekday(NaiveDateTime.t(), Calendar.day_of_week(), :start) :: boolean
defp nth_weekday(date = %NaiveDateTime{}, _, 0, :start),
do: NaiveDateTime.add(date, -1, :day).day
@spec nth_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :start) ::
boolean
defp nth_weekday(date, _, 0, :start), do: add(date, -1, :day).day

defp nth_weekday(date = %NaiveDateTime{year: year, month: month, day: day}, weekday, n, :start) do
if :calendar.day_of_the_week(year, month, day) == weekday do
nth_weekday(NaiveDateTime.add(date, 1, :day), weekday, n - 1, :start)
else
nth_weekday(NaiveDateTime.add(date, 1, :day), weekday, n, :start)
end
defp nth_weekday(date, weekday, n, :start) do
modifier = if Date.day_of_week(date) == weekday, do: n - 1, else: n
nth_weekday(add(date, 1, :day), weekday, modifier, :start)
end

@spec last_weekday_of_month(NaiveDateTime.t(), :end) :: Calendar.day()
defp last_weekday_of_month(date = %NaiveDateTime{year: year, month: month, day: day}, :end) do
weekday = :calendar.day_of_the_week(year, month, day)

if weekday > 5 do
last_weekday_of_month(NaiveDateTime.add(date, -1, :day), :end)
@spec last_weekday_of_month(date :: date(), position :: :end) :: Calendar.day()
defp last_weekday_of_month(date = %{day: day}, :end) do
if Date.day_of_week(date) > 5 do
last_weekday_of_month(add(date, -1, :day), :end)
else
day
end
end

@spec last_weekday(NaiveDateTime.t(), non_neg_integer, :end) :: Calendar.day()
defp last_weekday(date = %NaiveDateTime{year: year, month: month, day: day}, weekday, :end) do
if :calendar.day_of_the_week(year, month, day) == weekday do
@spec last_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :end) ::
Calendar.day()
defp last_weekday(date = %{day: day}, weekday, :end) do
if Date.day_of_week(date) == weekday do
day
else
last_weekday(NaiveDateTime.add(date, -1, :day), weekday, :end)
last_weekday(add(date, -1, :day), weekday, :end)
end
end

@doc false
def add(datetime = %NaiveDateTime{}, amt, unit), do: NaiveDateTime.add(datetime, amt, unit)

def add(datetime = %DateTime{}, amt, unit) do
candidate = DateTime.add(datetime, amt, unit)
adjustment = datetime.std_offset - candidate.std_offset
adjusted = DateTime.add(candidate, adjustment, :second)

if adjusted.std_offset != candidate.std_offset do
candidate
else
case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do
{:ambiguous, _, target} -> target
{:ok, target} -> target
end
end
end
end