defmodule Contex.Utils do @moduledoc false def date_compare(%DateTime{} = a, %DateTime{} = b) do DateTime.compare(a, b) end def date_compare(%NaiveDateTime{} = a, %NaiveDateTime{} = b) do NaiveDateTime.compare(a, b) end def date_diff(%DateTime{} = a, %DateTime{} = b, unit), do: DateTime.diff(a, b, unit) def date_diff(%NaiveDateTime{} = a, %NaiveDateTime{} = b, unit), do: NaiveDateTime.diff(a, b, unit) @doc """ Adds intervals to dates. Note that only system time units (nanosecond, microsecond, millisecond, second) are supported by default by DateTime and NaiveDateTime. Minutes, Hours, Days & Weeks need to be converted to one of the supported units. Special cases have been introduced for :months & :years due to variable days in month and leap year behaviour. This has been copied from Timex. iex> {:ok, d1, 0} = DateTime.from_iso8601("2016-01-31T03:00:00Z") {:ok, ~U[2016-01-31 03:00:00Z], 0} iex> d1 = Contex.Utils.date_add(d1, 1, :months) ~U[2016-02-29 03:00:00Z] iex> _d1 = Contex.Utils.date_add(d1, 1, :years) ~U[2017-02-28 03:00:00Z] iex> {:ok, d1, 0} = DateTime.from_iso8601("2016-03-31T03:00:00Z") {:ok, ~U[2016-03-31 03:00:00Z], 0} iex> d1 = Contex.Utils.date_add(d1, -1, :months) ~U[2016-02-29 03:00:00Z] iex> _d1 = Contex.Utils.date_add(d1, -1, :years) ~U[2015-02-28 03:00:00Z] """ def date_add(dt, amount_to_add, :years), do: shift_by(dt, amount_to_add, :years) def date_add(dt, amount_to_add, :months) do new_date = shift_by(dt, amount_to_add, :months) if is_last_day_of_month(dt) do ldom_new = :calendar.last_day_of_the_month(new_date.year, new_date.month) %{new_date | day: ldom_new} else new_date end end def date_add(%DateTime{} = dt, amount_to_add, unit), do: DateTime.add(dt, amount_to_add, unit) def date_add(%NaiveDateTime{} = dt, amount_to_add, unit), do: NaiveDateTime.add(dt, amount_to_add, unit) defp is_last_day_of_month(%{year: year, month: month, day: day}) do :calendar.last_day_of_the_month(year, month) == day end defp date_min(a, b), do: if(date_compare(a, b) == :lt, do: a, else: b) defp date_max(a, b), do: if(date_compare(a, b) != :lt, do: a, else: b) def safe_min(nil, nil), do: nil def safe_min(nil, b), do: b def safe_min(a, ""), do: a def safe_min("", b), do: b def safe_min(a, nil), do: a def safe_min(%DateTime{} = a, %DateTime{} = b), do: date_min(a, b) def safe_min(%NaiveDateTime{} = a, %NaiveDateTime{} = b), do: date_min(a, b) def safe_min(a, b) when is_number(a) and is_number(b), do: min(a, b) def safe_min(_, _), do: nil def safe_max(nil, nil), do: nil def safe_max(nil, b), do: b def safe_max(a, nil), do: a def safe_max("", b), do: b def safe_max(a, ""), do: a def safe_max(%DateTime{} = a, %DateTime{} = b), do: date_max(a, b) def safe_max(%NaiveDateTime{} = a, %NaiveDateTime{} = b), do: date_max(a, b) def safe_max(a, b) when is_number(a) and is_number(b), do: max(a, b) def safe_max(_, _), do: nil # def safe_min(x, y), do: safe_combine(x, y, fn x, y -> min(x, y) end) # def safe_max(x, y), do: safe_combine(x, y, fn x, y -> max(x, y) end) def safe_add(x, y), do: safe_combine(x, y, fn x, y -> x + y end) defp safe_combine(x, y, combiner) when is_number(x) and is_number(y), do: combiner.(x, y) defp safe_combine(x, _, _) when is_number(x), do: x defp safe_combine(_, y, _) when is_number(y), do: y defp safe_combine(_, _, _), do: nil def fixup_value_range({min, max}) when min == max and max > 0, do: {0, max} def fixup_value_range({min, max}) when min == max and max < 0, do: {max, 0} def fixup_value_range({0, 0}), do: {0, 1} def fixup_value_range({0.0, 0.0}), do: {0.0, 1.0} def fixup_value_range({min, max}), do: {min, max} # DateTime shifting methods copied from `defimpl Timex.Protocol, for: DateTime` # License Details: # The MIT License (MIT) ## Copyright (c) 2016 Paul Schoenfelder # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. defp shift_by(%{year: y} = datetime, value, :years) do shifted = %{datetime | year: y + value} # If a plain shift of the year fails, then it likely falls on a leap day, # so set the day to the last day of that month case :calendar.valid_date({shifted.year, shifted.month, shifted.day}) do false -> last_day = :calendar.last_day_of_the_month(shifted.year, shifted.month) %{shifted | day: last_day} true -> shifted end end defp shift_by(%{} = datetime, 0, :months), do: datetime # Positive shifts defp shift_by(%{year: year, month: month, day: day} = datetime, value, :months) when value > 0 do if month + value <= 12 do ldom = :calendar.last_day_of_the_month(year, month + value) if day > ldom do %{datetime | month: month + value, day: ldom} else %{datetime | month: month + value} end else diff = 12 - month + 1 shift_by(%{datetime | year: year + 1, month: 1}, value - diff, :months) end end # Negative shifts defp shift_by(%{year: year, month: month, day: day} = datetime, value, :months) do cond do month + value >= 1 -> ldom = :calendar.last_day_of_the_month(year, month + value) if day > ldom do %{datetime | month: month + value, day: ldom} else %{datetime | month: month + value} end :else -> shift_by(%{datetime | year: year - 1, month: 12}, value + month, :months) end end end