Space Vatican

Ramblings of a curious coder

Dates, Params and You

A not particularly nice area of Rails are the date and time helpers. 3 popups just isn’t a very nice bit of user interface. It’s a lot of clicks when you want to change dates and most people can’t reason in their head about just the date. It’s far easier to pick a date from a calendar type view. Still the helpers rails provides are fine for that quick and dirty date input.

Based on the questions on the mailing list about this, the thing that trips people up is that, unlike other attributes you might typically have, dates and times are not representable by a single input control. Instead you have several, one for each component (year, month, day etc…). So in particular, there is no single value in your params hash with your date or time. Exactly what is in your params hash depends on whether your using select_date or date_select (if you’re entering a datetime, select_datetime or datetime_select).

These are to each other as text_field_tag is to text_field: date_select is expecting to hook up to an attribute of an instance variable (or if you use form_for or fields_for an attribute of the corresponding object) whereas select_date isn’t. However unlike the other pairs of functions like textfield_tag/text_field, select/select_tag these two send very different parameters through to your controller.

select_date is perhaps the easiest to understand. It will result in a hash (by defaults it is named “date”, but you can override this with the :prefix option) with keys like year, month, day. You can then put those together to get an instance of Date or Time. For example the following in your view

1
<%= select_date Date::today, :prefix => 'start' %>

will result in a params hash like this:

1
{:start => {:year => '2008', :month => '11', :day => '22'}}

As I said, there is nothing in the params hash that is the actual value. You have to put it together yourself, for example

1
2
my_date = Date::civil(params[:start][:year].to_i, params[:start][:month].to_i,
                      params[:start][:day].to_i)

A bit more work than you average parameter, but there’s nothing mysterious going on here. Under the hood, select_date is also quite boring: it’s just calling select_year, select_month, select_day with appropriate options and concatenating the result. A consequence of that is that if you want some odd combination (eg just months and seconds) you can just do that concatenation work yourself. One interesting thing about those subhelpers is that the first parameter you give them can be one of two things: – an integer in which case the corresponding day/month/year is displayed (eg 3 for March) – something like a Date or DateTime in which case the relevant date component is extracted from it

date_select is where the fun is. Here the expectation is that there is a model object we will want to update and we want to be able to do

1
my_object.update_attributes params[:my_object]

However update_attributes just wants to set attributes. If you pass it {‘foo’ => ‘bar’} it will try and call the method foo= passing bar as a parameter. For a date input that is made up of these multiple parameters this is clearly a problem. What solves this is something called multiparameter assignment. If there are parameters whose name is in a certain format, then instead of just trying to call the appropriate accessor Rails will gather the related parameters, feed them through a transformation function (for example Time.mktime or Date::new[1]) and then set the appropriate attribute.

The format used is as follows: all the related parameters start with the name of the attribute which lets Rails know they are related. Next Rails needs to know in what order to pass them to the transformation function and whether a typecast is needed. If your view contained

1
<%= date_select 'product', 'release_date' %>

Then your parameters hash would look like

1
2
{:product =>
        {'release_date(1i)' => '2008', 'release_date(2i)' => '11', 'release_date(3i)' => '22'}}

Rails can look at this and see that this is to do with the release_date attribute. It’s a date column, so rails knows to use Date::civil. The suffixes tell rails that 2008 is the first parameter to Date::civil and is an integer, that 11 is the second parameter and so on. Rails constructs the value using Date::civil(2008,11,22) and assigns that to release_date.

If you don’t intend to pass the parameters to update_attributes (or other functions with that syntax such as the new or create methods on an ActiveRecord class) there’s not a lot of point in putting up with the scary parameter names althouh you can of course construct the date yourself with

1
2
3
Date::civil(params[:product]['release\_date(1i)'].to_i,
 params[:product]['release\_date(2i)'].to_i,
params[:product]['release\_date(3i)'].to_i)

You might as well just use select_date and have readable parameter names though.

So, to sum up use date_select or datetime_select when creating/updating ActiveRecord objects but select_date or select_datetime for just a general purpose date input. As a closing tip, with select_datetime you can use the :use_hidden option in which case hidden form inputs are generated instead of select boxes.[2]


[1] There’s a bit more to this. For one the range of times representable by a Time object is limited on most platforms (since it’s commonly a 32 bit number of seconds since an epoch). Rails has some conversion code that will try and create an instance of Time but if necessary will fall back and create a DateTime object. Secondly there’s some cleverness to do with interpreting the user’s input with respect to the correct time zone.

[2] This is (I think) a slight misuse. The intent of the use hidden is that it is the mechanism by which the :discard_day and so on work