Elixir: Add Time.to/from_duration and Date.to/from_gregorian_days

Created on 30 May 2017  路  25Comments  路  Source: elixir-lang/elixir

  • [ ] Date.to_gregorian_days(date)
  • [ ] Date.from_gregorian_days(days)
  • [ ] Time.from_duration(integer, unit \\ :second)
  • [ ] Time.to_duration(time, unit \\ :second)

Both to_duration and to_gregorian_days need to convert to ISO calendar before.

Elixir Enhancement Intermediate

All 25 comments

/cc @Qqwy

In March 29, I proposed this:

  • Date.to_gregorian(date, unit \\ :day)
  • Date.from_gregorian(days, unit \\ :day)
  • Time.to_gregorian(time, unit \\ :second)
  • Time.from_gregorian(value, unit \\ :second)
  • NaiveDateTime.to_gregorian(naive_datetime, unit \\ :second)
  • NaiveDateTime.from_gregorian(value, unit \\ :second)
  • DateTime.to_gregorian(datetime, unit \\ :second)

I like it in that it is consistent. But not having days in the name makes it sound a bit too cryptic.

I am including this as part of v1.5 because not having this functionality is forcing us to use :calendar in more places then we were supposed to.

Pinging @michalmuskala @fxn @lau @Qqwy to get this discussion moving as I would like to make this part of 1.5.0 and the first RC will be out next week. :)

@josevalim Is your suggestion that Date.from_gregorian would work like :calendar.gregorian_days_to_date? Would the functions be based on days/second since 0000-01-01T00:00:00?

@lau yes! The goal is to add a consistent API so we no longer need to use :calendar. In many projects (Ecto, Postgrex, etc) we need to use both Calendar and :calendar because we are missing those functions.

Also note we do not need to use gregorian days as out point of reference. It can be anything we want.

We could even use rata_die as reference (which is 0001-01-01) but I think rata_die is even more obscure than gregorian for those not familar with calendars.

It is also worth mentioning that we kinda have this functionality for NaiveDateTime via the add and diff functions:

# to_gregorian_seconds
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10], ~N[0000-01-01 00:00:00])
63579428950

# from Gregorian seconds
iex> NaiveDateTime.add(~N[0000-01-01 00:00:00], 63579428950)
~N[2014-10-02 00:29:10]

The advantage of the above is that you can use any frame of reference. For example, unix epoch. The downside is that it is more inefficient (unless we decide to inline gregorian and unix times as references) and it may be non-obvious.

So one alternative is to not implement to_gregorian/from_gregorian but add Date.diff, Date.add, Time.diff, Time.add and DateTime.diff.

Since Time.to_gregorian does not have any information about dates, we cannot know the amount of seconds since 0000-01-01T00:00:00.

If we have Date.to_gregorian_days then we don't need the same function in NaiveDateTime or DateTime unless they have to work differently somehow.

If the goal is to avoid using :calendar, we could simply implement the same kind of functions that are in :calendar. Just a few of simple ones that make sense for the type. E.g. days for Date and seconds for NaiveDateTime.

@lau Yes, that's why Time.to_duration or Time.diff will likely be better than Time.to_gregorian (since to_gregorian is just plain inaccurate).

You are also right that if we have Time.to_duration and Date.to_gregorian_days, we don't need NaiveDateTime.to_gregorian. However, NaiveDateTime.from_gregorian is not that straight-forward to implement with Date.from_gregorian_days and Time.from_duration:

days = div(seconds, 86400)
remainder = rem(seconds, 86400)
date = Date.from_gregorian_days(days)
time = Time.from_duration(remainder)
NaiveDateTime.new(date, time)

So I would argue adding the conversion functions to NaiveDateTime is worthy based on developer ergonomics alone.

If we can't agree on the following names: Time.from_duration, Date.from_gregorian_days and NaiveDateTime.from_gregorian, then I will suggest for us to go ahead with the add/diff API, which will cover all of the fronts that we need.

I like add, subtract, diff which partly already exists in e.g. NaiveDateTime.

