Epoch fail, Clojure edition

A while back, I wrote about doing epoch calculations in Elixir. Recently, I tried the same sort of thing in Clojure. Since Clojure sits on top of Java, I was not optimistic. Historically, Java has not had a good story to tell when it comes to date-time manipulation. But Java 1.8 added a new java.time package, so maybe it’s better now?

Clojure has clj-time, which wraps joda-time. Joda Time is the third-party library that most Java folks used before version 1.8 (and still do, I guess, if they’re stuck on 1.7 or 1.6 for some reason). Perhaps clj-time will move to java.time eventually, but for now at least they are still on joda-time. Maybe it would have been more sensible for me to just have used clj-time, but I wanted to use the java.time package.

I discovered there is a Clojure java-time (with a dash) that wraps java.time (with a period) and I started out doing that, but eventually abandoned it. When I got to the google-calendar calculation, I needed the .toInstant method, and java-time didn’t seem to supply it.

So, I changed to using java.time directly (if you look back in the git history of Epochs-clojure, you can see where I switched). Since Clojure’s Java interop is so seamless, it turned out pretty nice! Here is that google-calendar portion:

(defn google-calendar
  "Google Calendar time seems to count 32-day months from the day
  before the Unix epoch. @noppers worked out how to do this."
  [n]
  (let [seconds-per-day (* 24 60 60)
        utc (java.time.ZoneOffset/UTC)
        n-days (quot n seconds-per-day)
        n-seconds (rem n seconds-per-day)
        g-months (quot n-days 32)
        g-days (rem n-days 32)]
    (-> (java.time.LocalDateTime/ofEpochSecond (- seconds-per-day) 0 utc)
        (.plusDays g-days)
        (.plusMonths g-months)
        (.toInstant utc)
        (.plusSeconds n-seconds))))

Java time has Instants, which are the naive date-times that I want to do everything with. Similar to Elixir, java.time won’t let me add days or months to an Instant. So I create a LocalDateTime, add the days and months, then convert to an Instant and add the seconds.

Not bad. Not as nice as the Perl version, but better than the Elixir version. At least java.time gives me the .plusMonths method I need, even if it does make me choose a timezone to get it. And, remarkably, it’s just Java. I usually can’t bear to look at Java code, but Clojure makes it almost pretty.

I look forward to clj-time or java-time supplying more Clojure-like access to java.time in the future. But in the meantime, accessing it directly isn’t that bad!


(Update: 2017-08-06) I went to see if I could add .toInstant support for LocalDateTimes in java-time. I figured rather than just complain, I’d send a pull request. In so doing, I discovered it is already there! If I have a LocalDateTime in ldt, then this Java interop code

(.toInstant ldt java.time.ZoneOffset/UTC)

can be written as this java-time code

(instant ldt (zone-offset 0))

I think I’m going to leave Epochs-clojure as Java interop for now, but maybe I’ll switch back to java-time in the future if things start to get too Java-y.

Advertisements
Epoch fail, Clojure edition

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

VS Code

One of the things I learned at Go Maryland tonight was that VS Code is not just for Windows; they have versions for Linux and OS X too! With a name like Visual Studio Code, I just assumed it was a Windows thing. Not so!

Naturally, when I got home I had to try it! And here it is running on my Linux machine!

Screen shot of VS Code

As you can see, it understands Go code (on the left), but not Elixir (on the right). At least, not yet. I’m sure it will eventually. I also tried out Perl (yes), Python (yes), and Ruby (yes) — no surprises there — as well as Erlang (no), Pony (no), Rust (yes), and Clojure (yes) — a couple of nice surprises there!

The cursor blinks by default, so the first thing I had to do was figure out how to shut that off 1. It only took me a minute or two to find and change the configuration to a non-blinking cursor. Well done, VS Code!

I doubt I’ll be giving up Emacs any time soon (indeed, I’m typing this blog entry with org2blog), but it’s nice to see another open source editor available. Great job, Microsoft!

Update: In case you’re curious, here’s a shot of the same two files opened in Emacs, which has an Elixir mode.

Screen shot of same two files in Emacs

Footnotes:

1

I can’t stand blinking. I think it’s genetic. My Mom never let us have Christmas lights that blinked either. And to be fair, my beloved Emacs has a blinking cursor by default also
VS Code

Hello, Elixir!

A while ago, I talked about trying Elixir and finished with this example of Hello World

#!/usr/bin/env elixir

greet = fn s -> IO.puts "Hello, #{s}!" end

if length(System.argv) == 0 do
  greet.("World")
else 
  Enum.each(System.argv, greet)
end

This looks pretty much the same as it would in an imperative language. First, we define a greet function that prints “Hello, string!” for whatever string we give it. If we’re given no arguments, we call this with “World”. Otherwise, we call it for each of the arguments (okay, that part is already looking a tiny bit functional, since a for loop is missing).

But we don’t often use if-then-else in Elixir. I ended that post saying I would look for a more elixiry way to do it. I did so, but I didn’t post about it.

My second pass at hello world in Elixir looked like this

#!/usr/bin/env elixir

greet = fn s -> IO.puts "Hello, #{s}!" end

hello = fn
  [] -> greet.("World")
  list -> Enum.each(list, greet)
end

hello.(System.argv)

This is the same greet function, but rather than an if-then-else, we define a new function, hello, that has one behavior when handed an empty list and a different behavior when handed a non-empty list. Then we simply call this function with the argument list of the program.

For a third pass, I threw in Elixir’s amazing pipe operator.

#!/usr/bin/env elixir

greet = fn s -> IO.puts "Hello, #{s}!" end

hello = fn
  [] -> greet.("World")
  list -> list |> Enum.each(greet)
end

hello.(System.argv)

This doesn’t really show it off much, but the pipe operator seems to be an important part of Elixir’s readability in real code.

Finally, I learned that we usually name standalone scripts like this with a .exs rather than .ex in Elixir, so the new hello.exs works the same as before

$ ./hello.exs
Hello, World!
$ ./hello.exs Hank Dean Brock
Hello, Hank!
Hello, Dean!
Hello, Brock!

Keen!

Hello, Elixir!