From 9fb0fba82133d71ba3a22b8dacf730e6570b370c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20M=C3=A4nnchen?= Date: Tue, 4 Aug 2020 20:03:55 +0200 Subject: [PATCH 01/15] DateHelper DateTime Support --- lib/crontab/date_helper.ex | 268 +++++++++++++++++++++++++++--- test/crontab/date_helper_test.exs | 2 +- 2 files changed, 244 insertions(+), 26 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index bbf4c47..fdc45aa 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -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}}, @@ -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 @@ -35,16 +40,28 @@ 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) @@ -52,9 +69,19 @@ defmodule Crontab.DateHelper do 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) @@ -62,7 +89,16 @@ defmodule Crontab.DateHelper do 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 @@ -70,9 +106,18 @@ defmodule Crontab.DateHelper do 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() + @spec next_weekday_to(date :: date) :: 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) @@ -87,8 +132,36 @@ defmodule Crontab.DateHelper do end end - @spec inc_year(NaiveDateTime.t()) :: NaiveDateTime.t() - def inc_year(date) do + def next_weekday_to(date = %DateTime{year: year, month: month, day: day}) do + weekday = :calendar.day_of_the_week(year, month, day) + # FIXME: How to correct date with tz? + next_day = DateTime.add(date, 1, :day) + # FIXME: How to correct date with tz? + previous_day = DateTime.add(date, -1, :day) + + cond do + weekday == 7 && next_day.month == date.month -> next_day.day + weekday == 7 -> DateTime.add(date, -2, :day).day + weekday == 6 && previous_day.month == date.month -> previous_day.day + weekday == 6 -> DateTime.add(date, 2, :day).day + true -> date.day + end + end + + @doc """ + Increment Year + + ### Examples: + + iex> Crontab.DateHelper.inc_year(~N[2016-03-14 01:45:45.123]) + ~N[2017-03-15 01:45:45.123] + + iex> Crontab.DateHelper.inc_year(~U[2016-03-14 01:45:45.123Z]) + ~U[2017-03-15 01:45:45.123Z] + + """ + @spec inc_year(date) :: date when date: date + def inc_year(date = %NaiveDateTime{}) do leap_year? = date |> NaiveDateTime.to_date() @@ -101,8 +174,35 @@ defmodule Crontab.DateHelper do end end - @spec dec_year(NaiveDateTime.t()) :: NaiveDateTime.t() - def dec_year(date) do + def inc_year(date = %DateTime{}) do + leap_year? = + date + |> DateTime.to_date() + |> Date.leap_year?() + + if leap_year? do + # FIXME: How to correct date with tz? + DateTime.add(date, 366, :day) + else + # FIXME: How to correct date with tz? + DateTime.add(date, 365, :day) + end + end + + @doc """ + Decrement Year + + ### 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 = %NaiveDateTime{}) do leap_year? = date |> NaiveDateTime.to_date() @@ -115,7 +215,34 @@ defmodule Crontab.DateHelper do end end - @spec inc_month(NaiveDateTime.t()) :: NaiveDateTime.t() + def dec_year(date = %DateTime{}) do + leap_year? = + date + |> DateTime.to_date() + |> Date.leap_year?() + + if leap_year? do + # FIXME: How to correct date with tz? + DateTime.add(date, -366, :day) + else + # FIXME: How to correct date with tz? + DateTime.add(date, -365, :day) + end + end + + @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 = %NaiveDateTime{day: day}) do days = date @@ -125,25 +252,69 @@ defmodule Crontab.DateHelper do NaiveDateTime.add(date, days + 1 - day, :day) end - @spec dec_month(NaiveDateTime.t()) :: NaiveDateTime.t() - def dec_month(date) do + def inc_month(date = %DateTime{day: day}) do days = + date + |> DateTime.to_date() + |> Date.days_in_month() + + # FIXME: How to correct date with tz? + DateTime.add(date, days + 1 - day, :day) + end + + @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] + + 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 = %NaiveDateTime{day: day}) do + days_in_last_month = date |> NaiveDateTime.to_date() + |> day_in_last_month + |> Date.days_in_month() + + NaiveDateTime.add(date, -(day + max(days_in_last_month - day, 0)), :day) + end + + def dec_month(date = %DateTime{day: day}) do + days_in_last_month = + date + |> DateTime.to_date() + |> day_in_last_month |> Date.days_in_month() - NaiveDateTime.add(date, -days, :day) + # FIXME: How to correct date with tz? + DateTime.add(date, -(day + max(days_in_last_month - day, 0)), :day) end - @spec _beginning_of(NaiveDateTime.t(), [{unit, {any, any}}]) :: NaiveDateTime.t() + defp day_in_last_month(start_date), do: day_in_last_month(start_date, start_date) + + defp day_in_last_month(date = %Date{month: month}, start_date = %Date{month: month}), + do: date |> Date.add(-1) |> day_in_last_month(start_date) + + defp day_in_last_month(date, _start_date), do: date + + @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 + @spec _end_of(date, [{unit, {any, any}}]) :: date when date: date + defp _end_of(date = %NaiveDateTime{}, [{unit, {_, :end_of_month}} | tail]) do upper = date |> NaiveDateTime.to_date() @@ -152,6 +323,16 @@ defmodule Crontab.DateHelper do _end_of(Map.put(date, unit, upper), tail) end + defp _end_of(date = %DateTime{}, [{unit, {_, :end_of_month}} | tail]) do + upper = + date + |> DateTime.to_date() + |> Date.days_in_month() + + # FIXME: How to correct date with tz? + _end_of(Map.put(date, unit, upper), tail) + end + defp _end_of(date, [{unit, {_, upper}} | tail]) do _end_of(Map.put(date, unit, upper), tail) end @@ -165,7 +346,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}] @@ -174,14 +355,20 @@ defmodule Crontab.DateHelper do [] end end) + |> Enum.reverse() units end - @spec nth_weekday(NaiveDateTime.t(), Calendar.day_of_week(), :start) :: boolean + @spec nth_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :start) :: + boolean defp nth_weekday(date = %NaiveDateTime{}, _, 0, :start), do: NaiveDateTime.add(date, -1, :day).day + # FIXME: How to correct date with tz? + defp nth_weekday(date = %DateTime{}, _, 0, :start), + do: DateTime.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) @@ -190,7 +377,17 @@ defmodule Crontab.DateHelper do end end - @spec last_weekday_of_month(NaiveDateTime.t(), :end) :: Calendar.day() + defp nth_weekday(date = %DateTime{year: year, month: month, day: day}, weekday, n, :start) do + if :calendar.day_of_the_week(year, month, day) == weekday do + # FIXME: How to correct date with tz? + nth_weekday(DateTime.add(date, 1, :day), weekday, n - 1, :start) + else + # FIXME: How to correct date with tz? + nth_weekday(DateTime.add(date, 1, :day), weekday, n, :start) + end + end + + @spec last_weekday_of_month(date :: date(), position :: :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) @@ -201,7 +398,19 @@ defmodule Crontab.DateHelper do end end - @spec last_weekday(NaiveDateTime.t(), non_neg_integer, :end) :: Calendar.day() + defp last_weekday_of_month(date = %DateTime{year: year, month: month, day: day}, :end) do + weekday = :calendar.day_of_the_week(year, month, day) + + if weekday > 5 do + # FIXME: How to correct date with tz? + last_weekday_of_month(DateTime.add(date, -1, :day), :end) + else + day + end + end + + @spec last_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :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 day @@ -209,4 +418,13 @@ defmodule Crontab.DateHelper do last_weekday(NaiveDateTime.add(date, -1, :day), weekday, :end) end end + + defp last_weekday(date = %DateTime{year: year, month: month, day: day}, weekday, :end) do + if :calendar.day_of_the_week(year, month, day) == weekday do + day + else + # FIXME: How to correct date with tz? + last_weekday(DateTime.add(date, -1, :day), weekday, :end) + end + end end diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index f1bfe44..f6eb7bc 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -6,7 +6,7 @@ defmodule Crontab.DateHelperTest do doctest Crontab.DateHelper describe "inc_month/1" do - test "does not jump obver month" do + test "does not jump over month" do assert Crontab.DateHelper.inc_month(~N[2019-05-31 23:00:00]) == ~N[2019-06-01 23:00:00] end end From 939315520cc13fd66e9c2c4fde7041724ece6664 Mon Sep 17 00:00:00 2001 From: shaolang Date: Sun, 11 Feb 2024 20:42:36 +0800 Subject: [PATCH 02/15] Add mix_test_watch as dev dep --- mix.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 59014c5..a5fe4a7 100644 --- a/mix.exs +++ b/mix.exs @@ -43,7 +43,8 @@ defmodule Crontab.Mixfile do {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:excoveralls, "~> 0.5", only: [:test], runtime: false}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, - {:credo, "~> 1.0", only: [:dev], runtime: false} + {:credo, "~> 1.0", only: [:dev], runtime: false}, + {:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false} ] end From 5a556d8f8a152b4ee7fc101690c23c0b0ecb91f7 Mon Sep 17 00:00:00 2001 From: shaolang Date: Sun, 11 Feb 2024 21:33:00 +0800 Subject: [PATCH 03/15] Simplify date_helper with extracted add/3 and days_in_year/1 --- lib/crontab/date_checker.ex | 16 +-- lib/crontab/date_helper.ex | 198 +++++++----------------------------- 2 files changed, 43 insertions(+), 171 deletions(-) diff --git a/lib/crontab/date_checker.ex b/lib/crontab/date_checker.ex index 3d4df08..ef0a4c2 100644 --- a/lib/crontab/date_checker.ex +++ b/lib/crontab/date_checker.ex @@ -7,8 +7,6 @@ defmodule Crontab.DateChecker do alias Crontab.DateHelper - @type date :: NaiveDateTime.t() | DateTime.t() - @doc """ Check a condition list against a given date. @@ -27,7 +25,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 :: DateHelper.date()) :: + boolean | no_return def matches_date?(cron_expression_or_condition_list, date) def matches_date?(%CronExpression{reboot: true}, _), @@ -39,7 +38,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 :: DateHelper.date() + ) :: boolean def matches_date?([], _), do: true def matches_date?([{interval, conditions} | tail], execution_date) do @@ -61,7 +63,7 @@ defmodule Crontab.DateChecker do @spec matches_date?( interval :: CronExpression.interval(), condition_list :: CronExpression.condition_list(), - date :: date + date :: DateHelper.date() ) :: boolean def matches_date?(_, [:* | _], _), do: true def matches_date?(_, [], _), do: false @@ -80,7 +82,7 @@ defmodule Crontab.DateChecker do interval :: CronExpression.interval(), values :: [CronExpression.time_unit()], condition :: CronExpression.value(), - date :: date + date :: DateHelper.date() ) :: boolean defp matches_specific_date?(_, [], _, _), do: false defp matches_specific_date?(_, _, :*, _), do: true @@ -171,7 +173,7 @@ defmodule Crontab.DateChecker do end end - @spec get_interval_value(interval :: CronExpression.interval(), date :: date) :: [ + @spec get_interval_value(interval :: CronExpression.interval(), date :: DateHelper.date()) :: [ CronExpression.time_unit() ] defp get_interval_value(:second, %{second: second}), do: [second] diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index fdc45aa..c3d4dd1 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -100,10 +100,8 @@ defmodule Crontab.DateHelper do 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 next occurrence of weekday relative to date @@ -118,32 +116,16 @@ defmodule Crontab.DateHelper do """ @spec next_weekday_to(date :: date) :: 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) - - cond do - weekday == 7 && next_day.month == date.month -> next_day.day - weekday == 7 -> NaiveDateTime.add(date, -2, :day).day - weekday == 6 && previous_day.month == date.month -> previous_day.day - weekday == 6 -> NaiveDateTime.add(date, 2, :day).day - true -> date.day - end - end - - def next_weekday_to(date = %DateTime{year: year, month: month, day: day}) do + def next_weekday_to(date = %{year: year, month: month, day: day}) do weekday = :calendar.day_of_the_week(year, month, day) - # FIXME: How to correct date with tz? - next_day = DateTime.add(date, 1, :day) - # FIXME: How to correct date with tz? - previous_day = DateTime.add(date, -1, :day) + 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 -> DateTime.add(date, -2, :day).day + weekday == 7 -> add(date, -2, :day).day weekday == 6 && previous_day.month == date.month -> previous_day.day - weekday == 6 -> DateTime.add(date, 2, :day).day + weekday == 6 -> add(date, 2, :day).day true -> date.day end end @@ -161,33 +143,7 @@ defmodule Crontab.DateHelper do """ @spec inc_year(date) :: date when date: date - def inc_year(date = %NaiveDateTime{}) do - leap_year? = - date - |> NaiveDateTime.to_date() - |> Date.leap_year?() - - if leap_year? do - NaiveDateTime.add(date, 366, :day) - else - NaiveDateTime.add(date, 365, :day) - end - end - - def inc_year(date = %DateTime{}) do - leap_year? = - date - |> DateTime.to_date() - |> Date.leap_year?() - - if leap_year? do - # FIXME: How to correct date with tz? - DateTime.add(date, 366, :day) - else - # FIXME: How to correct date with tz? - DateTime.add(date, 365, :day) - end - end + def inc_year(date), do: add(date, days_in_year(date), :day) @doc """ Decrement Year @@ -202,33 +158,7 @@ defmodule Crontab.DateHelper do """ @spec dec_year(date) :: date when date: date - def dec_year(date = %NaiveDateTime{}) do - leap_year? = - date - |> NaiveDateTime.to_date() - |> Date.leap_year?() - - if leap_year? do - NaiveDateTime.add(date, -366, :day) - else - NaiveDateTime.add(date, -365, :day) - end - end - - def dec_year(date = %DateTime{}) do - leap_year? = - date - |> DateTime.to_date() - |> Date.leap_year?() - - if leap_year? do - # FIXME: How to correct date with tz? - DateTime.add(date, -366, :day) - else - # FIXME: How to correct date with tz? - DateTime.add(date, -365, :day) - end - end + def dec_year(date), do: add(date, -days_in_year(date), :day) @doc """ Increment Month @@ -243,23 +173,12 @@ defmodule Crontab.DateHelper do """ @spec inc_month(date) :: date when date: date - def inc_month(date = %NaiveDateTime{day: day}) do - days = - date - |> NaiveDateTime.to_date() - |> Date.days_in_month() - - NaiveDateTime.add(date, days + 1 - day, :day) - end - - def inc_month(date = %DateTime{day: day}) do + def inc_month(date = %{year: year, month: month, day: day}) do days = - date - |> DateTime.to_date() + Date.new!(year, month, day) |> Date.days_in_month() - # FIXME: How to correct date with tz? - DateTime.add(date, days + 1 - day, :day) + add(date, days + 1 - day, :day) end @doc """ @@ -278,25 +197,13 @@ defmodule Crontab.DateHelper do """ @spec dec_month(date) :: date when date: date - def dec_month(date = %NaiveDateTime{day: day}) do - days_in_last_month = - date - |> NaiveDateTime.to_date() - |> day_in_last_month - |> Date.days_in_month() - - NaiveDateTime.add(date, -(day + max(days_in_last_month - day, 0)), :day) - end - - def dec_month(date = %DateTime{day: day}) do + def dec_month(date = %{year: year, month: month, day: day}) do days_in_last_month = - date - |> DateTime.to_date() + Date.new!(year, month, day) |> day_in_last_month |> Date.days_in_month() - # FIXME: How to correct date with tz? - DateTime.add(date, -(day + max(days_in_last_month - day, 0)), :day) + add(date, -(day + max(days_in_last_month - day, 0)), :day) end defp day_in_last_month(start_date), do: day_in_last_month(start_date, start_date) @@ -314,22 +221,11 @@ defmodule Crontab.DateHelper do defp _beginning_of(date, []), do: date @spec _end_of(date, [{unit, {any, any}}]) :: date when date: date - defp _end_of(date = %NaiveDateTime{}, [{unit, {_, :end_of_month}} | tail]) do - upper = - date - |> NaiveDateTime.to_date() - |> Date.days_in_month() - - _end_of(Map.put(date, unit, upper), tail) - end - - defp _end_of(date = %DateTime{}, [{unit, {_, :end_of_month}} | tail]) do + defp _end_of(date = %{year: year, month: month, day: day}, [{unit, {_, :end_of_month}} | tail]) do upper = - date - |> DateTime.to_date() + Date.new!(year, month, day) |> Date.days_in_month() - # FIXME: How to correct date with tz? _end_of(Map.put(date, unit, upper), tail) end @@ -362,48 +258,19 @@ defmodule Crontab.DateHelper do @spec nth_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :start) :: boolean - defp nth_weekday(date = %NaiveDateTime{}, _, 0, :start), - do: NaiveDateTime.add(date, -1, :day).day - - # FIXME: How to correct date with tz? - defp nth_weekday(date = %DateTime{}, _, 0, :start), - do: DateTime.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 - end + defp nth_weekday(date, _, 0, :start), do: add(date, -1, :day).day - defp nth_weekday(date = %DateTime{year: year, month: month, day: day}, weekday, n, :start) do - if :calendar.day_of_the_week(year, month, day) == weekday do - # FIXME: How to correct date with tz? - nth_weekday(DateTime.add(date, 1, :day), weekday, n - 1, :start) - else - # FIXME: How to correct date with tz? - nth_weekday(DateTime.add(date, 1, :day), weekday, n, :start) - end + defp nth_weekday(date = %{year: year, month: month, day: day}, weekday, n, :start) do + modifier = if :calendar.day_of_the_week(year, month, day) == weekday, do: n - 1, else: n + nth_weekday(add(date, 1, :day), weekday, modifier, :start) end @spec last_weekday_of_month(date :: date(), position :: :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) - else - day - end - end - - defp last_weekday_of_month(date = %DateTime{year: year, month: month, day: day}, :end) do + defp last_weekday_of_month(date = %{year: year, month: month, day: day}, :end) do weekday = :calendar.day_of_the_week(year, month, day) if weekday > 5 do - # FIXME: How to correct date with tz? - last_weekday_of_month(DateTime.add(date, -1, :day), :end) + last_weekday_of_month(add(date, -1, :day), :end) else day end @@ -411,20 +278,23 @@ defmodule Crontab.DateHelper do @spec last_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :end) :: Calendar.day() - defp last_weekday(date = %NaiveDateTime{year: year, month: month, day: day}, weekday, :end) do + defp last_weekday(date = %{year: year, month: month, day: day}, weekday, :end) do if :calendar.day_of_the_week(year, month, day) == weekday do day else - last_weekday(NaiveDateTime.add(date, -1, :day), weekday, :end) + last_weekday(add(date, -1, :day), weekday, :end) end end - defp last_weekday(date = %DateTime{year: year, month: month, day: day}, weekday, :end) do - if :calendar.day_of_the_week(year, month, day) == weekday do - day - else - # FIXME: How to correct date with tz? - last_weekday(DateTime.add(date, -1, :day), weekday, :end) + defp add(datetime = %DateTime{}, amt, unit), do: DateTime.add(datetime, amt, unit) + defp add(datetime = %NaiveDateTime{}, amt, unit), do: NaiveDateTime.add(datetime, amt, unit) + + defp days_in_year(%{year: year}) do + Date.new!(year, 1, 1) + |> Date.leap_year?() + |> case do + true -> 366 + false -> 365 end end end From fe2920eb98e495358eeb262b1d52cbde3b921e05 Mon Sep 17 00:00:00 2001 From: shaolang Date: Mon, 12 Feb 2024 23:25:15 +0800 Subject: [PATCH 04/15] Handle daylight savings start and end calculations --- lib/crontab/date_helper.ex | 37 ++++++++++++++++-- mix.exs | 4 +- test/crontab/date_helper_test.exs | 63 ++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 5 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index c3d4dd1..f2d98d6 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -1,4 +1,6 @@ defmodule Crontab.DateHelper do + use Private + @moduledoc false @type unit :: :year | :month | :day | :hour | :minute | :second | :microsecond @@ -286,9 +288,6 @@ defmodule Crontab.DateHelper do end end - defp add(datetime = %DateTime{}, amt, unit), do: DateTime.add(datetime, amt, unit) - defp add(datetime = %NaiveDateTime{}, amt, unit), do: NaiveDateTime.add(datetime, amt, unit) - defp days_in_year(%{year: year}) do Date.new!(year, 1, 1) |> Date.leap_year?() @@ -297,4 +296,36 @@ defmodule Crontab.DateHelper do false -> 365 end end + + private do + 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) + + case adjusted.std_offset == candidate.std_offset do + false -> + candidate + + true -> + adj_plus_1h = DateTime.add(adjusted, 1, :hour) + + case adj_plus_1h.std_offset == adjusted.std_offset do + true -> + adjusted + + _ -> + # the one hour at end of daylight savings with ambiguous timezone + # return datetime with the standard (not daylight savings) timezone + %DateTime{ + adjusted + | std_offset: adj_plus_1h.std_offset, + zone_abbr: adj_plus_1h.zone_abbr + } + end + end + end + end end diff --git a/mix.exs b/mix.exs index a5fe4a7..627b4ff 100644 --- a/mix.exs +++ b/mix.exs @@ -44,7 +44,9 @@ defmodule Crontab.Mixfile do {:excoveralls, "~> 0.5", only: [:test], runtime: false}, {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:credo, "~> 1.0", only: [:dev], runtime: false}, - {:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false} + {:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false}, + {:private, "~> 0.1", only: [:dev, :test], runtime: false}, + {:tz, "~> 0.26", only: [:dev, :test]} ] end diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index f6eb7bc..b1e3d92 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -4,10 +4,71 @@ defmodule Crontab.DateHelperTest do use ExUnit.Case, async: true doctest Crontab.DateHelper + alias Crontab.DateHelper describe "inc_month/1" do test "does not jump over month" do - assert Crontab.DateHelper.inc_month(~N[2019-05-31 23:00:00]) == ~N[2019-06-01 23:00:00] + assert DateHelper.inc_month(~N[2019-05-31 23:00:00]) == ~N[2019-06-01 23:00:00] + end + end + + describe "add/3 on NaiveDateTime" do + test "one day to day before NY DST starts" do + date = ~N[2024-03-09 12:34:56] + assert DateHelper.add(date, 1, :day) == ~N[2024-03-10 12:34:56] + end + end + + describe "add/3 on DateTime UTC" do + test "one day to day before NY DST starts" do + date = ~U[2024-03-09 12:34:56Z] + assert DateHelper.add(date, 1, :day) == ~U[2024-03-10 12:34:56Z] + end + end + + describe "add/3 on DateTime NYT" do + test "one day to day before NY DST starts" do + day_before = DateTime.from_naive!(~N[2024-03-09 12:34:56], "America/New_York") + expected = DateTime.from_naive!(~N[2024-03-10 12:34:56], "America/New_York") + + assert DateHelper.add(day_before, 1, :day) == expected + end + + test "one day to day before NY DST ends" do + day_before = DateTime.from_naive!(~N[2024-11-02 12:34:56], "America/New_York") + expected = DateTime.from_naive!(~N[2024-11-03 12:34:56], "America/New_York") + + assert DateHelper.add(day_before, 1, :day) == expected + end + + for {unit, time} <- [{:second, ~T[03:00:00]}, {:minute, ~T[03:00:59]}, {:hour, ~T[03:59:59]}] do + test "one #{unit} to one second before NY DST starts" do + one_sec_before = DateTime.from_naive!(~N[2024-03-10 01:59:59], "America/New_York") + expected = DateTime.new!(~D[2024-03-10], unquote(Macro.escape(time)), "America/New_York") + + assert DateHelper.add(one_sec_before, 1, unquote(unit)) == expected + end + end + + # , {:minute, ~T[01:00:59]}, {:hour, ~T[01:59:59]}] do + for {unit, hour, minute, second} <- [{:second, 1, 0, 0}, {:minute, 1, 0, 59}, {:hour, 1, 59, 59}] do + test "one #{unit} to one second before NY DST ends" do + one_sec_before = DateTime.from_naive!(~N[2024-11-03 00:59:59], "America/New_York") + + # 'cos 1:00:00 to 1:59:00 can be represented as timezones for EDT and EST, + # so "work backwards" by getting the EST time from 2:00 onwards then minus 1 hour + two_plus = Time.new!(unquote(hour) + 1, unquote(minute), unquote(second)) + + expected = + DateTime.new!(~D[2024-11-03], two_plus, "America/New_York") + |> DateTime.add(-1, :hour) + + assert DateHelper.add(one_sec_before, 1, unquote(unit)) == expected + end + end + + setup do + Calendar.put_time_zone_database(Tz.TimeZoneDatabase) end end end From 22b21bd25cabf6f82ee52640f5a4045443c38bf5 Mon Sep 17 00:00:00 2001 From: shaolang Date: Mon, 12 Feb 2024 23:38:08 +0800 Subject: [PATCH 05/15] Fix formatting issues --- test/crontab/date_helper_test.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index b1e3d92..dc2c014 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -51,7 +51,11 @@ defmodule Crontab.DateHelperTest do end # , {:minute, ~T[01:00:59]}, {:hour, ~T[01:59:59]}] do - for {unit, hour, minute, second} <- [{:second, 1, 0, 0}, {:minute, 1, 0, 59}, {:hour, 1, 59, 59}] do + for {unit, hour, minute, second} <- [ + {:second, 1, 0, 0}, + {:minute, 1, 0, 59}, + {:hour, 1, 59, 59} + ] do test "one #{unit} to one second before NY DST ends" do one_sec_before = DateTime.from_naive!(~N[2024-11-03 00:59:59], "America/New_York") From b71ead253ffb822785c6256e5158556a437b9327 Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 00:08:03 +0800 Subject: [PATCH 06/15] Use DateTime.from_naive/2 to determine ambiguity --- lib/crontab/date_helper.ex | 19 ++++--------------- test/crontab/date_helper_test.exs | 1 - 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index f2d98d6..1c3ecee 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -309,21 +309,10 @@ defmodule Crontab.DateHelper do false -> candidate - true -> - adj_plus_1h = DateTime.add(adjusted, 1, :hour) - - case adj_plus_1h.std_offset == adjusted.std_offset do - true -> - adjusted - - _ -> - # the one hour at end of daylight savings with ambiguous timezone - # return datetime with the standard (not daylight savings) timezone - %DateTime{ - adjusted - | std_offset: adj_plus_1h.std_offset, - zone_abbr: adj_plus_1h.zone_abbr - } + _ -> + case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do + {:ambiguous, _, target} -> target + {:ok, target} -> target end end end diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index dc2c014..93694ad 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -50,7 +50,6 @@ defmodule Crontab.DateHelperTest do end end - # , {:minute, ~T[01:00:59]}, {:hour, ~T[01:59:59]}] do for {unit, hour, minute, second} <- [ {:second, 1, 0, 0}, {:minute, 1, 0, 59}, From 1fbaaf98dffbd174de29c8e913967e956191d224 Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 00:11:10 +0800 Subject: [PATCH 07/15] Remove private as dep --- lib/crontab/date_helper.ex | 31 ++++++++++++++----------------- mix.exs | 1 - 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index 1c3ecee..b5f3a8c 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -1,6 +1,4 @@ defmodule Crontab.DateHelper do - use Private - @moduledoc false @type unit :: :year | :month | :day | :hour | :minute | :second | :microsecond @@ -297,24 +295,23 @@ defmodule Crontab.DateHelper do end end - private do - def add(datetime = %NaiveDateTime{}, amt, unit), do: NaiveDateTime.add(datetime, amt, unit) + @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) + 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) - case adjusted.std_offset == candidate.std_offset do - false -> - candidate + case adjusted.std_offset == candidate.std_offset do + false -> + candidate - _ -> - case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do - {:ambiguous, _, target} -> target - {:ok, target} -> target - end - end + _ -> + case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do + {:ambiguous, _, target} -> target + {:ok, target} -> target + end end end end diff --git a/mix.exs b/mix.exs index 627b4ff..5ed6d02 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,6 @@ defmodule Crontab.Mixfile do {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:credo, "~> 1.0", only: [:dev], runtime: false}, {:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false}, - {:private, "~> 0.1", only: [:dev, :test], runtime: false}, {:tz, "~> 0.26", only: [:dev, :test]} ] end From 7d4a869d8e92b4f98f2e3b505057df265d7d119f Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 00:16:36 +0800 Subject: [PATCH 08/15] Reinstate date type in DateChecker --- lib/crontab/date_checker.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/crontab/date_checker.ex b/lib/crontab/date_checker.ex index ef0a4c2..8e85819 100644 --- a/lib/crontab/date_checker.ex +++ b/lib/crontab/date_checker.ex @@ -7,6 +7,8 @@ defmodule Crontab.DateChecker do alias Crontab.DateHelper + @type date :: NaiveDateTime.t() | DateTime.t() + @doc """ Check a condition list against a given date. From 3fbafe8e0aa9f4684006e51e52bde8e63bdcc933 Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 12:57:32 +0800 Subject: [PATCH 09/15] Replace all DateHelper.date with DateChecker.date in date_checker --- lib/crontab/date_checker.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/crontab/date_checker.ex b/lib/crontab/date_checker.ex index 8e85819..e87990c 100644 --- a/lib/crontab/date_checker.ex +++ b/lib/crontab/date_checker.ex @@ -27,7 +27,7 @@ defmodule Crontab.DateChecker do true """ - @spec matches_date?(cron_expression :: CronExpression.t(), date :: DateHelper.date()) :: + @spec matches_date?(cron_expression :: CronExpression.t(), date :: date) :: boolean | no_return def matches_date?(cron_expression_or_condition_list, date) @@ -42,7 +42,7 @@ defmodule Crontab.DateChecker do @spec matches_date?( condition_list :: CronExpression.condition_list(), - date :: DateHelper.date() + date :: date ) :: boolean def matches_date?([], _), do: true @@ -65,7 +65,7 @@ defmodule Crontab.DateChecker do @spec matches_date?( interval :: CronExpression.interval(), condition_list :: CronExpression.condition_list(), - date :: DateHelper.date() + date :: date ) :: boolean def matches_date?(_, [:* | _], _), do: true def matches_date?(_, [], _), do: false @@ -84,7 +84,7 @@ defmodule Crontab.DateChecker do interval :: CronExpression.interval(), values :: [CronExpression.time_unit()], condition :: CronExpression.value(), - date :: DateHelper.date() + date :: date ) :: boolean defp matches_specific_date?(_, [], _, _), do: false defp matches_specific_date?(_, _, :*, _), do: true @@ -175,7 +175,7 @@ defmodule Crontab.DateChecker do end end - @spec get_interval_value(interval :: CronExpression.interval(), date :: DateHelper.date()) :: [ + @spec get_interval_value(interval :: CronExpression.interval(), date :: date) :: [ CronExpression.time_unit() ] defp get_interval_value(:second, %{second: second}), do: [second] From 76552ed1808d0731be4c1b86169f4003e67a8c43 Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 13:28:07 +0800 Subject: [PATCH 10/15] Change to using if, instead of case, in date_helper.add/3 --- lib/crontab/date_helper.ex | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index b5f3a8c..96bdc79 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -303,15 +303,13 @@ defmodule Crontab.DateHelper do adjustment = datetime.std_offset - candidate.std_offset adjusted = DateTime.add(candidate, adjustment, :second) - case adjusted.std_offset == candidate.std_offset do - false -> + if adjusted.std_offset != candidate.std_offset do candidate - - _ -> - case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do - {:ambiguous, _, target} -> target - {:ok, target} -> target - end + else + case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do + {:ambiguous, _, target} -> target + {:ok, target} -> target + end end end end From 238aafcddf1ce8b6ffe3b624bb75098176bf676a Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 14:11:52 +0800 Subject: [PATCH 11/15] Fix inc_year/1 and dec_year/1 in date_helper --- lib/crontab/date_helper.ex | 35 ++++++++++++-------- test/crontab/date_helper_test.exs | 54 +++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index 96bdc79..f29fb9a 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -136,14 +136,22 @@ defmodule Crontab.DateHelper do ### Examples: iex> Crontab.DateHelper.inc_year(~N[2016-03-14 01:45:45.123]) - ~N[2017-03-15 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-15 01:45:45.123Z] + ~U[2017-03-14 01:45:45.123Z] """ @spec inc_year(date) :: date when date: date - def inc_year(date), do: add(date, days_in_year(date), :day) + 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 @doc """ Decrement Year @@ -158,7 +166,15 @@ defmodule Crontab.DateHelper do """ @spec dec_year(date) :: date when date: date - def dec_year(date), do: add(date, -days_in_year(date), :day) + 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 @doc """ Increment Month @@ -286,15 +302,6 @@ defmodule Crontab.DateHelper do end end - defp days_in_year(%{year: year}) do - Date.new!(year, 1, 1) - |> Date.leap_year?() - |> case do - true -> 366 - false -> 365 - end - end - @doc false def add(datetime = %NaiveDateTime{}, amt, unit), do: NaiveDateTime.add(datetime, amt, unit) @@ -304,7 +311,7 @@ defmodule Crontab.DateHelper do adjusted = DateTime.add(candidate, adjustment, :second) if adjusted.std_offset != candidate.std_offset do - candidate + candidate else case DateTime.from_naive(DateTime.to_naive(adjusted), adjusted.time_zone) do {:ambiguous, _, target} -> target diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index 93694ad..58eed24 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -12,6 +12,60 @@ defmodule Crontab.DateHelperTest do end end + describe "dec_year/1" do + test "non-leap year back to leap year at end feb" do + given = ~N[2025-02-28 00:00:00] + assert DateHelper.dec_year(given) == ~N[2024-02-28 00:00:00] + end + + test "leap year back to non-leap year at end feb" do + given = ~N[2024-02-29 00:00:00] + assert DateHelper.dec_year(given) == ~N[2023-02-28 00:00:00] + end + + test "non-leap year back to leap year at start mar" do + given = ~N[2025-03-01 00:00:00] + assert DateHelper.dec_year(given) == ~N[2024-03-01 00:00:00] + end + + test "leap year back to non-leap year at start mar" do + given = ~N[2024-03-01 00:00:00] + assert DateHelper.dec_year(given) == ~N[2023-03-01 00:00:00] + end + + test "non-leap year back to non-leap year" do + given = ~N[2026-03-01 00:00:00] + assert DateHelper.dec_year(given) == ~N[2025-03-01 00:00:00] + end + end + + describe "inc_year/1" do + test "non-leap year to leap year at end feb" do + given = ~N[2023-02-28 00:00:00] + assert DateHelper.inc_year(given) == ~N[2024-02-28 00:00:00] + end + + test "leap year to non-leap year at end feb" do + given = ~N[2024-02-29 00:00:00] + assert DateHelper.inc_year(given) == ~N[2025-02-28 00:00:00] + end + + test "non-leap year to leap year at start mar" do + given = ~N[2023-03-01 00:00:00] + assert DateHelper.inc_year(given) == ~N[2024-03-01 00:00:00] + end + + test "leap year to non-leap year at start mar" do + given = ~N[2024-03-01 00:00:00] + assert DateHelper.inc_year(given) == ~N[2025-03-01 00:00:00] + end + + test "non-leap year to non-leap year" do + given = ~N[2025-03-01 00:00:00] + assert DateHelper.inc_year(given) == ~N[2026-03-01 00:00:00] + end + end + describe "add/3 on NaiveDateTime" do test "one day to day before NY DST starts" do date = ~N[2024-03-09 12:34:56] From d669fa22beb6e9f251ae4c0c03adfe8d8769c66a Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 14:15:18 +0800 Subject: [PATCH 12/15] Move time zone database setup to config.exs --- config/config.exs | 3 +++ test/crontab/date_helper_test.exs | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 config/config.exs diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..8a05082 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,3 @@ +import Config + +config :elixir, :time_zone_database, Tz.TimeZoneDatabase diff --git a/test/crontab/date_helper_test.exs b/test/crontab/date_helper_test.exs index 58eed24..e91b4f2 100644 --- a/test/crontab/date_helper_test.exs +++ b/test/crontab/date_helper_test.exs @@ -123,9 +123,5 @@ defmodule Crontab.DateHelperTest do assert DateHelper.add(one_sec_before, 1, unquote(unit)) == expected end end - - setup do - Calendar.put_time_zone_database(Tz.TimeZoneDatabase) - end end end From f6b023157ce8c4eadc5204a45fad218ad816219c Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 14:19:31 +0800 Subject: [PATCH 13/15] Replace all :calendar.day_of_the_week with Date.day_of_week in date_helper --- lib/crontab/date_helper.ex | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index f29fb9a..295e385 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -116,8 +116,8 @@ defmodule Crontab.DateHelper do """ @spec next_weekday_to(date :: date) :: Calendar.day() - def next_weekday_to(date = %{year: year, month: month, day: day}) do - weekday = :calendar.day_of_the_week(year, month, 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) @@ -276,16 +276,14 @@ defmodule Crontab.DateHelper do boolean defp nth_weekday(date, _, 0, :start), do: add(date, -1, :day).day - defp nth_weekday(date = %{year: year, month: month, day: day}, weekday, n, :start) do - modifier = if :calendar.day_of_the_week(year, month, day) == weekday, do: n - 1, else: n + 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(date :: date(), position :: :end) :: Calendar.day() - defp last_weekday_of_month(date = %{year: year, month: month, day: day}, :end) do - weekday = :calendar.day_of_the_week(year, month, day) - - if weekday > 5 do + 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 @@ -294,8 +292,8 @@ defmodule Crontab.DateHelper do @spec last_weekday(date :: date, weekday :: Calendar.day_of_week(), position :: :end) :: Calendar.day() - defp last_weekday(date = %{year: year, month: month, day: day}, weekday, :end) do - if :calendar.day_of_the_week(year, month, day) == weekday do + defp last_weekday(date = %{day: day}, weekday, :end) do + if Date.day_of_week(date) == weekday do day else last_weekday(add(date, -1, :day), weekday, :end) From fed56e91d6fb6290c43c6c14bfa5d60ed3bac856 Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 14:33:15 +0800 Subject: [PATCH 14/15] Simplify DateHelper.dec_month/1 --- lib/crontab/date_helper.ex | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index 295e385..97b23d8 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -214,20 +214,12 @@ defmodule Crontab.DateHelper do """ @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, day) - |> day_in_last_month - |> Date.days_in_month() - + 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 - defp day_in_last_month(start_date), do: day_in_last_month(start_date, start_date) - - defp day_in_last_month(date = %Date{month: month}, start_date = %Date{month: month}), - do: date |> Date.add(-1) |> day_in_last_month(start_date) - - defp day_in_last_month(date, _start_date), do: date + # defp day_in_last_month(%{year: year, month: month}), + # do: Date.new!(year, month, 1) |> Date.add(-1) @spec _beginning_of(date, [{unit, {any, any}}]) :: date when date: date defp _beginning_of(date, [{unit, {lower, _}} | tail]) do From 733cc1ba75865a08dc284e343c79aa49a3abdc2f Mon Sep 17 00:00:00 2001 From: shaolang Date: Tue, 13 Feb 2024 14:51:57 +0800 Subject: [PATCH 15/15] Address lingering issues from code review --- lib/crontab/date_helper.ex | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/crontab/date_helper.ex b/lib/crontab/date_helper.ex index 97b23d8..2834b74 100644 --- a/lib/crontab/date_helper.ex +++ b/lib/crontab/date_helper.ex @@ -218,9 +218,6 @@ defmodule Crontab.DateHelper do add(date, -(day + max(days_in_last_month - day, 0)), :day) end - # defp day_in_last_month(%{year: year, month: month}), - # do: Date.new!(year, month, 1) |> Date.add(-1) - @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) @@ -229,11 +226,8 @@ defmodule Crontab.DateHelper do defp _beginning_of(date, []), do: date @spec _end_of(date, [{unit, {any, any}}]) :: date when date: date - defp _end_of(date = %{year: year, month: month, day: day}, [{unit, {_, :end_of_month}} | tail]) do - upper = - Date.new!(year, month, day) - |> Date.days_in_month() - + 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