:calendar.datetime_to_gregorian_seconds can already be done using
NaiveDateTime.diff(ndt, ~N[0000-01-01 00:00:00])

Making diffing with 0000-01-01T00:00:00 and 1970-01-01T00:00:00 faster could be a documented feature.

Date.diff exists too, so that can be used instead of :calendar.date_to_gregorian_days. So maybe the solution is to simply optimize those diffs for 0000-01-01 and 0000-01-01T00:00:00. Then there is no need to add new functions.

Oh, Date.diff is in master. I was looking at the stable API. :) So I will open up an issue to add:

  • Time.add and Time.diff
  • Date.add
  • DateTime.diff

I will also improve the module docs to include examples of using those functions to compute reference points.

I think these types can easily seem more similar than they are. For instance Time being quite different from the others. In how it "rolls over" and starts again at midnight.

@lau that's a very good point. Would that be a case against Time.add or do you think we should just rollover?

A good question and I'm not sure what the best answer is.

Maybe a simple version is to return an error if the result of add would be past 23:59:59.999999. Same if negative numbers can be used in add in order to subtract and the result would go "below" 00:00:00. (Speaking of which we could consider having a subtract function for convenience.)

But what if the user wants to know that if you add 7200 seconds to 23:00 then you get 01:00 in the next day? Do we also want to allow that? If so, should a result of {~T[01:00:00], 1} be returned for 1 day later?

@lau returning a tuple with the amount of overlaps is probably the best solution as you can simply discard the second element if you don't care about that and match on 0 if you don't want any overlap.

Just a little remark: In Time I would stay away from any semantics that involve durations. As you know, what you get when you add seconds may depend on a DST change in the middle, for example.

I don't have enough perspective to have an opinion about whether it should wrap or err, but if it wraps I think the docs should be really neutral in that the functions are doing modulo arithmetic and that is all.

Are you aware of use cases where wrapping without DST info may make sense? That could be a way to decide.

@fxn DST concerns are handled in DateTime. Which is exactly why we are not adding DateTime.add because Elixir does not handle timezone concerns in itself (but leaves it up to libraries).

My remark was triggered by

But what if the user wants to know that if you add 7200 seconds to 23:00 then you get 01:00 in the next day?

A user cannot wonder that, because Time is not able to know. All Time can offer, if it wraps, is modulo arithmetic. So the question would be, are there use cases for bare modulo arithmetic that wraps?

@fxn I am not sure. But as long as we return all information for the user to be able to take a decision, we should be golden. Alternatively, we can just go with Time.diff and leave Time.add for later.

The most important thing here in my mind is to document in a way that makes clear it's all mod arithmetic (and this is independent of wrapping, the DST jump could happen in the same day).

So, the wording an examples would be in that line. You can represent a time table of flights with Time, but you cannot shift it with Time.add.

BTW, I have in mind the early days of Active Support in which 1.month had a fixed duration. People misused it because the API kind of invited to add one month to a date. The problem is that there are few real use cases for that API, which then switched to duration objects whose semantics actually make them useful.

So, real use cases are a driver in a case like this. In which situation would you want to add a time shift in a way that is useful without DST info?

I can't think of any for now so let's go with what makes sense instead of trying to make the API consistent but without use cases. :)

See new #6183.

And thanks everyone for the discussion!

@fxn Time like Date andNaiveDateTime does not have a timezone. NaiveDateTime has an add function too. A use case for Time.add is if you do not know the time zone but want to know the result of a calculation that assumes no DST changes and no leap seconds. These assumptions are the same for NaiveDateTime. For instance if someone has an alarm clock (that does not automatically set itself) and there is a snooze function. When you press the button and it adds 10 minutes, what would the clock read when the alarm goes off 10 minutes later?

That being said, I haven't personally needed such a function and I didn't add one to the Calendar library. Because of a preference of adding functionality that is some good combination of being easy to understand, not confusing and solving real problems people have. Also how it is easy to add functions to libraries, but hard to remove.

Was this page helpful?
0 / 5 - 0 ratings