jamesmintram

jamesmintram

I have been reading the docs for Elixir’s Date/DateTime libraries but I cannot find a way to calculate a date which is either N months or N years before/after a particular date.

Date.add/2 appears to only accept days.
DateTime.add/3 will only accept “up to” :seconds

Can someone point me in the right direction?

Showing Posts 1 to 10

hubertlepicki

hubertlepicki

I generally use third party library for this, although maybe it’s possible withing standard library these days, but I default to Timex.shift from timex library:

kip

kip

ex_cldr Core Team

ex_cldr_calendars also provides the ability to add date parts to dates: Cldr.Calendar — Cldr Calendars v2.4.4

    iex> Cldr.Calendar.minus ~D[2019-03-01], :days, 1
    ~D[2019-02-28]
    
    iex> Cldr.Calendar.minus ~D[2019-03-01], :months, 1
    ~D[2019-02-01]
    
    iex> Cldr.Calendar.minus ~D[2019-03-01], :quarters, 1
    ~D[2018-12-01]
    
    iex> Cldr.Calendar.minus ~D[2019-03-01], :years, 1
    ~D[2018-03-01]
josevalim

josevalim

Creator of Elixir

I would love to have this feature in Elixir. To me it is really the last pending feature in the Calendar module. It seems it is a simple contract on a function called “plus”, which I would call “shift” instead.

@kip, how are the rules generated in your case? Automatically from CLDR? Is there a way we could see the rules converted to Elixir code for the ISO calendar?

Thank you!

kip

kip

ex_cldr Core Team

@josevalim, there’s no CLDR dependency on this part - its all straight Calendar callbacks.

The only part that is tricky is what to do with something like Calendar.shift ~[2016-02-29], :year, 1. It is an invalid result?

In my implementation I have an option :coerce to force a valid date at the end of the target month. I’ve never liked it. Suggestions very welcome on how to handle this.

Happy to draft a PR for this, its not very complicated other than that issue.

cmo

cmo

Timex has the concept of ambiguous datetimes, which force you to decide whether you want to take the earlier or later time.

LostKobrakai

LostKobrakai

Same on DateTime.shift_zone.

josevalim

josevalim

Creator of Elixir

The ambiguous DateTime is about daylight summer time, which is a separate issue from invalid dates. That’s because shift should be considered a shift on the wall clock instead of shifting the data in a contiguous line. This implies two things:

  1. You can land on invalid datetimes caused by datetime shifts in a timezone
  2. You can land on a date that does not really exist

For the second one, which was the one @kip mentioned, I suggest handling it with as a rounding operation, so we can either round :up or :down. I would pick :down as the default so you always land within the same month. Note this applies for quarter/month/year shifting.

For example, adding one day to Feb 28th is always March 1st. No invalid date is built in the process. Adding one month to Jan 31st should be Feb 28th (unless you have a leap year).

This further introduces complication related to commutative property of those operators. For example:

~D[2022-01-31] + 1 month + 1 month == ~D[2022-03-28]
~D[2022-01-31] + 2 month == ~D[2022-03-31]

That’s why shifting operations are often done with durations, because you need to express the whole operation in one tackle. For example, consider the difference between:

~D[2020-02-29] + 1 year + 1 month == ~D[2021-03-28]
~D[2020-02-29] + 1 year and 1 month == ~D[2021-03-29]

However, things get a bit trickier once you consider 1 year and -1 day. Do you remove the day from the resolved date or from the invalid date? E.g. which one is the desired result?

~D[2020-02-29] + 1 year and -1 day == ~D[2021-02-27]
~D[2020-02-29] + 1 year and -1 day == ~D[2021-02-28]

I think it should be the first. So my proposal would be to start with:

NaiveDateTime.shift(naive_date_time, shift_options) :: {:ok, naive_date_time}

shift_options is a keyword list with year, quarter, month, week, day, and round. shift_options will be applied on the order of highest to lower (i.e. the order defined above). Once you apply any of year/quarter/month, you need to round to a valid date. Then add week/day as wall clock shifts too.

We could support hour/minute/second too, but because we are strictly talking about wall-clock shifts, they have to be implemented as wall-clock shifts instead of operations on iso days, which is slightly annoying.

Besides the contiguous time interpretation existing in add today, another difference between this and the add function is that we will consider the definition of duration/interval specific to your calendar while add always adds a unit of value specific to the gregorian calendar.

I would also suggest starting with the implementation for NaiveDateTime, and then move to DateTime and Date next. If you agree @kip, a pull request is welcome!

LostKobrakai

LostKobrakai

All those complications are why I’m excited about alternative approaches explored by calendar_interval [1] or being explored with tempo.

Adding “1 month” to a date can mean many things like for example:

  1. Add the duration of a month to the current date (usually defaulted to 30 days)
  2. Get to the same day of month in the next month (usually take the last day of month if the day itself is invalid)
  3. Get me the day the same number of days before the end of month next month
today = Date.utc_today()

# Case 1
in_30_days = Date.add(today, 30)

# Case 2
first_of_next_month = 
  today
  |> Date.end_of_month()
  |> Date.add(1)

add = max(today.day, Date.days_in_month(first_of_next_month))
same_day_of_month_in_next = Date.add(first_of_next_month, add - 1)

# Case 3
diff = Date.diff(today, Date.end_of_month(today))
same_day_before_end_of_month_next_month = 
  today
  |> Date.end_of_month()
  |> Date.add(1)
  |> Date.end_of_month()
  |> Date.add(diff)

I’d love an API, which can express the difference in meaning without feeling overly imperative. The last two of my examples in my mind are much more a variant of:

date = ~D[2022-06-14]
day_of_month = day_of_month(date)
month = to_month(date)
next_month = add(month, 1)
result = somehow_add_back_day_of_month_part(next_month, day_of_month)

This however becomes more complicated once not just a single “unit of time” is added, but multiple ones and the precisions need to be taken at multiple levels of the addition.

1: https://www.youtube.com/watch?v=Zm95cYAtAa8

josevalim

josevalim

Creator of Elixir

Unfortunately I don’t think those libraries solve the problems I described above (@wojtekmach and @kip, pls correct me if I am wrong).

They do help expressing more complex constructs and how to compose and interpret them. However, all of the challenges in my previous comment already picked a unique answer for the question of what adding “1 month” means, which is:

After that, we still need to answer the following questions:

  1. what are the order of operations in a single interval? The order you add the units and round them matter. Maybe those libraries propose a semantic order already, which we could copy.

  2. What is your interpretation of time when it comes to days/seconds and timezones? Time may mean two things: wall-clock and a contiguous line. Working on the contiguous line is much easier but surprising, because it means adding 1 day during daylight saving time is actually adding 23 hours or 25 hours. A wall-clock operation would literally shift the day and return ambiguity errors if it falls exactly during dst.

And the second question in paticular is not answered in the libraries above (afaik).

LostKobrakai

LostKobrakai

I don’t think so as well – hence the “exploring” verb – but they feel closer to allowing users to express the different interpretations instead of the specific implementation picking one.

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
ryanwinchester
apply_graft/2 doesn’t rewrite an add_many sub-workflow’s deps on an add step. Grafted jobs cancel with “upstream job was deleted” Version...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews