diff --git a/lib/machete/matchers/iso8601_datetime_matcher.ex b/lib/machete/matchers/iso8601_datetime_matcher.ex index cb1ba31..9523e06 100644 --- a/lib/machete/matchers/iso8601_datetime_matcher.ex +++ b/lib/machete/matchers/iso8601_datetime_matcher.ex @@ -5,6 +5,7 @@ defmodule Machete.ISO8601DateTimeMatcher do import Machete.DateTimeMatcher import Machete.Mismatch + import Machete.NaiveDateTimeMatcher import Machete.Operators defstruct datetime_opts: nil @@ -20,6 +21,7 @@ defmodule Machete.ISO8601DateTimeMatcher do @type opts :: [ {:precision, 0..6}, {:time_zone, Calendar.time_zone() | :utc}, + {:offset_required, boolean()}, {:exactly, DateTime.t()}, {:roughly, DateTime.t() | :now}, {:before, DateTime.t() | :now}, @@ -34,6 +36,7 @@ defmodule Machete.ISO8601DateTimeMatcher do * `precision`: Requires the matched ISO8601 string to have the specified microsecond precision * `time_zone`: Requires the matched ISO8601 string to have the specified time zone. The atom `:utc` can be used to specify the "Etc/UTC" time zone + * `offset_required`: Requires the matched ISO8601 string to have a timezone. Defaults to true * `exactly`: Requires the matched ISO8601 string to be exactly equal to the specified DateTime * `roughly`: Requires the matched ISO8601 string to be within +/- 10 seconds of the specified DateTime. This values must be specified as a DateTime. The atom `:now` can be used to use the @@ -65,6 +68,9 @@ defmodule Machete.ISO8601DateTimeMatcher do iex> assert DateTime.utc_now() |> DateTime.to_iso8601() ~> iso8601_datetime(roughly: :now) true + iex> assert NaiveDateTime.utc_now() |> NaiveDateTime.to_iso8601() ~> iso8601_datetime(roughly: :now, offset_required: false) + true + iex> assert "2020-01-01T00:00:00.000000Z" ~> iso8601_datetime(roughly: ~U[2020-01-01 00:00:05.000000Z]) true @@ -85,13 +91,34 @@ defmodule Machete.ISO8601DateTimeMatcher do defimpl Machete.Matchable do def mismatches(%@for{} = a, b) when is_binary(b) do - DateTime.from_iso8601(b) - |> case do - {:ok, datetime_b, 0} -> datetime_b ~>> datetime(a.datetime_opts) - _ -> mismatch("#{inspect(b)} is not a parseable ISO8601 datetime") + with nil <- matches_date_time(b, a.datetime_opts) do + matches_naive_date_time(b, a.datetime_opts) end end def mismatches(%@for{}, b), do: mismatch("#{inspect(b)} is not a string") + + defp matches_date_time(b, datetime_opts) do + case DateTime.from_iso8601(b) do + {:ok, datetime_b, 0} -> datetime_b ~>> datetime(datetime_opts) + _ -> nil + end + end + + defp matches_naive_date_time(b, datetime_opts) do + {offset_required, datetime_opts} = Keyword.pop(datetime_opts, :offset_required, true) + + case NaiveDateTime.from_iso8601(b) do + {:ok, naive_datetime_b} -> + if offset_required do + mismatch("#{inspect(b)} does not have a time zone offset") + else + naive_datetime_b ~>> naive_datetime(datetime_opts) + end + + _ -> + mismatch("#{inspect(b)} is not a parseable ISO8601 datetime") + end + end end end diff --git a/test/machete/matchers/iso8601_datetime_matcher_test.exs b/test/machete/matchers/iso8601_datetime_matcher_test.exs index 6904673..ec967b5 100644 --- a/test/machete/matchers/iso8601_datetime_matcher_test.exs +++ b/test/machete/matchers/iso8601_datetime_matcher_test.exs @@ -28,6 +28,18 @@ defmodule ISO8601DateTimeMatcherTest do ) end + test "does not supports ISO8601 without timezones by default" do + assert "2020-01-01T00:00:00.000000" + ~>> iso8601_datetime(roughly: ~N[2020-01-01 00:00:00]) + ~> mismatch("\"2020-01-01T00:00:00.000000\" does not have a time zone offset") + end + + test "supports ISO8601 without timezones" do + assert "2020-01-01T00:00:00.000000" + ~>> iso8601_datetime(roughly: ~N[2020-01-01 00:00:00], offset_required: false) + ~> [] + end + test "produces a useful mismatch for exactly mismatches" do assert "2020-01-01T00:00:00.000000Z" ~>> iso8601_datetime(exactly: ~U[3000-01-01 00:00:00.000000Z])