From 54b94364094fd30808eccef8c2011d6f363e97c0 Mon Sep 17 00:00:00 2001 From: Brian Berlin Date: Sun, 24 Sep 2023 15:45:05 -0500 Subject: [PATCH] Removes dependency on Timex --- .tool-versions | 4 +- lib/cocktail/builder/i_calendar.ex | 20 +- lib/cocktail/parser/i_calendar.ex | 19 +- lib/cocktail/rule_state.ex | 2 +- lib/cocktail/schedule.ex | 6 +- lib/cocktail/schedule_state.ex | 19 +- lib/cocktail/span.ex | 30 +-- lib/cocktail/time.ex | 224 ++++++++++++++++++ lib/cocktail/time/diff.ex | 50 ++++ lib/cocktail/time/shift.ex | 76 ++++++ lib/cocktail/util.ex | 22 -- lib/cocktail/validation/day.ex | 5 +- lib/cocktail/validation/day_of_month.ex | 13 +- lib/cocktail/validation/hour_of_day.ex | 2 +- lib/cocktail/validation/interval.ex | 37 ++- lib/cocktail/validation/minute_of_hour.ex | 2 +- lib/cocktail/validation/schedule_lock.ex | 33 +-- lib/cocktail/validation/second_of_minute.ex | 2 +- lib/cocktail/validation/shift.ex | 8 +- lib/cocktail/validation/time_of_day.ex | 2 +- mix.exs | 2 +- mix.lock | 31 ++- test/cocktail/monthly_test.exs | 166 ++++++------- test/cocktail/parser/i_calendar_test.exs | 12 +- test/cocktail/time_test.exs | 5 + test/cocktail/validation/time_of_day_test.exs | 2 +- test/cocktail/validation/time_range_test.exs | 2 +- test/support/datetime_sigil.ex | 2 +- test/test_helper.exs | 2 + 29 files changed, 566 insertions(+), 234 deletions(-) create mode 100644 lib/cocktail/time.ex create mode 100644 lib/cocktail/time/diff.ex create mode 100644 lib/cocktail/time/shift.ex create mode 100644 test/cocktail/time_test.exs diff --git a/.tool-versions b/.tool-versions index 532f92b..626f7ed 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.14.5-otp-25 -erlang 25.3.2 +elixir 1.15.6-otp-26 +erlang 26.1 diff --git a/lib/cocktail/builder/i_calendar.ex b/lib/cocktail/builder/i_calendar.ex index 41189bf..ac93e91 100644 --- a/lib/cocktail/builder/i_calendar.ex +++ b/lib/cocktail/builder/i_calendar.ex @@ -5,12 +5,10 @@ defmodule Cocktail.Builder.ICalendar do TODO: write long description """ - import Cocktail.Util - alias Cocktail.{Rule, Schedule, Validation} alias Cocktail.Validation.{Day, DayOfMonth, HourOfDay, Interval, MinuteOfHour, SecondOfMinute, TimeOfDay, TimeRange} - @time_format_string "{YYYY}{0M}{0D}T{h24}{m}{s}" + @time_format_string "%Y%m%dT%H%M%S" @doc ~S""" Builds an iCalendar format string representation of a `t:Cocktail.Schedule.t/0`. @@ -18,7 +16,7 @@ defmodule Cocktail.Builder.ICalendar do ## Examples iex> alias Cocktail.Schedule - ...> start_time = Timex.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles") + ...> start_time = Cocktail.Time.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles") ...> schedule = Schedule.new(start_time) ...> schedule = Schedule.add_recurrence_rule(schedule, :daily, interval: 2, hours: [10, 12]) ...> build(schedule) @@ -64,7 +62,7 @@ defmodule Cocktail.Builder.ICalendar do ## Examples iex> alias Cocktail.Schedule - ...> start_time = Timex.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles") + ...> start_time = Cocktail.Time.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles") ...> schedule = Schedule.new(start_time) ...> schedule = Schedule.add_recurrence_rule(schedule, :daily, interval: 2, hours: [10, 12]) ...> build_rule(schedule) @@ -91,12 +89,12 @@ defmodule Cocktail.Builder.ICalendar do @spec build_time(Cocktail.time(), String.t()) :: String.t() defp build_time(%DateTime{} = time, prefix) do timezone = time.time_zone - time_string = Timex.format!(time, @time_format_string) + time_string = Calendar.strftime(time, @time_format_string) "#{prefix};TZID=#{timezone}:#{time_string}" end defp build_time(%NaiveDateTime{} = time, prefix) do - time_string = Timex.format!(time, @time_format_string) + time_string = Calendar.strftime(time, @time_format_string) "#{prefix}:#{time_string}" end @@ -108,17 +106,17 @@ defmodule Cocktail.Builder.ICalendar do defp build_end_time(%Schedule{start_time: start_time, duration: duration}) do start_time - |> shift_time(seconds: duration) + |> Cocktail.Time.shift(duration, :second) |> build_time("DTEND") end @spec build_utc_time(Cocktail.time()) :: String.t() - defp build_utc_time(%NaiveDateTime{} = time), do: Timex.format!(time, @time_format_string) + defp build_utc_time(%NaiveDateTime{} = time), do: Calendar.strftime(time, @time_format_string) defp build_utc_time(%DateTime{} = time) do time - |> Timex.to_datetime("UTC") - |> Timex.format!(@time_format_string <> "Z") + |> Cocktail.Time.to_datetime("UTC") + |> Calendar.strftime(@time_format_string <> "Z") end @spec do_build_rule(Rule.t()) :: String.t() diff --git a/lib/cocktail/parser/i_calendar.ex b/lib/cocktail/parser/i_calendar.ex index 4872fea..a7a5daa 100644 --- a/lib/cocktail/parser/i_calendar.ex +++ b/lib/cocktail/parser/i_calendar.ex @@ -8,8 +8,8 @@ defmodule Cocktail.Parser.ICalendar do alias Cocktail.{Rule, Schedule} @time_regex ~r/^:?;?(?:TZID=(.+?):)?(.*?)(Z)?$/ - @datetime_format "{YYYY}{0M}{0D}T{h24}{m}{s}" - @time_format "{h24}{m}{s}" + @datetime_format "%Y%m%dT%H%M%S" + @time_format "%H%M%S" @doc ~S""" Parses a string in iCalendar format into a `t:Cocktail.Schedule.t/0`. @@ -38,7 +38,7 @@ defmodule Cocktail.Parser.ICalendar do |> String.trim() |> String.split("\n") |> Enum.map(&String.trim/1) - |> parse_lines(Schedule.new(Timex.now()), 0) + |> parse_lines(Schedule.new(DateTime.utc_now()), 0) end @spec parse_lines([String.t()], Schedule.t(), non_neg_integer) :: {:ok, Schedule.t()} | {:error, term} @@ -105,15 +105,17 @@ defmodule Cocktail.Parser.ICalendar do end @spec parse_naive_datetime(String.t()) :: {:ok, NaiveDateTime.t()} | {:error, term} - defp parse_naive_datetime(time_string), do: Timex.parse(time_string, @datetime_format) + defp parse_naive_datetime(time_string) do + Cocktail.Time.parse(time_string, @datetime_format) + end @spec parse_utc_datetime(String.t()) :: {:ok, DateTime.t()} | {:error, term} - defp parse_utc_datetime(time_string), do: parse_zoned_datetime(time_string, "UTC") + defp parse_utc_datetime(time_string), do: parse_zoned_datetime(time_string, "Etc/UTC") @spec parse_zoned_datetime(String.t(), String.t()) :: {:ok, DateTime.t()} | {:error, term} defp parse_zoned_datetime(time_string, zone) do - with {:ok, naive_datetime} <- Timex.parse(time_string, @datetime_format), - %DateTime{} = datetime <- Timex.to_datetime(naive_datetime, zone) do + with {:ok, naive_datetime} <- Cocktail.Time.parse(time_string, @datetime_format), + %DateTime{} = datetime <- Cocktail.Time.to_datetime(naive_datetime, zone) do {:ok, datetime} end end @@ -421,7 +423,8 @@ defmodule Cocktail.Parser.ICalendar do @spec parse_time(String.t()) :: {:ok, Time.t()} | {:error, :invalid_time_format} defp parse_time(time_string) do - case Timex.parse(time_string, @time_format) do + case Cocktail.Time.parse(time_string, @time_format) do + {:ok, %Time{} = time} -> {:ok, time} {:ok, datetime} -> {:ok, NaiveDateTime.to_time(datetime)} _error -> {:error, :invalid_time_format} end diff --git a/lib/cocktail/rule_state.ex b/lib/cocktail/rule_state.ex index a962024..f92b937 100644 --- a/lib/cocktail/rule_state.ex +++ b/lib/cocktail/rule_state.ex @@ -77,7 +77,7 @@ defmodule Cocktail.RuleState do defp new_state(%__MODULE__{until: nil} = rule_state, time), do: %{rule_state | current_time: time} defp new_state(%__MODULE__{until: until} = rule_state, time) do - if Timex.compare(until, time) == -1 do + if Cocktail.Time.compare(until, time) == :lt do %{rule_state | current_time: nil} else %{rule_state | current_time: time} diff --git a/lib/cocktail/schedule.ex b/lib/cocktail/schedule.ex index 8bcb0ab..4b8b28e 100644 --- a/lib/cocktail/schedule.ex +++ b/lib/cocktail/schedule.ex @@ -84,7 +84,7 @@ defmodule Cocktail.Schedule do @doc false @spec set_end_time(t, Cocktail.time()) :: t def set_end_time(%__MODULE__{start_time: start_time} = schedule, end_time) do - duration = Timex.diff(end_time, start_time, :seconds) + duration = Cocktail.Time.diff(end_time, start_time, :second) %{schedule | duration: duration} end @@ -180,9 +180,9 @@ defmodule Cocktail.Schedule do ~N[2017-10-04 10:00:00]] # using a DateTime with a time zone - iex> start_time = Timex.to_datetime(~N[2017-01-02 10:00:00], "America/Los_Angeles") + iex> start_time = Cocktail.Time.to_datetime(~N[2017-01-02 10:00:00], "America/Los_Angeles") ...> schedule = start_time |> new() |> add_recurrence_rule(:daily) - ...> schedule |> occurrences() |> Enum.take(3) |> Enum.map(&Timex.format!(&1, "{ISO:Extended}")) + ...> schedule |> occurrences() |> Enum.take(3) |> Enum.map(&DateTime.to_iso8601/1) ["2017-01-02T10:00:00-08:00", "2017-01-03T10:00:00-08:00", "2017-01-04T10:00:00-08:00"] diff --git a/lib/cocktail/schedule_state.ex b/lib/cocktail/schedule_state.ex index 492371a..de8ac11 100644 --- a/lib/cocktail/schedule_state.ex +++ b/lib/cocktail/schedule_state.ex @@ -2,7 +2,6 @@ defmodule Cocktail.ScheduleState do @moduledoc false alias Cocktail.{RuleState, Schedule, Span} - import Cocktail.Util @type t :: %__MODULE__{ recurrence_rules: [RuleState.t()], @@ -26,19 +25,19 @@ defmodule Cocktail.ScheduleState do def new(%Schedule{} = schedule, current_time) do current_time = - if Timex.compare(current_time, schedule.start_time) < 0, + if Cocktail.Time.compare(current_time, schedule.start_time) == :lt, do: schedule.start_time, else: current_time recurrence_times_after_current_time = schedule.recurrence_times - |> Enum.filter(&(Timex.compare(&1, current_time) >= 0)) - |> Enum.sort(&(Timex.compare(&1, &2) <= 0)) + |> Enum.filter(&(Cocktail.Time.compare(&1, current_time) in [:gt, :eq])) + |> Enum.sort(&(Cocktail.Time.compare(&1, &2) in [:lt, :eq])) %__MODULE__{ recurrence_rules: schedule.recurrence_rules |> Enum.map(&RuleState.new/1), recurrence_times: recurrence_times_after_current_time, - exception_times: schedule.exception_times |> Enum.sort(&(Timex.compare(&1, &2) <= 0)), + exception_times: schedule.exception_times |> Enum.sort(&(Cocktail.Time.compare(&1, &2) in [:lt, :eq])), start_time: schedule.start_time, current_time: current_time, duration: schedule.duration @@ -84,7 +83,7 @@ defmodule Cocktail.ScheduleState do defp next_time_from_recurrence_times([next_time | rest], nil), do: {next_time, rest} defp next_time_from_recurrence_times([next_time | rest] = times, current_time) do - if Timex.compare(next_time, current_time) <= 0 do + if Cocktail.Time.compare(next_time, current_time) in [:lt, :eq] do {next_time, rest} else {current_time, times} @@ -96,7 +95,7 @@ defmodule Cocktail.ScheduleState do defp apply_exception_time(exceptions, nil), do: {false, exceptions} defp apply_exception_time([next_exception | rest] = exceptions, current_time) do - if Timex.compare(next_exception, current_time) == 0 do + if Cocktail.Time.compare(next_exception, current_time) == :eq do {true, rest} else {false, exceptions} @@ -115,7 +114,7 @@ defmodule Cocktail.ScheduleState do | recurrence_rules: rules, recurrence_times: times, exception_times: exceptions, - current_time: shift_time(time, seconds: 1) + current_time: Cocktail.Time.shift(time, 1, :second) } {occurrence, new_state} @@ -123,7 +122,7 @@ defmodule Cocktail.ScheduleState do @spec span_or_time(Cocktail.time() | nil, pos_integer | nil) :: Cocktail.occurrence() defp span_or_time(time, nil), do: time - defp span_or_time(time, duration), do: Span.new(time, shift_time(time, seconds: duration)) + defp span_or_time(time, duration), do: Span.new(time, Cocktail.Time.shift(time, duration, :second)) @spec min_time_for_rules([RuleState.t()]) :: Cocktail.time() | nil defp min_time_for_rules([]), do: nil @@ -131,7 +130,7 @@ defmodule Cocktail.ScheduleState do defp min_time_for_rules(rules) do rules - |> Enum.min_by(&Timex.to_erl(&1.current_time)) + |> Enum.min_by(&Cocktail.Time.to_erl(&1.current_time)) |> Map.get(:current_time) end diff --git a/lib/cocktail/span.ex b/lib/cocktail/span.ex index 1735f8e..3b6e137 100644 --- a/lib/cocktail/span.ex +++ b/lib/cocktail/span.ex @@ -39,7 +39,7 @@ defmodule Cocktail.Span do def new(from, until), do: %__MODULE__{from: from, until: until} @doc """ - Uses `Timex.compare/2` to determine which span comes first. + Uses `Cocktail.Time.compare/2` to determine which span comes first. Compares `from` first, then, if equal, compares `until`. @@ -48,21 +48,21 @@ defmodule Cocktail.Span do iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00]) ...> span2 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00]) ...> compare(span1, span2) - 0 + :eq iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00]) ...> span2 = new(~N[2017-01-01 07:00:00], ~N[2017-01-01 12:00:00]) ...> compare(span1, span2) - -1 + :lt iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00]) ...> span2 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 07:00:00]) ...> compare(span1, span2) - 1 + :gt """ - @spec compare(span_compat, span_compat) :: Timex.Comparable.compare_result() - def compare(%{from: t, until: until1}, %{from: t, until: until2}), do: Timex.compare(until1, until2) - def compare(%{from: from1}, %{from: from2}), do: Timex.compare(from1, from2) + @spec compare(span_compat, span_compat) :: :lt | :eq | :gt + def compare(%{from: t, until: until1}, %{from: t, until: until2}), do: Cocktail.Time.compare(until1, until2) + def compare(%{from: from1}, %{from: from2}), do: Cocktail.Time.compare(from1, from2) @doc """ Returns an `t:overlap_mode/0` to describe how the first span overlaps the second. @@ -94,16 +94,16 @@ defmodule Cocktail.Span do # credo:disable-for-next-line def overlap_mode(%{from: from1, until: until1}, %{from: from2, until: until2}) do - from_comp = Timex.compare(from1, from2) - until_comp = Timex.compare(until1, until2) + from_comp = Cocktail.Time.compare(from1, from2) + until_comp = Cocktail.Time.compare(until1, until2) cond do - from_comp <= 0 && until_comp >= 0 -> :contains - from_comp >= 0 && until_comp <= 0 -> :is_inside - Timex.compare(until1, from2) <= 0 -> :is_before - Timex.compare(from1, until2) >= 0 -> :is_after - from_comp < 0 && until_comp < 0 -> :overlaps_the_start_of - from_comp > 0 && until_comp > 0 -> :overlaps_the_end_of + from_comp in [:lt, :eq] && until_comp in [:gt, :eq] -> :contains + from_comp in [:gt, :eq] && until_comp in [:lt, :eq] -> :is_inside + Cocktail.Time.compare(until1, from2) in [:lt, :eq] -> :is_before + Cocktail.Time.compare(from1, until2) in [:gt, :eq] -> :is_after + from_comp == :lt && until_comp == :lt -> :overlaps_the_start_of + from_comp == :gt && until_comp == :gt -> :overlaps_the_end_of end end end diff --git a/lib/cocktail/time.ex b/lib/cocktail/time.ex new file mode 100644 index 0000000..c8f2502 --- /dev/null +++ b/lib/cocktail/time.ex @@ -0,0 +1,224 @@ +defmodule Cocktail.Time do + @moduledoc """ + Struct used to represent a time. + """ + @type interval :: :month | :day | :hour | :minute | System.time_unit() + + @doc """ + Shifts the given time by the given amount. + + ## Examples + + iex> shift(~N[2023-09-24 09:30:30], 1, :month) + ~N[2023-10-24 09:30:30] + iex> shift(~U[2023-09-24 09:30:30Z], 1, :month) + ~U[2023-10-24 09:30:30Z] + iex> shift(~N[2023-09-24 09:30:30], 1, :day) + ~N[2023-09-25 09:30:30] + iex> shift(~U[2023-09-24 09:30:30Z], 1, :hour) + ~U[2023-09-24 10:30:30Z] + """ + @spec shift(time :: Cocktail.time(), amount :: integer(), interval :: interval()) :: Cocktail.time() + defdelegate shift(time, amount, interval), to: Cocktail.Time.Shift + + @doc """ + Returns the difference between two times in the given interval. + + ## Examples + + iex> diff(~N[2023-09-24 09:30:31], ~N[2023-09-24 09:30:30], :second) + 1 + iex> diff(~N[2023-09-24 09:32:30], ~N[2023-09-24 09:30:30], :minute) + 2 + iex> diff(~N[2023-09-24 12:30:30], ~N[2023-09-24 09:30:30], :hour) + 3 + iex> diff(~N[2023-09-28 09:30:30], ~N[2023-09-24 09:30:30], :day) + 4 + iex> diff(~N[2024-02-24 09:30:30], ~N[2023-09-24 09:30:30], :month) + 5 + """ + @spec diff( + a :: Cocktail.time() | Date.t() | Time.t(), + b :: Cocktail.time() | Date.t() | Time.t(), + interval :: interval() + ) :: integer() + defdelegate diff(a, b, interval), to: Cocktail.Time.Diff + + @doc """ + Returns the beginning of the day for the given time. + + ## Examples + + iex> beginning_of_day(~N[2023-09-24 09:00:00]) + ~N[2023-09-24 00:00:00] + iex> beginning_of_day(~U[2023-09-24 09:00:00Z]) + ~U[2023-09-24 00:00:00Z] + """ + @spec beginning_of_day(Cocktail.time()) :: Cocktail.time() + def beginning_of_day(%NaiveDateTime{} = datetime) do + %{year: year, month: month, day: day, microsecond: {_, precision}} = datetime + {:ok, datetime} = NaiveDateTime.new(year, month, day, 0, 0, 0, {0, precision}) + datetime + end + + def beginning_of_day(%DateTime{} = datetime) do + {_, precision} = datetime.microsecond + {:ok, time} = Time.new(0, 0, 0, {0, precision}) + date = DateTime.to_date(datetime) + {:ok, datetime} = DateTime.new(date, time, datetime.time_zone, Tzdata.TimeZoneDatabase) + datetime + end + + @doc """ + Returns the beginning of the month for the given time. + + ## Examples + + iex> beginning_of_month(~N[2023-09-24 09:30:30]) + ~N[2023-09-01 00:00:00] + iex> beginning_of_month(~N[2023-09-24 09:30:30.500]) + ~N[2023-09-01 00:00:00.000] + iex> beginning_of_month(~N[2023-09-24 09:30:30.500000]) + ~N[2023-09-01 00:00:00.000000] + iex> beginning_of_month(~U[2023-09-24 09:30:30Z]) + ~U[2023-09-01 00:00:00Z] + iex> beginning_of_month(~U[2023-09-24 09:30:30.500Z]) + ~U[2023-09-01 00:00:00.000Z] + iex> beginning_of_month(~U[2023-09-24 09:30:30.500000Z]) + ~U[2023-09-01 00:00:00.000000Z] + """ + @spec beginning_of_month(Cocktail.time()) :: Cocktail.time() + def beginning_of_month(%NaiveDateTime{} = datetime) do + %{year: year, month: month, microsecond: {_, precision}} = datetime + {:ok, datetime} = NaiveDateTime.new(year, month, 1, 0, 0, 0, {0, precision}) + datetime + end + + def beginning_of_month(%DateTime{} = datetime) do + %{year: year, month: month, time_zone: time_zone, microsecond: {_, precision}} = datetime + {:ok, time} = Time.new(0, 0, 0, {0, precision}) + {:ok, date} = Date.new(year, month, 1) + {:ok, datetime} = DateTime.new(date, time, time_zone, Tzdata.TimeZoneDatabase) + datetime + end + + @doc """ + Compares two `DateTime` or `NaiveDateTime` structs. + + ## Examples + + iex> compare(~N[2023-09-24 09:30:30], ~U[2023-09-24 09:30:31Z]) + :lt + iex> compare(~N[2023-09-24 09:30:30], ~N[2023-09-24 09:30:29]) + :gt + iex> compare(~U[2023-09-24 09:30:30Z], ~U[2023-09-24 09:30:30Z]) + :eq + """ + @spec compare(Cocktail.time(), Cocktail.time()) :: :lt | :eq | :gt + def compare(%NaiveDateTime{} = a, b) do + NaiveDateTime.compare(a, b) + end + + def compare(%DateTime{} = a, %DateTime{} = b) do + DateTime.compare(a, b) + end + + @doc """ + Convert a date/time value and timezone name to a DateTime struct. + + ## Examples + + iex> to_datetime(~N[2023-09-24 09:30:30], "America/New_York") + #DateTime<2023-09-24 09:30:30-04:00 EDT America/New_York> + iex> to_datetime(~U[2023-09-24 09:30:30Z], "America/New_York") + #DateTime<2023-09-24 05:30:30-04:00 EDT America/New_York> + """ + @spec to_datetime(Cocktail.time(), String.t()) :: DateTime.t() | {:error, {:invalid_timezone, String.t()}} + def to_datetime(%NaiveDateTime{} = datetime, zone) do + if Tzdata.zone_exists?(zone) do + case DateTime.from_naive(datetime, zone, Tzdata.TimeZoneDatabase) do + {:ok, datetime} -> datetime + {:error, :time_zone_not_found} -> {:error, {:invalid_timezone, zone}} + end + else + {:error, {:invalid_timezone, zone}} + end + end + + def to_datetime(%DateTime{} = datetime, zone) do + case DateTime.shift_zone(datetime, zone, Tzdata.TimeZoneDatabase) do + {:ok, datetime} -> datetime + {:error, :time_zone_not_found} -> {:error, {:invalid_timezone, zone}} + end + end + + @doc """ + Converts a `DateTime`, `NaiveDateTime`, `Date` or `Time` struct to an Erlang datetime tuple. + + ## Examples + + iex> to_erl(~N[2023-09-24 09:30:30]) + {{2023, 9, 24}, {9, 30, 30}} + iex> to_erl(~U[2023-09-24 09:30:30Z]) + {{2023, 9, 24}, {9, 30, 30}} + iex> to_erl(~D[2000-01-01]) + {2000, 1, 1} + iex> to_erl(~T[09:30:30]) + {9, 30, 30} + """ + @spec to_erl(Time.t() | Date.t() | DateTime.t() | NaiveDateTime.t()) :: :calendar.time() | :calendar.datetime() + def to_erl(%Time{} = time), do: Time.to_erl(time) + def to_erl(%Date{} = time), do: Date.to_erl(time) + def to_erl(%DateTime{} = time), do: time |> DateTime.to_naive() |> to_erl() + def to_erl(%NaiveDateTime{} = time), do: NaiveDateTime.to_erl(time) + + @doc """ + Converts a `Calendar.time/0` to a `Date` struct. + + ## Examples + + iex> to_date(~N[2020-01-01 09:30:30]) + ~D[2020-01-01] + iex> to_date(~U[2020-01-01 09:30:30Z]) + ~D[2020-01-01] + """ + @spec to_date(Cocktail.time()) :: Date.t() + def to_date(%DateTime{} = datetime) do + DateTime.to_date(datetime) + end + + def to_date(%NaiveDateTime{} = datetime) do + NaiveDateTime.to_date(datetime) + end + + @doc """ + Parses a string into a `DateTime` or `Time` struct. + + ## Examples + + iex> parse("20230924T093030", "%Y%m%dT%H%M%S") + {:ok, ~N[2023-09-24 09:30:30]} + iex> parse("093030", "%H%M%S") + {:ok, ~T[09:30:30]} + iex> parse("0930", "%H%M%S") + {:error, :invalid_format} + """ + @spec parse(String.t(), String.t()) :: {:ok, NaiveDateTime.t() | Time.t()} | {:error, atom()} + def parse( + <>, + "%Y%m%dT%H%M%S" + ) do + [year, month, day, hour, minute, second] + |> Enum.map(&String.to_integer/1) + |> then(&apply(NaiveDateTime, :new, &1)) + end + + def parse(<>, "%H%M%S") do + [hour, minute, second] + |> Enum.map(&String.to_integer/1) + |> then(&apply(Time, :new, &1)) + end + + def parse(_, _), do: {:error, :invalid_format} +end diff --git a/lib/cocktail/time/diff.ex b/lib/cocktail/time/diff.ex new file mode 100644 index 0000000..98afe2d --- /dev/null +++ b/lib/cocktail/time/diff.ex @@ -0,0 +1,50 @@ +defmodule Cocktail.Time.Diff do + @moduledoc false + def diff(a, b, :month) do + a = :calendar.datetime_to_gregorian_seconds({{a.year, a.month, a.day}, {0, 0, 0}}) * 1000 * 1000 + b = :calendar.datetime_to_gregorian_seconds({{b.year, b.month, b.day}, {0, 0, 0}}) * 1000 * 1000 + diff = diff_months(a, b) + diff + end + + def diff(%Date{} = a, %Date{} = b, :day) do + Date.diff(a, b) + end + + def diff(a, b, interval) do + case {a, b} do + {%Time{}, %Time{}} -> + Time.diff(a, b, interval) + + {%NaiveDateTime{}, %NaiveDateTime{}} -> + NaiveDateTime.diff(a, b, interval) + + {%DateTime{}, %DateTime{}} -> + DateTime.diff(a, b, interval) + end + end + + defp diff_months(a, a), do: 0 + + defp diff_months(a, b) do + {start_date, _} = :calendar.gregorian_seconds_to_datetime(div(a, 1_000 * 1_000)) + {end_date, _} = :calendar.gregorian_seconds_to_datetime(div(b, 1_000 * 1_000)) + do_diff_months(start_date, end_date) + end + + defp do_diff_months({y1, m1, d1}, {y2, m2, d2}) do + months = (y1 - y2) * 12 + m1 - m2 + days_in_month2 = :calendar.last_day_of_the_month(y2, m2) + + cond do + months < 0 && d2 < d1 && (days_in_month2 >= d1 || days_in_month2 != d2) -> + months + 1 + + months > 0 && d2 > d1 -> + months - 1 + + true -> + months + end + end +end diff --git a/lib/cocktail/time/shift.ex b/lib/cocktail/time/shift.ex new file mode 100644 index 0000000..dabc6f0 --- /dev/null +++ b/lib/cocktail/time/shift.ex @@ -0,0 +1,76 @@ +defmodule Cocktail.Time.Shift do + @moduledoc false + def shift(datetime, amount, :month) do + shift_by_month(datetime, amount) + end + + def shift(%NaiveDateTime{} = datetime, amount, interval) do + NaiveDateTime.add(datetime, amount, interval) + end + + def shift(%DateTime{} = datetime, amount, interval) do + DateTime.add(datetime, amount, interval, Tzdata.TimeZoneDatabase) + end + + defp shift_by_month(%{year: year, month: month, day: day} = datetime, value) + when value > 0 do + add_years = div(value, 12) + add_months = rem(value, 12) + + {year, month} = + if month + add_months <= 12 do + {year + add_years, month + add_months} + else + total_months = month + add_months + {year + add_years + 1, total_months - 12} + end + + ldom = :calendar.last_day_of_the_month(year, month) + + cond do + day > ldom -> + datetime + |> Map.put(:year, year) + |> Map.put(:month, month) + |> Map.put(:day, ldom) + + :else -> + datetime + |> Map.put(:year, year) + |> Map.put(:month, month) + end + end + + # Negative shifts + defp shift_by_month(%{year: year, month: month, day: day} = datetime, value) do + add_years = div(value, 12) + add_months = rem(value, 12) + + {year, month} = + if month + add_months < 1 do + total_months = month + add_months + {year + (add_years - 1), 12 + total_months} + else + {year + add_years, month + add_months} + end + + if year < 0 do + {:error, :shift_to_invalid_date} + else + ldom = :calendar.last_day_of_the_month(year, month) + + cond do + day > ldom -> + datetime + |> Map.put(:year, year) + |> Map.put(:month, month) + |> Map.put(:day, ldom) + + :else -> + datetime + |> Map.put(:year, year) + |> Map.put(:month, month) + end + end + end +end diff --git a/lib/cocktail/util.ex b/lib/cocktail/util.ex index 21a85f5..637e86b 100644 --- a/lib/cocktail/util.ex +++ b/lib/cocktail/util.ex @@ -3,26 +3,4 @@ defmodule Cocktail.Util do def next_gte([], _), do: nil def next_gte([x | rest], search), do: if(x >= search, do: x, else: next_gte(rest, search)) - - def beginning_of_day(time) do - time - |> Timex.beginning_of_day() - |> no_ms() - end - - def beginning_of_month(time) do - time - |> Timex.beginning_of_month() - |> no_ms() - end - - def shift_time(datetime, opts) do - datetime - |> Timex.shift(opts) - |> no_ms() - end - - def no_ms(time) do - Map.put(time, :microsecond, {0, 0}) - end end diff --git a/lib/cocktail/validation/day.ex b/lib/cocktail/validation/day.ex index 8808d36..81dc641 100644 --- a/lib/cocktail/validation/day.ex +++ b/lib/cocktail/validation/day.ex @@ -15,11 +15,12 @@ defmodule Cocktail.Validation.Day do @spec next_time(t, Cocktail.time(), Cocktail.time()) :: Cocktail.Validation.Shift.result() def next_time(%__MODULE__{days: days}, time, _) do - current_day = Timex.weekday(time) + {current_day, _, _} = Calendar.ISO.day_of_week(time.year, time.month, time.day, :sunday) + current_day = current_day - 1 day = next_gte(days, current_day) || hd(days) diff = (day - current_day) |> mod(7) - shift_by(diff, :days, time, :beginning_of_day) + shift_by(diff, :day, time, :beginning_of_day) end @spec day_number(Cocktail.day()) :: Cocktail.day_number() diff --git a/lib/cocktail/validation/day_of_month.ex b/lib/cocktail/validation/day_of_month.ex index 8c2d78a..aff5472 100644 --- a/lib/cocktail/validation/day_of_month.ex +++ b/lib/cocktail/validation/day_of_month.ex @@ -28,21 +28,22 @@ defmodule Cocktail.Validation.DayOfMonth do case next_gte(normalized_days, current_day_of_month) do # go to next month nil -> - next_month_time = shift_time(time, months: 1) + next_month_time = Cocktail.Time.shift(time, 1, :month) next_month_normalized_days = Enum.map(days, &normalize_day_of_month(&1, next_month_time)) - next_month_earliest_day = Timex.set(next_month_time, day: hd(Enum.sort(next_month_normalized_days))) + next_month_earliest_day = Map.put(next_month_time, :day, hd(Enum.sort(next_month_normalized_days))) dst_accounted_days_diff(next_month_earliest_day, time) next_earliest_day_of_month -> next_earliest_day_of_month - current_day_of_month end - shift_by(diff, :days, time, :beginning_of_day) + shift_by(diff, :day, time, :beginning_of_day) end defp normalize_day_of_month(day_of_month, current_time) do - do_normalize_day_of_month(day_of_month, Timex.days_in_month(current_time)) + ldom = :calendar.last_day_of_the_month(current_time.year, current_time.month) + do_normalize_day_of_month(day_of_month, ldom) end defp do_normalize_day_of_month(day_of_month, days_in_month) when day_of_month > days_in_month do @@ -62,10 +63,10 @@ defmodule Cocktail.Validation.DayOfMonth do end defp dst_accounted_days_diff(next_month_earliest_day, time) do - case Timex.diff(next_month_earliest_day, time, :days) do + case Cocktail.Time.diff(next_month_earliest_day, time, :day) do 0 -> # get the hours diff to ensure we are not falling short because of DST - if Timex.diff(next_month_earliest_day, time, :hours) > @min_dst_resultant_hours do + if Cocktail.Time.diff(next_month_earliest_day, time, :hour) > @min_dst_resultant_hours do 1 else 0 diff --git a/lib/cocktail/validation/hour_of_day.ex b/lib/cocktail/validation/hour_of_day.ex index 5de7a40..c83ade9 100644 --- a/lib/cocktail/validation/hour_of_day.ex +++ b/lib/cocktail/validation/hour_of_day.ex @@ -19,6 +19,6 @@ defmodule Cocktail.Validation.HourOfDay do hour = next_gte(hours, current_hour) || hd(hours) diff = (hour - current_hour) |> mod(24) - shift_by(diff, :hours, time, :beginning_of_hour) + shift_by(diff, :hour, time, :beginning_of_hour) end end diff --git a/lib/cocktail/validation/interval.ex b/lib/cocktail/validation/interval.ex index 4f5ca43..ad3f368 100644 --- a/lib/cocktail/validation/interval.ex +++ b/lib/cocktail/validation/interval.ex @@ -3,9 +3,8 @@ defmodule Cocktail.Validation.Interval do import Integer, only: [mod: 2, floor_div: 2] import Cocktail.Validation.Shift - import Cocktail.Util - @typep iso_week :: {Timex.Types.year(), Timex.Types.weeknum()} + @typep iso_week :: {year :: integer(), weeknum :: non_neg_integer()} @type t :: %__MODULE__{type: Cocktail.frequency(), interval: pos_integer} @@ -21,35 +20,35 @@ defmodule Cocktail.Validation.Interval do def next_time(%__MODULE__{type: :monthly, interval: interval}, time, start_time) do start_time - |> beginning_of_month() - |> Timex.diff(beginning_of_month(time), :months) + |> Cocktail.Time.beginning_of_month() + |> Cocktail.Time.diff(Cocktail.Time.beginning_of_month(time), :month) |> mod(interval) - |> shift_by(:months, time) + |> shift_by(:month, time) end def next_time(%__MODULE__{type: :weekly, interval: interval}, time, start_time) do - week = Timex.iso_week(time) - start_week = Timex.iso_week(start_time) + week = :calendar.iso_week_number({time.year, time.month, time.day}) + start_week = :calendar.iso_week_number({start_time.year, start_time.month, start_time.day}) diff = weeks_diff(start_week, week) off_by = mod(diff * -1, interval) - shift_by(off_by * 7, :days, time) + shift_by(off_by * 7, :day, time) end def next_time(%__MODULE__{type: :daily, interval: interval}, time, start_time) do - date = Timex.to_date(time) - start_date = Timex.to_date(start_time) + date = Cocktail.Time.to_date(time) + start_date = Cocktail.Time.to_date(start_time) start_date - |> Timex.diff(date, :days) + |> Cocktail.Time.diff(date, :day) |> mod(interval) - |> shift_by(:days, time) + |> shift_by(:day, time) end def next_time(%__MODULE__{type: type, interval: interval}, time, start_time) do unit = unit_for_type(type) start_time - |> Timex.diff(time, unit) + |> Cocktail.Time.diff(time, unit) |> mod(interval) |> shift_by(unit, time) end @@ -60,7 +59,7 @@ defmodule Cocktail.Validation.Interval do defp weeks_diff({year1, week1}, {year2, week2}) when year2 > year1, do: (year1..(year2 - 1) |> Enum.map(&iso_weeks_per_year/1) |> Enum.sum()) - week1 + week2 - @spec iso_weeks_per_year(Timex.Types.year()) :: 52 | 53 + @spec iso_weeks_per_year(year :: integer()) :: 52 | 53 defp iso_weeks_per_year(year) do if year_cycle(year) == 4 || year_cycle(year - 1) == 3 do 53 @@ -69,15 +68,15 @@ defmodule Cocktail.Validation.Interval do end end - @spec year_cycle(Timex.Types.year()) :: integer + @spec year_cycle(year :: integer()) :: integer() defp year_cycle(year) do cycle = year + floor_div(year, 4) - floor_div(year, 100) + floor_div(year, 400) mod(cycle, 7) end - @spec unit_for_type(:hourly | :minutely | :secondly) :: :hours | :minutes | :seconds - defp unit_for_type(:hourly), do: :hours - defp unit_for_type(:minutely), do: :minutes - defp unit_for_type(:secondly), do: :seconds + @spec unit_for_type(:hourly | :minutely | :secondly) :: :hour | :minute | :second + defp unit_for_type(:hourly), do: :hour + defp unit_for_type(:minutely), do: :minute + defp unit_for_type(:secondly), do: :second end diff --git a/lib/cocktail/validation/minute_of_hour.ex b/lib/cocktail/validation/minute_of_hour.ex index f1192b2..513e525 100644 --- a/lib/cocktail/validation/minute_of_hour.ex +++ b/lib/cocktail/validation/minute_of_hour.ex @@ -19,6 +19,6 @@ defmodule Cocktail.Validation.MinuteOfHour do minute = next_gte(minutes, current_minute) || hd(minutes) diff = (minute - current_minute) |> mod(60) - shift_by(diff, :minutes, time, :beginning_of_minute) + shift_by(diff, :minute, time, :beginning_of_minute) end end diff --git a/lib/cocktail/validation/schedule_lock.ex b/lib/cocktail/validation/schedule_lock.ex index 5769e63..f122d23 100644 --- a/lib/cocktail/validation/schedule_lock.ex +++ b/lib/cocktail/validation/schedule_lock.ex @@ -3,7 +3,6 @@ defmodule Cocktail.Validation.ScheduleLock do import Integer, only: [mod: 2] import Cocktail.Validation.Shift - import Cocktail.Util @type lock :: :second | :minute | :hour | :wday | :mday @@ -17,25 +16,28 @@ defmodule Cocktail.Validation.ScheduleLock do @spec next_time(t, Cocktail.time(), Cocktail.time()) :: Cocktail.Validation.Shift.result() def next_time(%__MODULE__{type: :second}, time, start_time), - do: shift_by(mod(start_time.second - time.second, 60), :seconds, time) + do: shift_by(mod(start_time.second - time.second, 60), :second, time) def next_time(%__MODULE__{type: :minute}, time, start_time), - do: shift_by(mod(start_time.minute - time.minute, 60), :minutes, time) + do: shift_by(mod(start_time.minute - time.minute, 60), :minute, time) def next_time(%__MODULE__{type: :hour}, time, start_time), - do: shift_by(mod(start_time.hour - time.hour, 24), :hours, time) + do: shift_by(mod(start_time.hour - time.hour, 24), :hour, time) def next_time(%__MODULE__{type: :wday}, time, start_time) do - start_time_day = Timex.weekday(start_time) - time_day = Timex.weekday(time) + {start_time_day, _, _} = Calendar.ISO.day_of_week(start_time.year, start_time.month, start_time.day, :sunday) + start_time_day = start_time_day - 1 + + {time_day, _, _} = Calendar.ISO.day_of_week(time.year, time.month, time.day, :sunday) + time_day = time_day - 1 diff = mod(start_time_day - time_day, 7) - shift_by(diff, :days, time) + shift_by(diff, :day, time) end def next_time(%__MODULE__{type: :mday}, time, start_time) do if start_time.day > Calendar.ISO.days_in_month(time.year, time.month) do - next_time(%__MODULE__{type: :mday}, shift_time(time, months: 1), start_time) + next_time(%__MODULE__{type: :mday}, Cocktail.Time.shift(time, 1, :month), start_time) else next_mday_time(%__MODULE__{type: :mday}, time, start_time) end @@ -53,17 +55,16 @@ defmodule Cocktail.Validation.ScheduleLock do # We to the same day of month of start_time in next month if the days of month are not equal start_time_day_of_month when start_time_day_of_month > time_day_of_month -> time - |> Timex.set(day: start_time_day_of_month) - |> Timex.diff(time, :days) + |> Map.put(:day, start_time_day_of_month) + |> Cocktail.Time.diff(time, :day) start_time_day_of_month -> - next_month_date = shift_time(time, months: 1) - # Timex.set already handle the marginal case like setting a day of month more than the month contains - next_month_date - |> Timex.set(day: start_time_day_of_month) - |> Timex.diff(time, :days) + time + |> Cocktail.Time.shift(1, :month) + |> Map.put(:day, start_time_day_of_month) + |> Cocktail.Time.diff(time, :day) end - shift_by(day_diff, :days, time) + shift_by(day_diff, :day, time) end end diff --git a/lib/cocktail/validation/second_of_minute.ex b/lib/cocktail/validation/second_of_minute.ex index e37d677..359bce9 100644 --- a/lib/cocktail/validation/second_of_minute.ex +++ b/lib/cocktail/validation/second_of_minute.ex @@ -19,6 +19,6 @@ defmodule Cocktail.Validation.SecondOfMinute do second = next_gte(seconds, current_second) || hd(seconds) diff = (second - current_second) |> mod(60) - shift_by(diff, :seconds, time) + shift_by(diff, :second, time) end end diff --git a/lib/cocktail/validation/shift.ex b/lib/cocktail/validation/shift.ex index 1c5c766..61fe62c 100644 --- a/lib/cocktail/validation/shift.ex +++ b/lib/cocktail/validation/shift.ex @@ -5,12 +5,10 @@ defmodule Cocktail.Validation.Shift do @type result :: {change_type, Cocktail.time()} - @typep shift_type :: :months | :days | :hours | :minutes | :seconds + @typep shift_type :: :month | :day | :hour | :minute | :second @typep option :: nil | :beginning_of_day | :beginning_of_hour | :beginning_of_minute - import Cocktail.Util - @spec shift_by(integer, shift_type, Cocktail.time(), option) :: result def shift_by(amount, type, time, option \\ nil) def shift_by(0, _, time, _), do: {:no_change, time} @@ -18,7 +16,7 @@ defmodule Cocktail.Validation.Shift do def shift_by(amount, type, time, option) do new_time = time - |> shift_time("#{type}": amount) + |> Cocktail.Time.shift(amount, type) |> apply_option(option) {:change, new_time} @@ -26,7 +24,7 @@ defmodule Cocktail.Validation.Shift do @spec apply_option(Cocktail.time(), option) :: Cocktail.time() defp apply_option(time, nil), do: time - defp apply_option(time, :beginning_of_day), do: time |> beginning_of_day() + defp apply_option(time, :beginning_of_day), do: time |> Cocktail.Time.beginning_of_day() defp apply_option(time, :beginning_of_hour), do: %{time | minute: 0, second: 0, microsecond: {0, 0}} defp apply_option(time, :beginning_of_minute), do: %{time | second: 0, microsecond: {0, 0}} end diff --git a/lib/cocktail/validation/time_of_day.ex b/lib/cocktail/validation/time_of_day.ex index 80bf98a..f2ee9d1 100644 --- a/lib/cocktail/validation/time_of_day.ex +++ b/lib/cocktail/validation/time_of_day.ex @@ -22,7 +22,7 @@ defmodule Cocktail.Validation.TimeOfDay do diff = Time.diff(target_time, current_time) diff = if diff < 0, do: diff + 86_400, else: diff - shift_by(diff, :seconds, time) + shift_by(diff, :second, time) end @spec to_time(Cocktail.time()) :: Time.t() diff --git a/mix.exs b/mix.exs index fd411d5..ea1adf1 100644 --- a/mix.exs +++ b/mix.exs @@ -81,7 +81,7 @@ defmodule Cocktail.Mixfile do {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.23", only: :dev, runtime: false}, {:excoveralls, "~> 0.10", only: :test}, - {:timex, "~> 3.6"} + {:tzdata, "~> 1.1"} ] end end diff --git a/mix.lock b/mix.lock index 9741a2d..2d608b4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,27 +1,24 @@ %{ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, - "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, - "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, - "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, + "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.36", "487ea8ef9bdc659f085e6e654f3c3feea1d36ac3943edf9d2ef6c98de9174c13", [:mix], [], "hexpm", "a524e395634bdcf60a616efe77fd79561bec2e930d8b82745df06ab4e844400a"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, - "excoveralls": {:hex, :excoveralls, "0.15.1", "83c8cf7973dd9d1d853dce37a2fb98aaf29b564bf7d01866e409abf59dac2c0e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f8416bd90c0082d56a2178cf46c837595a06575f70a5624f164a1ffe37de07e7"}, + "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, + "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "hackney": {:hex, :hackney, "1.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/cocktail/monthly_test.exs b/test/cocktail/monthly_test.exs index bb1771a..f0f7b00 100644 --- a/test/cocktail/monthly_test.exs +++ b/test/cocktail/monthly_test.exs @@ -24,14 +24,14 @@ defmodule Cocktail.MonthlyTest do test "Monthly" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-01-01 06:00:00 UTC], - ~Y[2017-02-01 06:00:00 UTC], - ~Y[2017-03-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC], + ~Y[2017-02-01 06:00:00 Etc/UTC], + ~Y[2017-03-01 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -61,14 +61,14 @@ defmodule Cocktail.MonthlyTest do test "Monthly starting on 31th March 2017" do schedule = - ~Y[2017-03-31 06:00:00 UTC] + ~Y[2017-03-31 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-03-31 06:00:00 UTC], - ~Y[2017-05-31 06:00:00 UTC], - ~Y[2017-07-31 06:00:00 UTC] + ~Y[2017-03-31 06:00:00 Etc/UTC], + ~Y[2017-05-31 06:00:00 Etc/UTC], + ~Y[2017-07-31 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -76,14 +76,14 @@ defmodule Cocktail.MonthlyTest do test "Monthly starting on 28th Feb 2017" do schedule = - ~Y[2017-02-28 06:00:00 UTC] + ~Y[2017-02-28 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-02-28 06:00:00 UTC], - ~Y[2017-03-28 06:00:00 UTC], - ~Y[2017-04-28 06:00:00 UTC] + ~Y[2017-02-28 06:00:00 Etc/UTC], + ~Y[2017-03-28 06:00:00 Etc/UTC], + ~Y[2017-04-28 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -91,14 +91,14 @@ defmodule Cocktail.MonthlyTest do test "every 12 month starting on 29th Feb 2020" do schedule = - ~Y[2020-02-29 06:00:00 UTC] + ~Y[2020-02-29 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, interval: 12) assert first_n_occurrences(schedule, 3) == [ - ~Y[2020-02-29 06:00:00 UTC], - ~Y[2024-02-29 06:00:00 UTC], - ~Y[2028-02-29 06:00:00 UTC] + ~Y[2020-02-29 06:00:00 Etc/UTC], + ~Y[2024-02-29 06:00:00 Etc/UTC], + ~Y[2028-02-29 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -106,25 +106,25 @@ defmodule Cocktail.MonthlyTest do test "Monthly: starting on 7th Nov 2019 and should return 7 th Nov 2039 after 20 years " do times = - ~Y[2019-11-07 06:00:00 UTC] + ~Y[2019-11-07 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly) |> Cocktail.Schedule.occurrences() |> Enum.at(20 * 12) - assert times == ~Y[2039-11-07 06:00:00 UTC] + assert times == ~Y[2039-11-07 06:00:00 Etc/UTC] end test "Every 2 months" do schedule = - ~Y[2017-02-28 06:00:00 UTC] + ~Y[2017-02-28 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, interval: 2) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-02-28 06:00:00 UTC], - ~Y[2017-04-28 06:00:00 UTC], - ~Y[2017-06-28 06:00:00 UTC] + ~Y[2017-02-28 06:00:00 Etc/UTC], + ~Y[2017-04-28 06:00:00 Etc/UTC], + ~Y[2017-06-28 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -132,19 +132,19 @@ defmodule Cocktail.MonthlyTest do test "Every 2 / 3 months" do schedule = - ~Y[2017-01-02 06:00:00 UTC] + ~U[2017-01-02 06:00:00Z] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, interval: 2) |> Schedule.add_recurrence_rule(:monthly, interval: 3) assert first_n_occurrences(schedule, 7) == [ - ~Y[2017-01-02 06:00:00 UTC], - ~Y[2017-03-02 06:00:00 UTC], - ~Y[2017-04-02 06:00:00 UTC], - ~Y[2017-05-02 06:00:00 UTC], - ~Y[2017-07-02 06:00:00 UTC], - ~Y[2017-09-02 06:00:00 UTC], - ~Y[2017-10-02 06:00:00 UTC] + ~U[2017-01-02 06:00:00Z], + ~U[2017-03-02 06:00:00Z], + ~U[2017-04-02 06:00:00Z], + ~U[2017-05-02 06:00:00Z], + ~U[2017-07-02 06:00:00Z], + ~U[2017-09-02 06:00:00Z], + ~U[2017-10-02 06:00:00Z] ] assert_icalendar_preserved(schedule) @@ -152,31 +152,31 @@ defmodule Cocktail.MonthlyTest do test "Monthly; overridden start month" do times = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly) - |> Cocktail.Schedule.occurrences(~Y[2017-05-01 06:00:00 UTC]) + |> Cocktail.Schedule.occurrences(~Y[2017-05-01 06:00:00 Etc/UTC]) |> Enum.take(3) assert times == [ - ~Y[2017-05-01 06:00:00 UTC], - ~Y[2017-06-01 06:00:00 UTC], - ~Y[2017-07-01 06:00:00 UTC] + ~Y[2017-05-01 06:00:00 Etc/UTC], + ~Y[2017-06-01 06:00:00 Etc/UTC], + ~Y[2017-07-01 06:00:00 Etc/UTC] ] end test "Monthly on Mondays and Fridays" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days: [:monday, :friday]) assert first_n_occurrences(schedule, 5) == [ - ~Y[2017-01-02 06:00:00 UTC], - ~Y[2017-01-06 06:00:00 UTC], - ~Y[2017-01-09 06:00:00 UTC], - ~Y[2017-01-13 06:00:00 UTC], - ~Y[2017-01-16 06:00:00 UTC] + ~Y[2017-01-02 06:00:00 Etc/UTC], + ~Y[2017-01-06 06:00:00 Etc/UTC], + ~Y[2017-01-09 06:00:00 Etc/UTC], + ~Y[2017-01-13 06:00:00 Etc/UTC], + ~Y[2017-01-16 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -184,19 +184,19 @@ defmodule Cocktail.MonthlyTest do test "Monthly on Mondays and Fridays and day of month" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days: [:monday, :friday], days_of_month: [1]) assert first_n_occurrences(schedule, 8) == [ - ~Y[2017-05-01 06:00:00 UTC], - ~Y[2017-09-01 06:00:00 UTC], - ~Y[2017-12-01 06:00:00 UTC], - ~Y[2018-01-01 06:00:00 UTC], - ~Y[2018-06-01 06:00:00 UTC], - ~Y[2018-10-01 06:00:00 UTC], - ~Y[2019-02-01 06:00:00 UTC], - ~Y[2019-03-01 06:00:00 UTC] + ~Y[2017-05-01 06:00:00 Etc/UTC], + ~Y[2017-09-01 06:00:00 Etc/UTC], + ~Y[2017-12-01 06:00:00 Etc/UTC], + ~Y[2018-01-01 06:00:00 Etc/UTC], + ~Y[2018-06-01 06:00:00 Etc/UTC], + ~Y[2018-10-01 06:00:00 Etc/UTC], + ~Y[2019-02-01 06:00:00 Etc/UTC], + ~Y[2019-03-01 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -204,14 +204,14 @@ defmodule Cocktail.MonthlyTest do test "Every month 11th day of the month" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days_of_month: [11]) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-01-11 06:00:00 UTC], - ~Y[2017-02-11 06:00:00 UTC], - ~Y[2017-03-11 06:00:00 UTC] + ~Y[2017-01-11 06:00:00 Etc/UTC], + ~Y[2017-02-11 06:00:00 Etc/UTC], + ~Y[2017-03-11 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -219,15 +219,15 @@ defmodule Cocktail.MonthlyTest do test "Every month 31day of the month" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days_of_month: [31]) assert first_n_occurrences(schedule, 4) == [ - ~Y[2017-01-31 06:00:00 UTC], - ~Y[2017-02-28 06:00:00 UTC], - ~Y[2017-03-31 06:00:00 UTC], - ~Y[2017-04-30 06:00:00 UTC] + ~Y[2017-01-31 06:00:00 Etc/UTC], + ~Y[2017-02-28 06:00:00 Etc/UTC], + ~Y[2017-03-31 06:00:00 Etc/UTC], + ~Y[2017-04-30 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -235,14 +235,14 @@ defmodule Cocktail.MonthlyTest do test "support negative for day of the month" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days_of_month: [-11]) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-01-21 06:00:00 UTC], - ~Y[2017-02-18 06:00:00 UTC], - ~Y[2017-03-21 06:00:00 UTC] + ~Y[2017-01-21 06:00:00 Etc/UTC], + ~Y[2017-02-18 06:00:00 Etc/UTC], + ~Y[2017-03-21 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -250,14 +250,14 @@ defmodule Cocktail.MonthlyTest do test "Every other month 10th of the month and sunday:" do schedule = - ~Y[2017-01-02 06:00:00 UTC] + ~Y[2017-01-02 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, interval: 2, days_of_month: [10, 12], days: [:sunday, :saturday]) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-03-12 06:00:00 UTC], - ~Y[2017-09-10 06:00:00 UTC], - ~Y[2017-11-12 06:00:00 UTC] + ~Y[2017-03-12 06:00:00 Etc/UTC], + ~Y[2017-09-10 06:00:00 Etc/UTC], + ~Y[2017-11-12 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -265,16 +265,16 @@ defmodule Cocktail.MonthlyTest do test "Monthly on the 10th and 14th hours of the day" do schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, hours: [10, 14]) assert first_n_occurrences(schedule, 5) == [ - ~Y[2017-01-01 10:00:00 UTC], - ~Y[2017-01-01 14:00:00 UTC], - ~Y[2017-02-01 10:00:00 UTC], - ~Y[2017-02-01 14:00:00 UTC], - ~Y[2017-03-01 10:00:00 UTC] + ~Y[2017-01-01 10:00:00 Etc/UTC], + ~Y[2017-01-01 14:00:00 Etc/UTC], + ~Y[2017-02-01 10:00:00 Etc/UTC], + ~Y[2017-02-01 14:00:00 Etc/UTC], + ~Y[2017-03-01 10:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -284,14 +284,14 @@ defmodule Cocktail.MonthlyTest do days_of_month = [-1, -2, -3, -4, -5, -6, -7] schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days: [:friday], days_of_month: days_of_month) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-01-27 06:00:00 UTC], - ~Y[2017-02-24 06:00:00 UTC], - ~Y[2017-03-31 06:00:00 UTC] + ~Y[2017-01-27 06:00:00 Etc/UTC], + ~Y[2017-02-24 06:00:00 Etc/UTC], + ~Y[2017-03-31 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -301,14 +301,14 @@ defmodule Cocktail.MonthlyTest do days_of_month = [-1, -2, -3, -4, -5, -6, -7] |> Enum.reverse() schedule = - ~Y[2017-01-01 06:00:00 UTC] + ~Y[2017-01-01 06:00:00 Etc/UTC] |> Cocktail.schedule() |> Schedule.add_recurrence_rule(:monthly, days: [:friday], days_of_month: days_of_month) assert first_n_occurrences(schedule, 3) == [ - ~Y[2017-01-27 06:00:00 UTC], - ~Y[2017-02-24 06:00:00 UTC], - ~Y[2017-03-31 06:00:00 UTC] + ~Y[2017-01-27 06:00:00 Etc/UTC], + ~Y[2017-02-24 06:00:00 Etc/UTC], + ~Y[2017-03-31 06:00:00 Etc/UTC] ] assert_icalendar_preserved(schedule) @@ -317,7 +317,7 @@ defmodule Cocktail.MonthlyTest do test "a monthly schedule with a UTC datetime and a days of month option" do schedule = ~N[2021-02-28 06:00:00] - |> Timex.to_datetime("UTC") + |> Cocktail.Time.to_datetime("UTC") |> Schedule.new() |> Schedule.add_recurrence_rule(:monthly, days_of_month: [1]) @@ -332,7 +332,7 @@ defmodule Cocktail.MonthlyTest do test "a monthly schedule with a zoned datetime and a days of month option" do schedule = ~N[2021-02-28 06:00:00] - |> Timex.to_datetime("America/Vancouver") + |> Cocktail.Time.to_datetime("America/Vancouver") |> Schedule.new() |> Schedule.add_recurrence_rule(:monthly, days_of_month: [1]) diff --git a/test/cocktail/parser/i_calendar_test.exs b/test/cocktail/parser/i_calendar_test.exs index 945dbc1..75c6ebf 100644 --- a/test/cocktail/parser/i_calendar_test.exs +++ b/test/cocktail/parser/i_calendar_test.exs @@ -24,7 +24,7 @@ defmodule Cocktail.Parser.ICalendarTest do """ assert {:ok, schedule} = parse(schedule_string) - assert schedule.start_time == ~Y[2017-08-10 16:00:00 UTC] + assert schedule.start_time == ~U[2017-08-10 16:00:00Z] end test "parse a schedule with a zoned time" do @@ -155,12 +155,12 @@ defmodule Cocktail.Parser.ICalendarTest do test "parse a schedule with an incomplete DTSTART" do schedule_string = "DTSTART" - assert {:error, {"Input datetime string cannot be empty!", 0}} = parse(schedule_string) + assert {:error, {:invalid_format, 0}} = parse(schedule_string) end test "parse a schedule with an invalid DTSTART" do schedule_string = "DTSTART:invalid" - assert {:error, {"Expected `1-4 digit year` at line 1, column 1.", 0}} = parse(schedule_string) + assert {:error, {:invalid_format, 0}} = parse(schedule_string) end test "parse a schedule with an invalid timezone" do @@ -168,7 +168,7 @@ defmodule Cocktail.Parser.ICalendarTest do DTSTART;TZID=invalid:20170810T160000 """ - assert {:error, {:time_zone_not_found, 0}} = parse(schedule_string) + assert {:error, {{:invalid_timezone, "invalid"}, 0}} = parse(schedule_string) end test "parse a schedule with an invalid DTEND" do @@ -177,7 +177,7 @@ defmodule Cocktail.Parser.ICalendarTest do DTEND:invalid """ - assert {:error, {"Expected `1-4 digit year` at line 1, column 1.", 1}} = parse(schedule_string) + assert {:error, {:invalid_format, 1}} = parse(schedule_string) end test "parse a schedule with an rrule with an invalid frequency" do @@ -213,7 +213,7 @@ defmodule Cocktail.Parser.ICalendarTest do RRULE:FREQ=DAILY;INTERVAL=2;UNTIL=invalid """ - assert {:error, {"Expected `1-4 digit year` at line 1, column 1.", 1}} = parse(schedule_string) + assert {:error, {:invalid_format, 1}} = parse(schedule_string) end test "parse a schedule with an rrule with an invalid day" do diff --git a/test/cocktail/time_test.exs b/test/cocktail/time_test.exs new file mode 100644 index 0000000..faae841 --- /dev/null +++ b/test/cocktail/time_test.exs @@ -0,0 +1,5 @@ +defmodule Cocktail.TimeTest do + use ExUnit.Case + + doctest Cocktail.Time, import: true +end diff --git a/test/cocktail/validation/time_of_day_test.exs b/test/cocktail/validation/time_of_day_test.exs index 7a71558..ac8eb5c 100644 --- a/test/cocktail/validation/time_of_day_test.exs +++ b/test/cocktail/validation/time_of_day_test.exs @@ -26,7 +26,7 @@ defmodule Cocktail.TimeOfDayTest do test "a daily schedule with a zoned datetime and a time of day option" do schedule = ~N[2017-09-09 09:00:00] - |> Timex.to_datetime("America/Chicago") + |> Cocktail.Time.to_datetime("America/Chicago") |> Schedule.new() |> Schedule.add_recurrence_rule(:daily, times: [~T[10:00:00], ~T[12:30:00], ~T[17:45:00]]) diff --git a/test/cocktail/validation/time_range_test.exs b/test/cocktail/validation/time_range_test.exs index 29ed67f..92ffcb2 100644 --- a/test/cocktail/validation/time_range_test.exs +++ b/test/cocktail/validation/time_range_test.exs @@ -29,7 +29,7 @@ defmodule Cocktail.TimeRangeTest do test "a daily schedule with a zoned datetime and a time range option" do schedule = ~N[2017-09-09 09:00:00] - |> Timex.to_datetime("America/Chicago") + |> Cocktail.Time.to_datetime("America/Chicago") |> Schedule.new() |> Schedule.add_recurrence_rule( :daily, diff --git a/test/support/datetime_sigil.ex b/test/support/datetime_sigil.ex index 82fb7bb..70ee5fe 100644 --- a/test/support/datetime_sigil.ex +++ b/test/support/datetime_sigil.ex @@ -8,6 +8,6 @@ defmodule Cocktail.TestSupport.DateTimeSigil do "#{date} #{time}" |> NaiveDateTime.from_iso8601!() - |> Timex.to_datetime(zone) + |> Cocktail.Time.to_datetime(zone) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 29a3d99..580d1cc 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,4 @@ ExUnit.configure(exclude: [pending: true], formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) ExUnit.start() + +Code.require_file("test/support/datetime_sigil.ex")