Epoch fail

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.

Epoch fail