How to implement Date.add(date, n, :month) in Elixir

Viewed 2260

Would be nice to have this in the standard Elixir library, but we don't.

Date.add(date, n, :month) # where n could be +/-

How would you implement this?

This looks like a good starting point: https://stackoverflow.com/a/53407676/44080

5 Answers

You could use the Timex implementation:

  defp shift_by(%NaiveDateTime{:year => year, :month => month} = datetime, value, :months) do
    m = month + value
    shifted =
      cond do
        m > 0 ->
          years = div(m - 1, 12)
          month = rem(m - 1, 12) + 1
          %{datetime | :year => year + years, :month => month}
        m <= 0 ->
          years = div(m, 12) - 1
          month = 12 + rem(m, 12)
          %{datetime | :year => year + years, :month => month}
      end

    # If the shift fails, it's because it's a high day number, and the month
    # shifted to does not have that many days. This will be handled by always
    # shifting to the last day of the month shifted to.
    case :calendar.valid_date({shifted.year,shifted.month,shifted.day}) do
      false ->
        last_day = :calendar.last_day_of_the_month(shifted.year, shifted.month)
        cond do
          shifted.day <= last_day ->
            shifted
          :else ->
            %{shifted | :day => last_day}
        end
      true ->
        shifted
    end
  end

Timex uses the MIT license, so you should be able to incorporate this in pretty much any project.

Date.utc_today() |> Timex.shift(months: -1) 

ex_cldr_calendars can also do basic date math for adding and subtracting years, quarters, months, weeks and days for any calendar that implements the Calendar behaviour.

iex> Cldr.Calendar.plus ~D[2019-03-31], :months, -1
~D[2019-02-28]

# The :coerce option determines whether to force an end
# of month date when the result of the operation is an invalid date
iex> Cldr.Calendar.plus ~D[2019-03-31], :months, -1, coerce: false
{:error, :invalid_date}

Without adding a dependency like Timex, the following works for adding/subtracting Gregorian months without too much trouble - assuming you only need the first of each month. Shifting to a day of the month directly may be best served through a library, given how many calendrical fallacies there are.

defmodule DateUtils
  @doc """
  Shift a given date forward or back n months
  """
  def shift_n_months(date, n) when n < 0, do: subtract_n_months(date, -1 * n)
  def shift_n_months(date, n), do: add_n_months(date, n)

  def add_n_months(date, 0), do: Date.beginning_of_month(date)

  def add_n_months(date, n) do
    date
    |> Date.end_of_month()
    |> Date.add(1)
    |> add_n_months(n - 1)
  end

  def subtract_n_months(date, 0), do: Date.beginning_of_month(date)

  def subtract_n_months(date, n) do
    date
    |> Date.beginning_of_month()
    |> Date.add(-1)
    |> subtract_n_months(n - 1)
  end
end

There is an elixir function Date.add/2. Give it any date and it will add the dates for you.

iex>Date.add(~D[2000-01-03], -2)
~D[2000-01-01]

If you want to create the date to add to then i suggest you use the Date.new/4

iex>{:ok, date} = Date.new(year, month, day) 
iex>date |> Date.add(n)
Related