Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set correct day when year or month is changed too #671

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 15 additions & 12 deletions lib/datetime/datetime.ex
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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} <-
Expand All @@ -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} <-
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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} <-
Expand All @@ -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} <-
Expand Down Expand Up @@ -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} <-
Expand Down Expand Up @@ -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
Expand All @@ -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} <-
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
9 changes: 6 additions & 3 deletions 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}})
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions lib/datetime/helpers.ex
Expand Up @@ -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
Expand Down Expand Up @@ -145,4 +153,10 @@ defmodule Timex.DateTime.Helpers do
new_p
end
end

def sort_options(options) when is_list(options) do
oliver-kriska marked this conversation as resolved.
Show resolved Hide resolved
Enum.sort_by(options, fn {k, _} -> Map.get(@set_option_priority, k, 99) end)
end

def sort_options(options), do: options
end
13 changes: 8 additions & 5 deletions lib/datetime/naivedatetime.ex
Expand Up @@ -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}})
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions test/set_test.exs
Expand Up @@ -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