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

6

6

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://mcmap.net/q/1772608/-how-to-get-previous-month-in-elixir

Apportion answered 26/6, 2019 at 12:22 Comment(7)
What would be wrong with taking my answer from the question linked and wrap it with Enum.reduce(1..n, date, fn _, acc -> previous_month(acc) end)?Sarazen
If @AlekseiMatiushkin's answer isn't working for you probably you should add some detail to your question to explain why you can't use his answer. It's not apparent to me why his answer won't work for your need.Intermolecular
@OnorioCatenacci say you need the date 14 months ago? How many calls to previous_month(acc) are you going to make? I thought the idea would be to make just one callApportion
Eh. How many processor ticks is a single call to one Elixir function going to make? Just write a wrapper as I showed above and make one call to this wrapper. @OnorioCatenacci honestly, I am not sure what the solution the OP after.Sarazen
@AlekseiMatiushkin maybe i don't understand your proposed solution: Enum.reduce(1..n, date, fn _, acc -> previous_month(acc) end). You are doing Enum.reduce(1..n, ...meaning you are going to call previous_month(acc) at least n times. That is what i meant by n callsApportion
So what? I do not get why you consider this harmful. After all, you want a single call, go ahead, write your own implementation and publish it to OSS.Sarazen
@AlekseiMatiushkin no fight bro, i was just asking. Thanks.Apportion
K
5
Date.utc_today() |> Timex.shift(months: -1) 
Karim answered 27/1, 2021 at 14:12 Comment(1)
While this may answer the question, please consider adding some explanation.Commissioner
A
3

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.

Actionable answered 26/6, 2019 at 12:55 Comment(1)
Timex doesn't support the Calendar behaviour and assumes every date is in the proleptic Gregorian calendar. Sure, thats a very common usage in a lot of countries but its not universal. Even in the US, for example, many corporate calendars use 445 calendars and start on different months and days of the week.Joacima
J
3

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}
Joacima answered 27/1, 2021 at 23:46 Comment(0)
E
1

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
Elaboration answered 5/10, 2021 at 19:38 Comment(0)
D
1

Since elixir 1.17, you can add or subtract durations using elixir's Date.shift/2 function

Date.shift/2 expects a date as it's first argument and a duration as it's second argument.

Here's a couple of examples taken from the documentation

iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]

Date.shift(~D[2016-01-31], year: 4, day: 1)
~D[2020-02-01]

Note that you can use positive or negative integers depending on wether you want to add or subtract a duration to the date.

Dunham answered 30/6 at 12:22 Comment(0)
J
-3

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)
Jonme answered 27/6, 2019 at 2:55 Comment(2)
How do you suppose to use this bare function to subtract exactly three months?Sarazen
In that case you can find the number of days in those months then do the subtraction.Jonme

© 2022 - 2024 — McMap. All rights reserved.