From 01e539bc79ea0dfec84fd07ff6fc42f8f115e305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Thu, 22 Dec 2016 15:42:16 +0000 Subject: [PATCH] Support Specific Dividers --- README.md | 4 +-- lib/crontab/cron_date_checker.ex | 22 ++++++++++----- lib/crontab/cron_format_parser.ex | 24 ++++++++-------- lib/crontab/cron_format_writer.ex | 4 +-- lib/crontab/cron_scheduler.ex | 2 +- test/crontab/cron_date_checker_test.exs | 24 ++++++++-------- test/crontab/cron_format_parser_test.exs | 4 +-- test/crontab/cron_scheduler_test.exs | 8 +++--- test/crontab/functional_test.exs | 36 ++++++++++++------------ 9 files changed, 69 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 23d5f9c..f8f30de 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ iex> Crontab.CronFormatParser.parse "fooo" ```elixir iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{} "* * * * * *" -iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{minute: [9, {:-, 4, 6}, {:/, 9}]} +iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{minute: [9, {:-, 4, 6}, {:/, :*, 9}]} "9,4-6,*/9 * * * * *" ``` @@ -64,6 +64,6 @@ false iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{}, ~N[2002-01-13 23:00:07]) {:ok, ~N[2002-01-13 23:00:00]} -iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{year: [{:/, 9}]}, ~N[2002-01-13 23:00:07]) +iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) {:ok, ~N[2007-01-01 00:00:00]} ``` diff --git a/lib/crontab/cron_date_checker.ex b/lib/crontab/cron_date_checker.ex index a4a5f95..a47cbce 100644 --- a/lib/crontab/cron_date_checker.ex +++ b/lib/crontab/cron_date_checker.ex @@ -9,21 +9,21 @@ defmodule Crontab.CronDateChecker do Check a Crontab.CronInterval against a given date. ### Examples - iex> Crontab.CronDateChecker.matches_date :hour, [{:"/", 4}, 7], ~N[2004-04-16 04:07:08] + iex> Crontab.CronDateChecker.matches_date :hour, [{:"/", :*, 4}, 7], ~N[2004-04-16 04:07:08] true iex> Crontab.CronDateChecker.matches_date :hour, [8], ~N[2004-04-16 04:07:08] false - iex> Crontab.CronDateChecker.matches_date %Crontab.CronInterval{minute: [{:"/", 8}]}, ~N[2004-04-16 04:08:08] + iex> Crontab.CronDateChecker.matches_date %Crontab.CronInterval{minute: [{:"/", :*, 8}]}, ~N[2004-04-16 04:08:08] true - iex> Crontab.CronDateChecker.matches_date %Crontab.CronInterval{minute: [{:"/", 9}]}, ~N[2004-04-16 04:07:08] + iex> Crontab.CronDateChecker.matches_date %Crontab.CronInterval{minute: [{:"/", :*, 9}]}, ~N[2004-04-16 04:07:08] false """ def matches_date(_, [:* | _], _), do: true def matches_date(_, [], _), do: false - def matches_date(interval, [condition = {:/, _} | tail], execution_date) do + def matches_date(interval, [condition = {:/, _, _} | tail], execution_date) do values = get_interval_value(interval, execution_date) if matches_specific_date(interval, values, condition) do true @@ -58,6 +58,7 @@ defmodule Crontab.CronDateChecker do end defp matches_specific_date(_, [], _), do: false + defp matches_specific_date(_, _, :*), do: true defp matches_specific_date(interval, [head_value | tail_values], condition = {:-, from, to}) do cond do from > to && (head_value >= from || head_value <= to) -> true @@ -65,11 +66,18 @@ defmodule Crontab.CronDateChecker do true -> matches_specific_date(interval, tail_values, condition) end end - defp matches_specific_date(:weekday, [0 | tail_values], condition = {:/, _}) do + defp matches_specific_date(:weekday, [0 | tail_values], condition = {:/, _, _}) do matches_specific_date(:weekday, tail_values, condition) end - defp matches_specific_date(interval, [head_value | tail_values], condition = {:/, divider}) do - if rem(head_value, divider) == 0 do + defp matches_specific_date(interval, values = [head_value | tail_values], condition = {:/, base = {:-, from, _}, divider}) do + if matches_specific_date(interval, values, base) && rem(head_value - from, divider) == 0 do + true + else + matches_specific_date(interval, tail_values, condition) + end + end + defp matches_specific_date(interval, values = [head_value | tail_values], condition = {:/, base, divider}) do + if matches_specific_date(interval, values, base) && rem(head_value, divider) == 0 do true else matches_specific_date(interval, tail_values, condition) diff --git a/lib/crontab/cron_format_parser.ex b/lib/crontab/cron_format_parser.ex index 33b5dc7..e5f05c3 100644 --- a/lib/crontab/cron_format_parser.ex +++ b/lib/crontab/cron_format_parser.ex @@ -92,18 +92,11 @@ defmodule Crontab.CronFormatParser do end defp tokenize(_, "*"), do: {:ok, :*} - defp tokenize(_, "*/" <> divider) do - case Integer.parse(divider, 10) do - :error -> "Divider " <> divider <> " is not numeric." - {number, _} -> {:ok, {:"/", number}} - end - end - #defp tokenize(_, min <> "-" <> max), do: {:ok, {:-, min, max}} defp tokenize(interval, other) do - if Regex.match?(~r/^.+-.+$/, other) do - tokenize interval, :-, other - else - tokenize interval, :single_value, other + cond do + String.contains?(other, "/") -> tokenize interval, :complex_divider, other + Regex.match?(~r/^.+-.+$/, other) -> tokenize interval, :-, other + true -> tokenize interval, :single_value, other end end defp tokenize(interval, :-, whole_string) do @@ -117,6 +110,15 @@ defmodule Crontab.CronFormatParser do defp tokenize(interval, :single_value, value) do clean_value(interval, value) end + defp tokenize(interval, :complex_divider, value) do + [base, divider] = String.split value, "/" + + case {tokenize(interval, base), Integer.parse(divider, 10)} do + {{:ok, clean_base}, {clean_divider, _}} -> {:ok, {:/, clean_base, clean_divider}} + {_, :error} -> {:error, "Can't parse " <> value <> " as interval " <> Atom.to_string(interval) <> "."} + {error = {:error}, _} -> error + end + end defp clean_value(:weekday, value) do case {Map.fetch(@weekday_values, String.to_atom(String.upcase(value))), Integer.parse(value, 10)} do diff --git a/lib/crontab/cron_format_writer.ex b/lib/crontab/cron_format_writer.ex index 60415ae..879dee9 100644 --- a/lib/crontab/cron_format_writer.ex +++ b/lib/crontab/cron_format_writer.ex @@ -11,7 +11,7 @@ defmodule Crontab.CronFormatWriter do ### Examples iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{} "* * * * * *" - iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{minute: [9, {:-, 4, 6}, {:/, 9}]} + iex> Crontab.CronFormatWriter.write %Crontab.CronInterval{minute: [9, {:-, 4, 6}, {:/, :*, 9}]} "9,4-6,*/9 * * * * *" """ def write(cron_interval = %Crontab.CronInterval{}) do @@ -30,6 +30,6 @@ defmodule Crontab.CronFormatWriter do end def write(:*), do: "*" def write(number) when is_number(number), do: Integer.to_string(number) - def write({:/, divider}), do: "*/" <> Integer.to_string(divider) + def write({:/, base, divider}), do: write(base) <> "/" <> Integer.to_string(divider) def write({:-, min, max}), do: Integer.to_string(min) <> "-" <> Integer.to_string(max) end diff --git a/lib/crontab/cron_scheduler.ex b/lib/crontab/cron_scheduler.ex index ea0eea6..62c7240 100644 --- a/lib/crontab/cron_scheduler.ex +++ b/lib/crontab/cron_scheduler.ex @@ -17,7 +17,7 @@ defmodule Crontab.CronScheduler do iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{}, ~N[2002-01-13 23:00:07]) {:ok, ~N[2002-01-13 23:00:00]} - iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{year: [{:/, 9}]}, ~N[2002-01-13 23:00:07]) + iex> Crontab.CronScheduler.get_next_run_date(%Crontab.CronInterval{year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) {:ok, ~N[2007-01-01 00:00:00]} """ def get_next_run_date(cron_interval = %Crontab.CronInterval{}, date) do diff --git a/test/crontab/cron_date_checker_test.exs b/test/crontab/cron_date_checker_test.exs index a65816c..5fa9145 100644 --- a/test/crontab/cron_date_checker_test.exs +++ b/test/crontab/cron_date_checker_test.exs @@ -15,21 +15,21 @@ defmodule Crontab.CronDateCheckerTest do test "2004-04-16 04:04:08 matches */4 */4 */4 */5 */4" do base_date = ~N[2004-04-16 04:04:08] - assert matches_date(:minute, [{:"/", 4}], base_date) == true - assert matches_date(:hour, [{:"/", 4}], base_date) == true - assert matches_date(:day, [{:"/", 4}], base_date) == true - assert matches_date(:month, [{:"/", 4}], base_date) == true - assert matches_date(:weekday, [{:"/", 5}], base_date) == true - assert matches_date(:year, [{:"/", 4}], base_date) == true + assert matches_date(:minute, [{:"/", :*, 4}], base_date) == true + assert matches_date(:hour, [{:"/", :*, 4}], base_date) == true + assert matches_date(:day, [{:"/", :*, 4}], base_date) == true + assert matches_date(:month, [{:"/", :*, 4}], base_date) == true + assert matches_date(:weekday, [{:"/", :*, 5}], base_date) == true + assert matches_date(:year, [{:"/", :*, 4}], base_date) == true end test "2003-04-17 04:04:08 doesn't match */3 */3 */3 */3 */3" do base_date = ~N[2003-04-17 04:04:08] - assert matches_date(:minute, [{:"/", 3}], base_date) == false - assert matches_date(:hour, [{:"/", 3}], base_date) == false - assert matches_date(:day, [{:"/", 3}], base_date) == false - assert matches_date(:month, [{:"/", 3}], base_date) == false - assert matches_date(:weekday, [{:"/", 3}], base_date) == false - assert matches_date(:year, [{:"/", 3}], base_date) == false + assert matches_date(:minute, [{:"/", :*, 3}], base_date) == false + assert matches_date(:hour, [{:"/", :*, 3}], base_date) == false + assert matches_date(:day, [{:"/", :*, 3}], base_date) == false + assert matches_date(:month, [{:"/", :*, 3}], base_date) == false + assert matches_date(:weekday, [{:"/", :*, 3}], base_date) == false + assert matches_date(:year, [{:"/", :*, 3}], base_date) == false end end diff --git a/test/crontab/cron_format_parser_test.exs b/test/crontab/cron_format_parser_test.exs index c7307dc..1e936f7 100644 --- a/test/crontab/cron_format_parser_test.exs +++ b/test/crontab/cron_format_parser_test.exs @@ -47,8 +47,8 @@ defmodule Crontab.CronFormatParserTest do assert parse("*") == {:ok, %Crontab.CronInterval{minute: [:*], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*]}} end - test "parse \"*/4,9,1-10\" gives [{:\"/\", 4}, 9, {:\"-\", 1, 10}]" do - assert parse("*/4,9,1-10") == {:ok, %Crontab.CronInterval{minute: [{:"/", 4}, 9, {:-, 1, 10}], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*]}} + test "parse \"*/4,9,1-10\" gives [{:\"/\", :*, 4}, 9, {:\"-\", 1, 10}]" do + assert parse("*/4,9,1-10") == {:ok, %Crontab.CronInterval{minute: [{:"/", :*, 4}, 9, {:-, 1, 10}], hour: [:*], day: [:*], month: [:*], weekday: [:*], year: [:*]}} end test "parse \"*/4,9,JAN-DEC\" gives error" do diff --git a/test/crontab/cron_scheduler_test.exs b/test/crontab/cron_scheduler_test.exs index 927b932..93113c3 100644 --- a/test/crontab/cron_scheduler_test.exs +++ b/test/crontab/cron_scheduler_test.exs @@ -4,19 +4,19 @@ defmodule Crontab.CronSchedulerTest do import Crontab.CronScheduler test "check cron expression for year" do - assert get_next_run_date(%Crontab.CronInterval{year: [{:/, 9}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2007-01-01 00:00:00]} + assert get_next_run_date(%Crontab.CronInterval{year: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2007-01-01 00:00:00]} end test "check cron expression for weekday" do - assert get_next_run_date(%Crontab.CronInterval{weekday: [{:/, 3}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-01-16 00:00:00]} + assert get_next_run_date(%Crontab.CronInterval{weekday: [{:/, :*, 3}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-01-16 00:00:00]} end test "check cron expression for month" do - assert get_next_run_date(%Crontab.CronInterval{month: [{:/, 9}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-09-01 00:00:00]} + assert get_next_run_date(%Crontab.CronInterval{month: [{:/, :*, 9}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-09-01 00:00:00]} end test "check cron expression for day" do - assert get_next_run_date(%Crontab.CronInterval{day: [{:/, 16}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-01-16 00:00:00]} + assert get_next_run_date(%Crontab.CronInterval{day: [{:/, :*, 16}]}, ~N[2002-01-13 23:00:07]) == {:ok, ~N[2002-01-16 00:00:00]} end test "check cron expression for hour" do diff --git a/test/crontab/functional_test.exs b/test/crontab/functional_test.exs index 1cb00e8..54e3b58 100644 --- a/test/crontab/functional_test.exs +++ b/test/crontab/functional_test.exs @@ -5,21 +5,21 @@ defmodule Crontab.FunctionalTest do {"*/2 */2 * * *", ~N[2015-08-10 21:47:27], ~N[2015-08-10 22:00:00], false}, {"* * * * *", ~N[2015-08-10 21:50:37], ~N[2015-08-10 21:50:00], true}, {"* 20,21,22 * * *", ~N[2015-08-10 21:50:00], ~N[2015-08-10 21:50:00], true}, - # Handles CSV values + # Handles CSV values {"* 20,22 * * *", ~N[2015-08-10 21:50:00], ~N[2015-08-10 22:00:00], false}, - # CSV values can be complex + # CSV values can be complex {"* 5,21-22 * * *", ~N[2015-08-10 21:50:00], ~N[2015-08-10 21:50:00], true}, {"7-9 * */9 * *", ~N[2015-08-10 22:02:33], ~N[2015-08-18 00:07:00], false}, - # 15th minute, of the second hour, every 15 days, in January, every Friday + # 15th minute, of the second hour, every 15 days, in January, every Friday {"1 * * * 7", ~N[2015-08-10 21:47:27], ~N[2015-08-16 00:01:00], false}, - # Test with exact times + # Test with exact times {"47 21 * * *", ~N[2015-08-10 21:47:30], ~N[2015-08-10 21:47:00], true}, - # Test Day of the week (issue #1) - # According cron implementation, 0|7 = sunday, 1 => monday, etc + # Test Day of the week (issue #1) + # According cron implementation, 0|7 = sunday, 1 => monday, etc {"* * * * 0", ~N[2011-06-15 23:09:00], ~N[2011-06-19 00:00:00], false}, {"* * * * 7", ~N[2011-06-15 23:09:00], ~N[2011-06-19 00:00:00], false}, {"* * * * 1", ~N[2011-06-15 23:09:00], ~N[2011-06-20 00:00:00], false}, - # Should return the sunday date as 7 equals 0 + # Should return the sunday date as 7 equals 0 {"0 0 * * MON,SUN", ~N[2011-06-15 23:09:00], ~N[2011-06-19 00:00:00], false}, {"0 0 * * 1,7", ~N[2011-06-15 23:09:00], ~N[2011-06-19 00:00:00], false}, {"0 0 * * 0-4", ~N[2011-06-15 23:09:00], ~N[2011-06-16 00:00:00], false}, @@ -28,22 +28,22 @@ defmodule Crontab.FunctionalTest do {"0 0 * * 7-3", ~N[2011-06-15 23:09:00], ~N[2011-06-19 00:00:00], false}, {"0 0 * * 3-7", ~N[2011-06-15 23:09:00], ~N[2011-06-16 00:00:00], false}, {"0 0 * * 3-7", ~N[2011-06-18 23:09:00], ~N[2011-06-19 00:00:00], false}, - # Test lists of values and ranges (Abhoryo) + # Test lists of values and ranges (Abhoryo) {"0 0 * * 2-7", ~N[2011-06-20 23:09:00], ~N[2011-06-21 00:00:00], false}, {"0 0 * * 0,2-6", ~N[2011-06-20 23:09:00], ~N[2011-06-21 00:00:00], false}, {"0 0 * * 2-7", ~N[2011-06-18 23:09:00], ~N[2011-06-19 00:00:00], false}, {"0 0 * * 4-7", ~N[2011-07-19 00:00:00], ~N[2011-07-21 00:00:00], false}, - # Test increments of ranges + # Test increments of ranges {"0-12/4 * * * *", ~N[2011-06-20 12:04:00], ~N[2011-06-20 12:04:00], true}, - #{"4-59/2 * * * *", ~N[2011-06-20 12:04:00], ~N[2011-06-20 12:04:00], true}, - #{"4-59/2 * * * *", ~N[2011-06-20 12:06:00], ~N[2011-06-20 12:06:00], true}, - #{"4-59/3 * * * *", ~N[2011-06-20 12:06:00], ~N[2011-06-20 12:07:00], false}, - # Test Day of the Week and the Day of the Month (issue #1) + {"4-59/2 * * * *", ~N[2011-06-20 12:04:00], ~N[2011-06-20 12:04:00], true}, + {"4-59/2 * * * *", ~N[2011-06-20 12:06:00], ~N[2011-06-20 12:06:00], true}, + {"4-59/3 * * * *", ~N[2011-06-20 12:06:00], ~N[2011-06-20 12:07:00], false}, + # Test Day of the Week and the Day of the Month (issue #1) {"0 0 1 1 0", ~N[2011-06-15 23:09:00], ~N[2012-01-01 00:00:00], false}, {"0 0 1 JAN 0", ~N[2011-06-15 23:09:00], ~N[2012-01-01 00:00:00], false}, {"0 0 1 * 0", ~N[2011-06-15 23:09:00], ~N[2012-01-01 00:00:00], false}, - #{"0 0 L * *", ~N[2011-07-15 00:00:00], ~N[2011-07-31 00:00:00], false}, - # Test the W day of the week modifier for day of the month field + # {"0 0 L * *", ~N[2011-07-15 00:00:00], ~N[2011-07-31 00:00:00], false}, + # Test the W day of the week modifier for day of the month field # {"0 0 2W * *", ~N[2011-07-01 00:00:00], ~N[2011-07-01 00:00:00], true}, # {"0 0 1W * *", ~N[2011-05-01 00:00:00], ~N[2011-05-02 00:00:00], false}, # {"0 0 1W * *", ~N[2011-07-01 00:00:00], ~N[2011-07-01 00:00:00], true}, @@ -52,16 +52,16 @@ defmodule Crontab.FunctionalTest do # {"0 0 28W * *", ~N[2011-07-01 00:00:00], ~N[2011-07-28 00:00:00], false}, # {"0 0 30W * *", ~N[2011-07-01 00:00:00], ~N[2011-07-29 00:00:00], false}, # {"0 0 31W * *", ~N[2011-07-01 00:00:00], ~N[2011-07-29 00:00:00], false}, - # Test the year field + # Test the year field {"* * * * * 2012", ~N[2011-05-01 00:00:00], ~N[2012-01-01 00:00:00], false}, - # Test the last weekday of a month + # Test the last weekday of a month # {"* * * * 5L", ~N[2011-07-01 00:00:00], ~N[2011-07-29 00:00:00], false}, # {"* * * * 6L", ~N[2011-07-01 00:00:00], ~N[2011-07-30 00:00:00], false}, # {"* * * * 7L", ~N[2011-07-01 00:00:00], ~N[2011-07-31 00:00:00], false}, # {"* * * * 1L", ~N[2011-07-24 00:00:00], ~N[2011-07-25 00:00:00], false}, # {"* * * * TUEL", ~N[2011-07-24 00:00:00], ~N[2011-07-26 00:00:00], false}, # {"* * * 1 5L", ~N[2011-12-25 00:00:00], ~N[2012-01-27 00:00:00], false}, - # # Test the hash symbol for the nth weekday of a given month + # # Test the hash symbol for the nth weekday of a given month # {"* * * * 5#2", ~N[2011-07-01 00:00:00], ~N[2011-07-08 00:00:00], false}, # {"* * * * 5#1", ~N[2011-07-01 00:00:00], ~N[2011-07-01 00:00:00], true}, # {"* * * * 3#4", ~N[2011-07-01 00:00:00], ~N[2011-07-27 00:00:00], false},