Skip to content

Commit

Permalink
Date Lib Independence
Browse files Browse the repository at this point in the history
Signed-off-by: Jonatan Männchen <jonatan@maennchen.ch>
  • Loading branch information
maennchen committed Mar 29, 2017
1 parent f969de0 commit 2af98e7
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 43 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# CHANGELOG

## v1
## v1.1.0

* Date Library independent

## v1.0.0

* Removed Helper Functions in Module `Crontab`
* Moved `get_[next|previous]_run_dates` to `Crontab.Scheduler`
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ Parse Cron Format Strings, Write Cron Format Strings and Caluclate Execution Dat

```elixir
def deps do
[{:crontab, "~> 1.0.0"}]
[{:crontab, "~> 1.0.0"},
{:timex, "~> 3.0"}]
end
```

2. Ensure `crontab` is started before your application:

```elixir
def application do
[applications: [:crontab]]
[applications: [:crontab, :timex]]
end
```

Expand Down
32 changes: 17 additions & 15 deletions lib/crontab/date_checker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule Crontab.DateChecker do

alias Crontab.CronExpression

@date_library Application.get_env(:crontab, :date_library, Crontab.DateLibrary.Timex)

@doc """
Check a condition list against a given date.
Expand Down Expand Up @@ -83,7 +85,7 @@ defmodule Crontab.DateChecker do
end
end
defp matches_specific_date?(:day, [head_value | tail_values], :L, execution_date) do
if Timex.end_of_month(execution_date).day == head_value do
if @date_library.end_of_month(execution_date).day == head_value do
true
else
matches_specific_date?(:day, tail_values, :L, execution_date)
Expand All @@ -99,9 +101,9 @@ defmodule Crontab.DateChecker do
last_weekday_of_month(execution_date) === execution_date.day
end
defp matches_specific_date?(:day, _, {:W, day}, execution_date) do
last_day = Timex.end_of_month(execution_date).day
last_day = @date_library.end_of_month(execution_date).day
specific_day = case last_day < day do
true -> Timex.end_of_month(execution_date)
true -> @date_library.end_of_month(execution_date)
false -> Map.put(execution_date, :day, day)
end
next_weekday_to(specific_day) === execution_date.day
Expand All @@ -124,42 +126,42 @@ defmodule Crontab.DateChecker do
@spec last_weekday(NaiveDateTime.t, CronExpression.weekday) :: CronExpression.day
defp last_weekday(date, weekday) do
date
|> Timex.end_of_month
|> @date_library.end_of_month
|> last_weekday(weekday, :end)
end
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
day
else
last_weekday(Timex.shift(date, days: -1), weekday, :end)
last_weekday(@date_library.shift(date, -1, :days), weekday, :end)
end
end

@spec nth_weekday(NaiveDateTime.t, CronExpression.weekday, integer) :: CronExpression.day
defp nth_weekday(date, weekday, n) do
date
|> Timex.beginning_of_month
|> @date_library.beginning_of_month
|> nth_weekday(weekday, n, :start)
end
@spec nth_weekday(NaiveDateTime.t, CronExpression.weekday, :start) :: boolean
defp nth_weekday(date = %NaiveDateTime{}, _, 0, :start), do: Timex.shift(date, days: -1).day
defp nth_weekday(date = %NaiveDateTime{}, _, 0, :start), do: @date_library.shift(date, -1, :days).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(Timex.shift(date, days: 1), weekday, n - 1, :start)
nth_weekday(@date_library.shift(date, 1, :days), weekday, n - 1, :start)
else
nth_weekday(Timex.shift(date, days: 1), weekday, n, :start)
nth_weekday(@date_library.shift(date, 1, :days), weekday, n, :start)
end
end

@spec last_weekday_of_month(NaiveDateTime.t) :: CronExpression.day
defp last_weekday_of_month(date) do
last_weekday_of_month(Timex.end_of_month(date), :end)
last_weekday_of_month(@date_library.end_of_month(date), :end)
end
@spec last_weekday_of_month(NaiveDateTime.t, :end) :: CronExpression.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(Timex.shift(date, days: -1), :end)
last_weekday_of_month(@date_library.shift(date, -1, :days), :end)
else
day
end
Expand All @@ -168,14 +170,14 @@ defmodule Crontab.DateChecker do
@spec next_weekday_to(NaiveDateTime.t) :: CronExpression.day
defp next_weekday_to(date = %NaiveDateTime{year: year, month: month, day: day}) do
weekday = :calendar.day_of_the_week(year, month, day)
next_day = Timex.shift(date, days: 1)
previous_day = Timex.shift(date, days: -1)
next_day = @date_library.shift(date, 1, :days)
previous_day = @date_library.shift(date, -1, :days)

cond do
weekday == 7 && next_day.month == date.month -> next_day.day
weekday == 7 -> Timex.shift(date, days: -2).day
weekday == 7 -> @date_library.shift(date, -2, :days).day
weekday == 6 && previous_day.month == date.month -> previous_day.day
weekday == 6 -> Timex.shift(date, days: 2).day
weekday == 6 -> @date_library.shift(date, 2, :days).day
true -> date.day
end
end
Expand Down
28 changes: 28 additions & 0 deletions lib/crontab/date_library.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Crontab.DateLibrary do
@moduledoc """
This Behaviour offers Date Library Independant integration of helper
functions.
**This behavious is considered internal. Breaking Changes can occur on every
release.**
Make sure your implementation passes `Crontab.DateLibraryTest`. Otherwise
unexpected behaviour can occur.
"""

@type time_unit :: :days | :hours | :minutes | :seconds | :years | :months

@callback shift(NaiveDateTime.t, integer, time_unit) :: NaiveDateTime.t

@callback beginning_of_year(NaiveDateTime.t) :: NaiveDateTime.t

@callback end_of_year(NaiveDateTime.t) :: NaiveDateTime.t

@callback beginning_of_month(NaiveDateTime.t) :: NaiveDateTime.t

@callback end_of_month(NaiveDateTime.t) :: NaiveDateTime.t

@callback beginning_of_day(NaiveDateTime.t) :: NaiveDateTime.t

@callback end_of_day(NaiveDateTime.t) :: NaiveDateTime.t
end
23 changes: 23 additions & 0 deletions lib/crontab/date_library/timex.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
if Code.ensure_compiled?(Timex) do
defmodule Crontab.DateLibrary.Timex do
@moduledoc false

@behaviour Crontab.DateLibrary

def shift(date, amount, unit) do
Timex.shift(date, [{unit, amount}])
end

defdelegate beginning_of_year(date), to: Timex

defdelegate end_of_year(date), to: Timex

defdelegate beginning_of_month(date), to: Timex

defdelegate end_of_month(date), to: Timex

defdelegate beginning_of_day(date), to: Timex

defdelegate end_of_day(date), to: Timex
end
end
42 changes: 22 additions & 20 deletions lib/crontab/scheduler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Crontab.Scheduler do
import Crontab.DateChecker
alias Crontab.CronExpression

@date_library Application.get_env(:crontab, :date_library, Crontab.DateLibrary.Timex)

@type direction :: :increment | :decrement
@type result :: {:error, any} | {:ok, NaiveDateTime.t}

Expand Down Expand Up @@ -100,10 +102,10 @@ defmodule Crontab.Scheduler do
@spec get_next_run_dates(CronExpression.t, NaiveDateTime.t) :: Enumerable.t
def get_next_run_dates(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now))
def get_next_run_dates(cron_expression = %CronExpression{extended: false}, date) do
_get_next_run_dates(cron_expression, date, fn date -> Timex.shift(date, minutes: 1) end)
_get_next_run_dates(cron_expression, date, fn date -> @date_library.shift(date, 1, :minutes) end)
end
def get_next_run_dates(cron_expression = %CronExpression{extended: true}, date) do
_get_next_run_dates(cron_expression, date, fn date -> Timex.shift(date, seconds: 1) end)
_get_next_run_dates(cron_expression, date, fn date -> @date_library.shift(date, 1, :seconds) end)
end

@spec _get_next_run_dates(CronExpression.t, NaiveDateTime.t, function) :: Enumerable.t
Expand Down Expand Up @@ -208,10 +210,10 @@ defmodule Crontab.Scheduler do
@spec get_previous_run_dates(CronExpression.t, NaiveDateTime.t) :: Enumerable.t
def get_previous_run_dates(cron_expression, date \\ DateTime.to_naive(DateTime.utc_now))
def get_previous_run_dates(cron_expression = %CronExpression{extended: false}, date) do
_get_previous_run_dates(cron_expression, date, fn date -> Timex.shift(date, minutes: -1) end)
_get_previous_run_dates(cron_expression, date, fn date -> @date_library.shift(date, -1, :minutes) end)
end
def get_previous_run_dates(cron_expression = %CronExpression{extended: true}, date) do
_get_previous_run_dates(cron_expression, date, fn date -> Timex.shift(date, seconds: -1) end)
_get_previous_run_dates(cron_expression, date, fn date -> @date_library.shift(date, -1, :seconds) end)
end

@spec _get_previous_run_dates(CronExpression.t, NaiveDateTime.t, function) :: Enumerable.t
Expand Down Expand Up @@ -266,23 +268,23 @@ defmodule Crontab.Scheduler do

@spec correct_date(CronExpression.interval, NaiveDateTime.t, direction) :: NaiveDateTime.t | {:error, any}

defp correct_date(:second, date, :increment), do: date |> Timex.shift(seconds: 1)
defp correct_date(:minute, date, :increment), do: date |> Timex.shift(minutes: 1) |> reset(:seconds)
defp correct_date(:hour, date, :increment), do: date |> Timex.shift(hours: 1) |> reset(:minutes)
defp correct_date(:day, date, :increment), do: date |> Timex.shift(days: 1) |> Timex.beginning_of_day
defp correct_date(:month, date, :increment), do: date |> Timex.shift(months: 1) |> Timex.beginning_of_month
defp correct_date(:weekday, date, :increment), do: date |> Timex.shift(days: 1) |> Timex.beginning_of_day
defp correct_date(:second, date, :increment), do: date |> @date_library.shift(1, :seconds)
defp correct_date(:minute, date, :increment), do: date |> @date_library.shift(1, :minutes) |> reset(:seconds)
defp correct_date(:hour, date, :increment), do: date |> @date_library.shift(1, :hours) |> reset(:minutes)
defp correct_date(:day, date, :increment), do: date |> @date_library.shift(1, :days) |> @date_library.beginning_of_day
defp correct_date(:month, date, :increment), do: date |> @date_library.shift(1, :months) |> @date_library.beginning_of_month
defp correct_date(:weekday, date, :increment), do: date |> @date_library.shift(1, :days) |> @date_library.beginning_of_day
defp correct_date(:year, %NaiveDateTime{year: 9_999}, :increment), do: {:error, :upper_bound}
defp correct_date(:year, date, :increment), do: date |> Timex.shift(years: 1) |> Timex.beginning_of_year

defp correct_date(:second, date, :decrement), do: date |> Timex.shift(seconds: -1) |> reset(:microseconds)
defp correct_date(:minute, date, :decrement), do: date |> Timex.shift(minutes: -1) |> upper(:seconds) |> reset(:microseconds)
defp correct_date(:hour, date, :decrement), do: date |> Timex.shift(hours: -1) |> upper(:minutes) |> reset(:microseconds)
defp correct_date(:day, date, :decrement), do: date |> Timex.shift(days: -1) |> Timex.end_of_day |> reset(:microseconds)
defp correct_date(:month, date, :decrement), do: date |> Timex.shift(months: -1) |> Timex.end_of_month |> reset(:microseconds)
defp correct_date(:weekday, date, :decrement), do: date |> Timex.shift(days: -1) |> Timex.end_of_day |> reset(:microseconds)
defp correct_date(:year, date, :increment), do: date |> @date_library.shift(1, :years) |> @date_library.beginning_of_year

defp correct_date(:second, date, :decrement), do: date |> @date_library.shift(-1, :seconds) |> reset(:microseconds)
defp correct_date(:minute, date, :decrement), do: date |> @date_library.shift(-1, :minutes) |> upper(:seconds) |> reset(:microseconds)
defp correct_date(:hour, date, :decrement), do: date |> @date_library.shift(-1, :hours) |> upper(:minutes) |> reset(:microseconds)
defp correct_date(:day, date, :decrement), do: date |> @date_library.shift(-1, :days) |> @date_library.end_of_day |> reset(:microseconds)
defp correct_date(:month, date, :decrement), do: date |> @date_library.shift(-1, :months) |> @date_library.end_of_month |> reset(:microseconds)
defp correct_date(:weekday, date, :decrement), do: date |> @date_library.shift(-1, :days) |> @date_library.end_of_day |> reset(:microseconds)
defp correct_date(:year, date = %NaiveDateTime{year: 0}, :decrement), do: date
defp correct_date(:year, date, :decrement), do: date |> Timex.shift(years: -1) |> Timex.end_of_year |> reset(:microseconds)
defp correct_date(:year, date, :decrement), do: date |> @date_library.shift(-1, :years) |> @date_library.end_of_year |> reset(:microseconds)

@spec reset(NaiveDateTime.t, :microseconds | :seconds | :minutes) :: NaiveDateTime.t
defp reset(date = %NaiveDateTime{}, :microseconds), do: Map.put(date, :microsecond, {0,0})
Expand All @@ -298,7 +300,7 @@ defmodule Crontab.Scheduler do
defp clean_date(date = %NaiveDateTime{}, :microseconds) do
date
|> Map.put(:microsecond, {0,0})
|> Timex.shift(seconds: 1)
|> @date_library.shift(1, :seconds)
end
defp clean_date(date = %NaiveDateTime{}, :seconds) do
clean_microseconds = clean_date(date, :microseconds)
Expand Down
7 changes: 4 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Crontab.Mixfile do
#
# Type "mix help compile.app" for more information
def application do
[applications: [:logger, :timex]]
[applications: [:logger]]
end

defp description do
Expand All @@ -39,7 +39,7 @@ defmodule Crontab.Mixfile do
#
# Type "mix help deps" for more examples and options
defp deps do
[{:timex, "~> 3.0"},
[{:timex, "~> 3.0", optional: true},
{:ecto, "~> 1.0 or ~> 2.0 or ~> 2.1", optional: true},
{:ex_doc, ">= 0.0.0", only: :dev},
{:inch_ex, only: :docs},
Expand All @@ -64,7 +64,8 @@ defmodule Crontab.Mixfile do
extras: [
"pages/Getting Started.md",
"CHANGELOG.md",
"pages/Basic Usage.md"
"pages/Basic Usage.md",
"pages/Date Library.md"
]]
end
end
15 changes: 15 additions & 0 deletions pages/Date Library.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Date Library

This library can be used independent from `timex`.

Any date library can be used by implementing the `Crontab.DateLibrary` behavior.

**The library does not respect semver for this behaviour. Breaking Changes will
happen even in patch releases.**

To use another date library, change the implementation like this:

```elixir
config :crontab,
date_library: Crontab.DateLibrary.Timex
```
5 changes: 3 additions & 2 deletions pages/Getting Started.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@

```elixir
def deps do
[{:crontab, "~> 0.8.5"}]
[{:crontab, "~> 0.8.5"},
{:timex, "~> 3.0"}]
end
```

2. Ensure `crontab` is started before your application:

```elixir
def application do
[applications: [:crontab]]
[applications: [:crontab, :timex]]
end
```

Expand Down
Loading

0 comments on commit 2af98e7

Please sign in to comment.