diff --git a/lib/datetime/datetime.ex b/lib/datetime/datetime.ex index a4201efc..50ba3167 100644 --- a/lib/datetime/datetime.ex +++ b/lib/datetime/datetime.ex @@ -13,6 +13,7 @@ defimpl Timex.Protocol, for: DateTime do alias Timex.{Duration, AmbiguousDateTime} alias Timex.{Timezone, TimezoneInfo} + alias Timex.DateTime.Helpers def to_julian(%DateTime{:year => y, :month => m, :day => d}) do Timex.Calendar.Julian.julian_date(y, m, d) @@ -41,7 +42,7 @@ defimpl Timex.Protocol, for: DateTime do end def to_naive_datetime(%DateTime{} = d) do - # NOTE: For legacy reasons we shift DateTimes to UTC when making them naive, + # NOTE: For legacy reasons we shift DateTimes to UTC when making them naive, # but the standard library just drops the timezone info d |> Timex.DateTime.shift_zone!("Etc/UTC", Timex.Timezone.Database) @@ -57,7 +58,7 @@ defimpl Timex.Protocol, for: DateTime do def is_leap?(%DateTime{year: year}), do: :calendar.is_leap_year(year) def beginning_of_day(%DateTime{time_zone: time_zone, microsecond: {_, precision}} = datetime) do - us = Timex.DateTime.Helpers.construct_microseconds(0, precision) + us = Helpers.construct_microseconds(0, precision) time = Timex.Time.new!(0, 0, 0, us) with {:ok, datetime} <- @@ -80,7 +81,7 @@ defimpl Timex.Protocol, for: DateTime do end def end_of_day(%DateTime{time_zone: time_zone, microsecond: {_, precision}} = datetime) do - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) time = Timex.Time.new!(23, 59, 59, us) with {:ok, datetime} <- @@ -106,7 +107,7 @@ defimpl Timex.Protocol, for: DateTime do %DateTime{time_zone: time_zone, microsecond: {_, precision}} = date, weekstart ) do - us = Timex.DateTime.Helpers.construct_microseconds(0, precision) + us = Helpers.construct_microseconds(0, precision) time = Timex.Time.new!(0, 0, 0, us) with weekstart when is_atom(weekstart) <- Timex.standardize_week_start(weekstart), @@ -129,7 +130,7 @@ defimpl Timex.Protocol, for: DateTime do def end_of_week(%DateTime{time_zone: time_zone, microsecond: {_, precision}} = date, weekstart) do with weekstart when is_atom(weekstart) <- Timex.standardize_week_start(weekstart), date = Timex.Date.end_of_week(DateTime.to_date(date), weekstart), - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision), + us = Helpers.construct_microseconds(999_999, precision), time = Timex.Time.new!(23, 59, 59, us), {:ok, datetime} <- Timex.DateTime.new(date, time, time_zone, Timex.Timezone.Database) do datetime @@ -147,7 +148,7 @@ defimpl Timex.Protocol, for: DateTime do end def beginning_of_year(%DateTime{year: year, time_zone: time_zone, microsecond: {_, precision}}) do - us = Timex.DateTime.Helpers.construct_microseconds(0, precision) + us = Helpers.construct_microseconds(0, precision) time = Timex.Time.new!(0, 0, 0, us) with {:ok, datetime} <- @@ -169,7 +170,7 @@ defimpl Timex.Protocol, for: DateTime do end def end_of_year(%DateTime{year: year, time_zone: time_zone, microsecond: {_, precision}}) do - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) time = Timex.Time.new!(23, 59, 59, us) with {:ok, datetime} <- @@ -197,7 +198,7 @@ defimpl Timex.Protocol, for: DateTime do microsecond: {_, precision} }) do month = 1 + 3 * (Timex.quarter(month) - 1) - us = Timex.DateTime.Helpers.construct_microseconds(0, precision) + us = Helpers.construct_microseconds(0, precision) time = Timex.Time.new!(0, 0, 0, us) with {:ok, datetime} <- @@ -226,7 +227,7 @@ defimpl Timex.Protocol, for: DateTime do }) do month = 3 * Timex.quarter(month) date = Timex.Date.end_of_month(Timex.Date.new!(year, month, 1)) - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) time = Timex.Time.new!(23, 59, 59, us) with {:ok, datetime} <- Timex.DateTime.new(date, time, time_zone, Timex.Timezone.Database) do @@ -247,7 +248,7 @@ defimpl Timex.Protocol, for: DateTime do time_zone: time_zone, microsecond: {_, precision} }) do - us = Timex.DateTime.Helpers.construct_microseconds(0, precision) + us = Helpers.construct_microseconds(0, precision) time = Timex.Time.new!(0, 0, 0, us) with {:ok, datetime} <- @@ -275,7 +276,7 @@ defimpl Timex.Protocol, for: DateTime do microsecond: {_, precision} }) do date = Timex.Date.end_of_month(Timex.Date.new!(year, month, 1)) - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) time = Timex.Time.new!(23, 59, 59, us) with {:ok, datetime} <- Timex.DateTime.new(date, time, time_zone, Timex.Timezone.Database) do @@ -325,7 +326,9 @@ defimpl Timex.Protocol, for: DateTime do def set(%DateTime{} = date, options) do validate? = Keyword.get(options, :validate, true) - Enum.reduce(options, date, fn + options + |> Helpers.sort_options() + |> Enum.reduce(date, fn _option, {:error, _} = err -> err diff --git a/lib/datetime/erlang.ex b/lib/datetime/erlang.ex index cf915270..41741538 100644 --- a/lib/datetime/erlang.ex +++ b/lib/datetime/erlang.ex @@ -1,5 +1,6 @@ defimpl Timex.Protocol, for: Tuple do alias Timex.AmbiguousDateTime + alias Timex.DateTime.Helpers import Timex.Macros @epoch :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) @@ -40,7 +41,7 @@ defimpl Timex.Protocol, for: Tuple do end def to_datetime({{y, m, d}, {h, mm, s, us}}, timezone) when is_datetime(y, m, d, h, mm, s) do - us = Timex.DateTime.Helpers.construct_microseconds(us) + us = Helpers.construct_microseconds(us) dt = Timex.NaiveDateTime.new!(y, m, d, h, mm, s, us) with %DateTime{} = datetime <- Timex.Timezone.convert(dt, timezone) do @@ -57,7 +58,7 @@ defimpl Timex.Protocol, for: Tuple do def to_datetime(_, _), do: {:error, :invalid_date} def to_naive_datetime({{y, m, d}, {h, mm, s, us}}) when is_datetime(y, m, d, h, mm, s) do - us = Timex.DateTime.Helpers.construct_microseconds(us) + us = Helpers.construct_microseconds(us) Timex.NaiveDateTime.new!(y, m, d, h, mm, s, us) end @@ -285,7 +286,9 @@ defimpl Timex.Protocol, for: Tuple do defp do_set(date, options, datetime_type) do validate? = Keyword.get(options, :validate, true) - Enum.reduce(options, date, fn + options + |> Helpers.sort_options() + |> Enum.reduce(date, fn _option, {:error, _} = err -> err diff --git a/lib/datetime/helpers.ex b/lib/datetime/helpers.ex index b85c804d..10268d48 100644 --- a/lib/datetime/helpers.ex +++ b/lib/datetime/helpers.ex @@ -4,6 +4,14 @@ defmodule Timex.DateTime.Helpers do alias Timex.{Types, Timezone, TimezoneInfo, AmbiguousDateTime, AmbiguousTimezoneInfo} @type precision :: -1 | 0..6 + @set_option_priority %{ + year: 1, + month: 2, + day: 3, + hour: 4, + minute: 5, + second: 6 + } @doc """ Constructs an empty NaiveDateTime, for internal use only @@ -145,4 +153,10 @@ defmodule Timex.DateTime.Helpers do new_p end end + + def sort_options(options) when is_list(options) do + Enum.sort_by(options, fn {k, _} -> Map.get(@set_option_priority, k, 99) end) + end + + def sort_options(options), do: options end diff --git a/lib/datetime/naivedatetime.ex b/lib/datetime/naivedatetime.ex index 9d2529e5..5e7ebe17 100644 --- a/lib/datetime/naivedatetime.ex +++ b/lib/datetime/naivedatetime.ex @@ -3,6 +3,7 @@ defimpl Timex.Protocol, for: NaiveDateTime do This module implements Timex functionality for NaiveDateTime """ alias Timex.AmbiguousDateTime + alias Timex.DateTime.Helpers import Timex.Macros @epoch_seconds :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) @@ -56,7 +57,7 @@ defimpl Timex.Protocol, for: NaiveDateTime do end def end_of_day(%NaiveDateTime{microsecond: {_, precision}} = datetime) do - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) %{datetime | :hour => 23, :minute => 59, :second => 59, :microsecond => us} end @@ -70,7 +71,7 @@ defimpl Timex.Protocol, for: NaiveDateTime do def end_of_week(%NaiveDateTime{microsecond: {_, precision}} = date, weekstart) do with ws when is_atom(ws) <- Timex.standardize_week_start(weekstart) do date = Timex.Date.end_of_week(date, ws) - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) Timex.NaiveDateTime.new!(date.year, date.month, date.day, 23, 59, 59, us) end end @@ -80,7 +81,7 @@ defimpl Timex.Protocol, for: NaiveDateTime do end def end_of_year(%NaiveDateTime{year: year, microsecond: {_, precision}}) do - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) Timex.NaiveDateTime.new!(year, 12, 31, 23, 59, 59, us) end @@ -99,7 +100,7 @@ defimpl Timex.Protocol, for: NaiveDateTime do def end_of_month(%NaiveDateTime{year: year, month: month, microsecond: {_, precision}} = date) do day = days_in_month(date) - us = Timex.DateTime.Helpers.construct_microseconds(999_999, precision) + us = Helpers.construct_microseconds(999_999, precision) Timex.NaiveDateTime.new!(year, month, day, 23, 59, 59, us) end @@ -141,7 +142,9 @@ defimpl Timex.Protocol, for: NaiveDateTime do def set(%NaiveDateTime{} = date, options) do validate? = Keyword.get(options, :validate, true) - Enum.reduce(options, date, fn + options + |> Helpers.sort_options() + |> Enum.reduce(date, fn _option, {:error, _} = err -> err diff --git a/test/set_test.exs b/test/set_test.exs index 1377377f..44e6188b 100644 --- a/test/set_test.exs +++ b/test/set_test.exs @@ -125,4 +125,21 @@ defmodule SetTests do assert new_date.minute == 0 assert new_date.second == 0 end + + test "set day 31 and another month from date with month with only 30 days" do + original_date = Timex.to_datetime({{2021, 4, 1}, {12, 0, 0}}) + new_date = Timex.set(original_date, day: 31, month: 5) + + assert new_date.month == 5 + assert new_date.day == 31 + end + + test "set day 29 for February from year without it" do + original_date = Timex.to_datetime({{2023, 2, 28}, {12, 0, 0}}) + new_date = Timex.set(original_date, day: 29, month: 2, year: 2024) + + assert new_date.month == 2 + assert new_date.day == 29 + assert new_date.year == 2024 + end end