inicio mail me! sindicaci;ón

Archive for WebDev

A good use for “:group” in Rails

Recently i needed to make a custom view for activity logs in my Ruby on Rails app. I wanted to create a summary of the activity grouped by the day, without any duplicates. e.g. if i performed an action an object twice, i didn’t want it to be listed twice.

So how does one do this in Rails? Simple - use the “:group” (i.e. GROUP BY) parameter when doing a find() to combine results into single results.

The nitty gritty

For reference i used the following schema on my logs:


  create_table "application_logs" do |t|
    t.integer  "rel_object_id"
    t.text     "object_name"
    t.string   "rel_object_type"
    t.datetime "created_on",
    t.integer  "created_by_id",
    t.boolean  "is_private",
    t.boolean  "is_silent",
    t.integer  "action_id",
    t.integer  "page_id",
  end

In my case, most of my objects were linked to pages. And for those, i only wanted page activity to be listed once. e.g. “Modified page X” instead of “Modified object 1 on page X”, “Modified object 2 on page X”.

So i needed to group by page_id, created_by_id, created_on (as a date), and both rel_object_id and rel_object_id.

First i came up with the following :group :


"created_by_id,
 date(created_on),
 page_id,
 rel_object_type || rel_object_id"

Note: the “||” operator in SQLite and PostgreSQL concatenates strings.

Unfortunately that doesn’t work properly since i have other objects which aren’t linked to a page, and they would only get listed once (since for all of them page_id would be NULL). So i needed to use the “CASE” statement to differentiate between the two:


"created_by_id,
 date(created_on),
 CASE page_id ISNULL
   WHEN 1 THEN rel_object_type || rel_object_id
   ELSE page_id
 END"

So now both the page objects and the regular objects were listed once per day. But there was another problem.

After the query i used the group_by method to group everything into blocks based on the date. But i also use the time zone support in Rails 2.1, and since the database stores its dates in UTC, i got this rather odd issue where in certain circumstances objects were listed twice.

In reality they were the same date, in UTC. But not in the current timezone i was using. So after a bit of investigation i came up with a solution. I needed to offset the date in the query by the UTC offset for the current timezone.

It turns out that there are a ton of different ways to do it, depending on which database you are using. In my case i was testing with SQLite, so the following sufficed:


date(created_on, '+#{Time.zone.utc_offset} seconds')

And for MYSQL (and perhaps others), using INTERVAL works:


date(created_on + INTERVAL #{Time.zone.utc_offset} SECOND)

Of course, this still has its problems. Like what about daylight savings time?

Unfortunately, since there doesn’t seem to be any set standard for specifying what timezone to evaluate times in, you are either going to have to write a specific case for it, or just put up with the dates potentially being off for an hour or two.

The end result

Well, it looks something like this:

Fuzzy Times

Recently i have been writing a simple web-based reminder app which requires one to input dates and times. To input the date and time, normally one would add some sort of calendar widget which pops up.

But personally i think this tends to be really awkward, especially if you don’t want to be specific about dates or times. e.g. Speculative Opportunities. It also requires a lot of mouse clicking to find and enter the correct date and time.

Fortunately, there are libraries about which aim to solve this issue by allowing you to specify the time in English. Typically they are referred to as “Natural language” date/time parsers. In my case, i found one called Chronic which is distributed as a gem for Ruby. It has even got a nice screencast.

sudo gem install chronic

Consisting of only one public function, Chronic is really easy to use:


require 'chronic'
Chronic.parse("tomorrow at 5")
 #=> Fri Aug 08 17:00:00 +0100 2008

See? Nice and simple. We can also use the options to help chronic get the right time. e.g. If i really meant to say “Tomorrow at 5 AM”, i could fix it like this:


require 'chronic'
Chronic.parse("tomorrow at 5", :ambiguous_time_range => :none)
 #=> Fri Aug 08 5:00:00 +0100 2008

The options you can choose from are as follows:

  • :context - the context in which the time is assessed (:past or :future).
  • :now - current time.
  • :guess - if false, this returns a time range instead of guessing at a specific time.
  • :ambiguous_time_range - range in hours in which an ambiguous time will be resolved. Best to think of it as hours to skip in the day when picking a time. e.g. setting this to 18 and inputting 5 will result in 5am the next day being chosen.

Sadly i couldn’t seem to find any way of setting which time zone to evaluate the time in. This would have been useful when working with Ruby on Rails 2.1’s new TimeZone support. Fortunately though, i figured out a workaround which is as follows:


# grab time using current time zone as reference
ctime = Chronic.parse(value, :now => Time.zone.now)
# re-interpret time in current timezone
ctime = Time.zone.local(ctime.year, ctime.mon, ctime.day, ctime.hour, ctime.min, ctime.sec)

Basically this gives Chronic the time in the current time zone, which deals with relative times (e.g. “tomorrow”). It also re-interprets the calculated time in case you are a bit more specific (e.g. “5AM”).

For something a bit less hackish, one might want to check out technoweenie’s fork on github. This appears to allow you to tell Chronic to use a different Time class for calculating times, which should solve the problem.

So to conclude, i think Chronic is a nice and simple solution that works rather nicely for common cases.

Other libraries

If you aren’t using Ruby, have no fear. There are similar “Natural language” parsers available for other programming environments which work in a similar fashion to Chronic.