Un poco de background sobre UTC en Rails 2.1 y ActiveRecord
Rails 2.1 viene con muy buen soporte para UTC que facilita mucho el hacer aplicaciones localizadas.
Es tan sencillo como declarar:
# config/environment.rb config.time_zone = 'UTC' # o 'Madrid' por ejemploTenemos a disposicion
rake tasks que nos facilitan el trabajo:
$ rake -T time rake time:zones:all # Displays names of all time zones recognized by the... rake time:zones:local # Displays names of time zones recognized by the Rai... rake time:zones:us # Displays names of US time zones recognized by the ..Para saber que timezones tenemos a disposicion hacemos:
$ rake time:zones:local * UTC +01:00 * Amsterdam Berlin ... Madrid Paris ...Luego para localizar la aplicacion, un
before_filter cumple facil la tarea:
# controllers/application.rb before_filter :set_time_zone def set_time_zone Time.zone = @current_user.time_zone if @current_user endcon su contraparte en las vistas para que el user seleccione su timezone:
# vale, TimeZone solo tiene us_zones pero algun hacker chovinista no tardara en mejorarlo <%= f.time_zone_select :time_zone, TimeZone.us_zones %> # y luego para mostrar la hora: <%= Time.zone.now.inspect %>Observa la diferencia entre Time.zone.now.inspect y Time.now.inspect:
# veamos la zona que tenemos activa >> Time.zone => #<ActiveSupport::TimeZone:0xb7aab45c @name="Madrid", @tzinfo=#<TZInfo::DataTimezone: Europe/Madrid>, @utc_offset=3600> # la fecha hora en formato espaƱol: >> Time.zone.now.inspect => "Tue, 05 Aug 2008 10:34:10 CEST +02:00" # y en formato ingles: >> Time.now.inspect => "Tue Aug 05 10:34:24 +0200 2008"Incluso podemos acceder el valor UTC de la fecha hora de un objeto antes de la conversion a la time zone con
before_type_cast:
# campo datetime convertido a la time zone >> m.created_at => Wed, 30 Jul 2008 14:41:28 CEST +02:00 # y el valor UTC archivado en la base de datos: >> m.created_at_before_type_cast => "2008-07-30 12:41:28"
A este valor al convertirlo a nuestra timezone simplemente habra que adicionarle el desplazamiento en husos horarios.
Tenemos mas helpers aun a disposicion:
# el constructor de fechas con numeros
>> Time.zone.local(2008, 8, 5, 10, 48, 18)
=> Tue, 05 Aug 2008 10:48:18 CEST +02:00
# el parseo pero con la timezone
>> Time.zone.parse('2008-08-05 10:48:18')
=> Tue, 05 Aug 2008 10:48:18 CEST +02:00
>> Time.zone.at(1207792098)
=> Thu, 10 Apr 2008 03:48:18 CEST +02:00
# y las horas en la timezone activa (Madrid)
>> t = Time.now
=> Tue Aug 05 10:50:47 +0200 2008
>> t.in_time_zone
=> Tue, 05 Aug 2008 10:50:47 CEST +02:00
>> t.in_time_zone('Madrid')
=> Tue, 05 Aug 2008 10:50:47 CEST +02:00
# para encontrar la hora en otra timezone por diferencias horarias con UTC
>> t.in_time_zone(+3.hours)
=> Tue, 05 Aug 2008 11:50:47 AST +03:00
Al respecto se han publicado interesantes articulos.
Trucos con el ActiveRecord
Sin embargo la cosa no va tan sobre rieles al encuestar al ActiveRecord pues este no hace conversiones a UTC por defecto. Veamoslo con ejemplos:# inicialicemos >> time_str = "2008-07-30T09:44:28+02:00" >> time = Time.parse time_strSi creamos un objeto cualquiera vemos que su
created_at se guarda en UTC en la bd:
# son las 9:44 en Madrid # en bd queda este valor created_at => '2008-07-30 07:44:28'
O sea, la fecha hora se guarda con hora UTC y luego segun la timezone que declaremos se le suman los desplazamientos en hora. El erb si que aprueba con sobresaliente en darnos soporte con la conversion a UTC, pero ActiveRecord tiene sus trucos.
Si ahora hacemos esta query:>> Xxx.find(:all, :conditions => ["created_at >= ?", time_str]) # esta es la query mal construida en el log: SELECT * FROM "xxxs" WHERE (created_at >= '2008-07-30T09:44:28+02:00')Esta claro que 2008-07-30T09:44:28+02:00 es la hora ya con su desplazamiento horario. Lo mismo ocurre mal con:
>> Xxx.find(:all, :conditions => ["created_at >= ?", time])Concluyendo: ActiceRecord no hace uso de UTC por defecto en todos los frentes, asi que necesitamos darle la vuelta usando time.utc en la
condition:
>> Xxx.find(:all, :conditions => ["created_at >= ?", time.utc]) # ahora si se ve correctamente en el log: SELECT * FROM "xxxs" WHERE (created_at >= '2008-07-30 07:44:28')