Tuesday, 12 May 2009

Date and Time in Rails

Rails adds some extra time and date functionality to Ruby, and so you can do cool things like 10.years. However, when you scratch below the surface, it all becomes a bit mysterious. I put this into a view to investigate:
<%
[1.seconds, 1.minutes, 1.hours, 1.days, 1.months, 1.years].each do |period|
%>
<ul>
<li><%="value=#{period}"%></li>
<li><%="inspect=#{period.inspect}"%></li>
<li><%="class=#{period.class}"%></li>
<li><%="Duration?=#{period.is_a?(ActiveSupport::Duration)}"%></li>
<li><%="to_i=#{period.to_i}"%></li>
<li><%="to_i.class=#{period.to_i.class}"%></li>
<li><%="to_i.inspect=#{period.to_i.inspect}"%></li>
</ul>
<% end %>

The output for seconds looked like this:

  • value=1

  • inspect=1 second

  • class=Fixnum

  • Duration?=true

  • to_i=1

  • to_i.class=Fixnum

  • to_i.inspect=1


It would seem that what we are using here are ActiveSupport::Duration objects, which are pretending to be Fixnum objects. Their value is the duration in seconds, but an inspect will add the units, and for days, months and years will give the value in those units (but minutes and hours are given in seconds). I would guess the point here is to fool other methods into treating these objects as Fixnum objects, while retaining the added functionality. Curiously, the years method returns an object that pretends to be a Float, even though it does admit to being an ActiveSupport::Duration.

By the way, each of these methods has a singular alias; year can be used instead of years, etc.

Something else a little odd here (I am multiply the first by 1.0 to convert to float arithmetic):
<p>Number of days in a month: <%= 1.0 * 1.months / 1.days %></p>
<p>Number of months in a year: <%= 1.years / 1.months %></p>
<p>Number of days in a year: <%= 1.years / 1.days %></p>

The result:
Number of days in a month: 30.0
Number of months in a year: 12.175
Number of days in a year: 365.25

So the Rails system quietly ignores anomolous leap years (the last was in 1900, the next in 2100 so is a reasonable approximation), but more bizarrely has slightly more than 12 months in a year. Would it not have made more sense to have 30.4375 days in a month?

Rails also introduces the ActiveSupport::TimeWithZone object. This is an object that "acts like" a Time object, but can handle different time zones.

The created_at and updated_at fields of an ActiveRecord return objects of this type. You can also convert an ActiveSupport::Duration to an ActiveSupport::TimeWithZone using ago (and its alias since) and from_now (and its alias until). Both these statements produce ActiveSupport::TimeWithZone objects:
first_sample = Sample.find(:first).created_at
now = 0.seconds.ago

Arithmetic with ActiveSupport::TimeWithZone objects will give a Float object representing a number of seconds (surely an ActiveSupport::Duration masquerading as a Float would make more sense).
elapsed = now - first_sample

You can use ActiveSupport::TimeWithZone objects in your database searches.
# Find all samples changed in the last week
Sample.find :all :conditions => ["updated_at > ?", 1.week.ago]
# Count all samples created between 1 and 2 years ago.
Sample.count :conditions => ["created_at > ? AND created_at < ?", 2.years.ago, 1.year.ago]


See also:
http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html

Struggling with Ruby: Contents Page

1 comment: