I was initially excited when Elixir 1.3 was released with a Calendar module, but now that I’ve tried to use it in a project, I’m disappointed. It turns out that they didn’t include everything we need, so we still need to import the :calendar module from Hex.
Worse, there is now twice as much documentation to sift through to try to find the functions we need. Are we looking for DateTime.add or Calendar.DateTime.add? While I was working, I ended up keeping tabs open to not only Date, Time, DateTime, and NaiveDateTime, but also Calendar.Date, Calendar.Time, Calendar.DateTime, and Calendar.NaiveDateTime. I’m not certain, but it feels as though I’m actually worse off than I was without the 1.3 additions.
Adding to my frustration was that the module felt both overly pedantic and incomplete. I had to define my own function to find the date n months from a given date
defp plus_months(date, 0) do date end defp plus_months(date, n) when n > 0 do dim = Calendar.Date.number_of_days_in_month(date) plus_months(Calendar.Date.add!(date, dim), n-1) end
That was to do this calculation where we add days and months and seconds.
def google_calendar(n) do {whole_days, seconds} = div_rem(n, @seconds_per_day) {months, days} = div_rem(whole_days, 32) {:ok, date} = Date.new(1969,12,31) {:ok, time} = Time.new(0,0,0, {0,6}) date = date |> Calendar.Date.add!(days) |> plus_months(months) Calendar.NaiveDateTime.from_date_and_time!(date, time) |> Calendar.NaiveDateTime.add!(seconds) end
But I don’t actually care about a Date or Time; I just wanted to do all that to a NaiveDateTime. I think it’s overly pedantic to say we must add days to a Date rather than a NaiveDateTime. It doesn’t seem to buy us anything. Obviously we can do the whole calculation without choosing a time zone, so why all the hand-wringing?
What I really want to do is define a NaiveDateTime and add days, months, and seconds to it
def google_calendar(n) do {whole_days, seconds} = div_rem(n, 24 * 60 * 60) # A "Google month" has 32 days! {months, days} = div_rem(whole_days, 32) # A "Google epoch" is one day early. {:ok, datetime} = NaiveDateTime.new(1969,12,31,0,0,0,0) datetime |> plus_days(days) |> plus_months(months) |> plus_seconds(seconds) end
But to do that I had to keep unwrapping and re-wrapping the NaiveDateTime, so it’s not worth it.
def plus_days(ndt, n) do d = NaiveDateTime.to_date(ndt) t = NaiveDateTime.to_time(ndt) {:ok, ndt} = NaiveDateTime.new(Calendar.Date.add!(d, n), t) ndt end def plus_months(ndt, 0) do ndt end def plus_months(ndt, n) do d = NaiveDateTime.to_date(ndt) dim = Calendar.Date.number_of_days_in_month(d) plus_months(plus_days(ndt, dim), n-1) end def plus_seconds(ndt, n) do Calendar.NaiveDateTime.add!(ndt, n) end
This is the kind of gorgeous code I expect from Elixir
datetime |> plus_days(days) |> plus_months(months) |> plus_seconds(seconds)
but to get it we have to do something stupid.
Note that the corresponding calculation in the Perl version of the same project is that gorgeous
Time::Moment ->from_epoch(-$SECONDS_PER_DAY) ->plus_days($days) ->plus_months($months) ->plus_seconds($seconds);
Perl’s Time::Moment gives us a single immutable object representing a date and time of day with an offset from UTC in the ISO 8601 calendar system. All of the methods above were supplied.
I hope we get something more like that in Elixir 1.4 or 1.5; the half-baked support we got in 1.3 does not seem helpful. I made a branch with :calendar removed to illustrate. It has the linear transformations in one direction, but not the other. It has only one of the other transformations.