Date Handling in Rails Applications

Brief walkthru of capturing, storing, and displaying days with Ruby on Rails.

Date handling is one of the web’s inevitables. Almost any web application you write is going to include some form of date-bound hacking. Whether it’s capturing user input via a web form, calculating the difference between two times, or fetching data via a date range, time and the web are closely linked.

Luckily for us, Rails make handling dates and times, like everything else, fairly trivial; that is, once you have an idea of how everything works.

I recently rewrote the section of our admin site in Rails that handles the creation of back issues. Since I had to hunt all over the web for a complete package of documentation to finalize this small task I thought it might be a good idea to share my experience.

In the interest of time we’ll assume you have a Rails app up and running and have controllers and models in place that you’re looking to add date-handling code to.

Form Helpers

Let’s start with data capture via a simple web form. When storing a back issue in the database the most important data we need to capture is its print date. As far as our back issues are concerned, print date is just a month and a year. To accomplish this, Rails provides a few FormHelpers for picking dates that we’ll use in our view.

/app/view/back_issues/new.rhtml

<%= error_messages_for :back_issue %>
<% form_for :back_issue do |f| %>

Print Month: <%= select_month(Date.today, :field_name => "print_month") %> 

Print Year: <%= select_year(Date.today, :start_year => 1999, :field_name => "print_year") %>

<%= submit_tag value='Create Backissue' %>
<% end %>

select_month() and select_year() generate pulldown menus for picking the issue’s month and year.

Pay special attention the :field_name option. It’s not documented well in the Rails API proper and will come in handy when you need to change the name of these select boxes to something other than the default of date[month] and date[year]; necessary if you are using multiple instances of these helpers in a single form.

For still greater control over a date submitted via a form, you might want to take look at select_datetime, which provides a pulldown for each element of a timestamp. Or for something a little more user friendly, check out the really nice Protoype-based Calendar Date Select.

Storing the Data

There’s no real reason for us to store the values from these two fields in separate database columns. Rather we’ll use the month and year values to construct a date in the controller and store the result in a single datetime field called print_date.

/app/controllers/back_issue_controller.rb

class BackIssuesController < ApplicationController

  def new
    @back_issue = BackIssue.new(params[:back_issue])
    return unless request.post?
    @back_issue.print_date = Date.new(params[:date]['print_year'].to_i, params[:date]['print_month'].to_i)
    @back_issue.save!
    flash[:notice] = "New back issue created."
    redirect_to "/covers/new?bi=" + @back_issue.id.to_s

	# In the event our validations fail
    rescue ActiveRecord::RecordInvalid
      render :action => 'new'
  end

end

Ok, that should do it. Once we’ve created an instance of @back_issue with the parameters from the form, we have access to print_date and can give it a newly constructed datetime with Date.new().

Date.new() can take three parameters (year, month, day). We’re taking the default of 1 on the day parameter by not passing anything to the method. In this example, we’re really only concerned with the month and year so the default is fine.

In reality, the new() method on our BackIssuesController is actually a bit more complex than this– hence the params[:back_issue] — but for the sake of simplicity, we’re just showing how to push a new date into the publish_date column from pulldown menus.

The redirect_to in this case point to our cover upload form, but you can point it to whatever destination you like following a successful save!.

Manipulating Dates

We don’t actually need to modify the date after it’s submitted to our controller, but if we did, Ruby has a few operators for quickly adjusting a Date object.

Operator Function
+(n) Add n days to the date
-(n) Subtract n days to the date
>>(n) Add n months to the date
<<(n) Subtract n months to the date

Pretty simple stuff. To demonstrate, let’s take these operators for a spin on the console.

>> d = Date.new(2007, 8, 23)
=> #
>> d.to_s
=> "2007-08-23"
>> yesterday = d-1
=> #
>> yesterday.to_s
=> "2007-08-22"
>> tomorrow = d+1
=> #
>> tomorrow.to_s
=> "2007-08-24"
>> lastmonth = d<<1
=> #
>> lastmonth.to_s
=> "2007-07-23"
>> nextmonth = d>>1
=> #
>> nextmonth.to_s
=> "2007-09-23"

Displaying Dates

Alright, now that we’ve written our back issue date to the database, all that’s left to do is display it in a view. We probably don’t want to display the date formatted as it’s stored: ‘YYYY-MM-DD’. Rather we want something like the current back issue page that displays the full month name and the year. To accomplish this we’ll use strftime().

If you’re familiar with PHP’s date() method, strftime() is similar, you pass the method a format string that defines how you want the date output. The difference is that while you pass PHP’s date() both a format and a timestamp, strftime() is a method that operates on a Date object. And everything in Rails is an object.

Rails automatically knows to treat our datetime columns in the database as Date objects, so, after you’ve fetched the back issue data from the database in the controller, formatting the date in the view is as simple as:

<%= @back_issue.print_date.strftime('%B %Y') %>

Which will print something like “January 2007″ to the screen.

For a full list of format codes for strftime(), see the table below.

Format Meaning
%a The abbreviated weekday name (“Sun”)
%A The full weekday name (“Sunday”)
%b The abbreviated month name (“Jan”)
%B The full month name (“January”)
%c The preferred local date and time representation
%d Day of the month (01..31)
%H Hour of the day, 24-hour clock (00..23)
%I Hour of the day, 12-hour clock (01..12)
%j Day of the year (001..366)
%m Month of the year (01..12)
%M Minute of the hour (00..59)
%p Meridian indicator (“AM” or “PM”)
%S Second of the minute (00..60)
%U Week number of the current year, starting with the first Sunday as the first day of the first week (00..53)
%W Week number of the current year, starting with the first Monday as the first day of the first week (00..53)
%w Day of the week (Sunday is 0, 0..6)
%x Preferred representation for the date alone, no time
%X Preferred representation for the time alone, no date
%y Year without a century (00..99)
%Y Year with century
%Z Time zone name
%% Literal “%” character

Rails Magic Wishlist

And that’s about it. Not too difficult but it’s actually a little more work that I’d like to do. Frankly, I wish there was some Rails magic that did the date handling in the controller for me.

Creating a new date and casting parameters in the controller isn’t how I’d like this to function — ultimately it doesn’t strike me as terribly DRY. Rather, I would like to have the option of passing the form fields to the controller as back_issue[print_date_month] and back_issue[print_date_year] and have Rails realize that these are components of my print_date field and compile these elements into a date for me. But all-in-all the solution we have here is reasonably clean and maybe I can build some of that magic into a plugin or helper at a later date.

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62