¿Qué son Time zones (Husos Horarios)?
En geografía, huso horario es cada una de las veinticuatro áreas en que se divide la Tierra, siguiendo la misma definición de tiempo cronométrico. Se llaman así porque tienen forma de huso de hilar, y están centrados en meridianos de una longitud que es un múltiplo de 15°. Anteriormente, se usaba el tiempo solar aparente, con lo que las diferencias de hora entre una ciudad y otra eran de unos pocos minutos en los casos en los que las ciudades comparadas no se encontraban sobre un mismo meridiano. El empleo de los husos horarios corrigió el problema parcialmente, al sincronizar los relojes de una región al mismo tiempo solar medio. Todos los husos horarios se definen en relación con el denominado tiempo universal coordinado (UTC), huso horario centrado sobre el meridiano de Greenwich que recibe ese nombre por pasar por el observatorio de Greenwich (Londres). Puesto que la Tierra gira de oeste a este, al pasar de un huso horario a otro en dirección este hay que sumar una hora. Por el contrario, al pasar de este a oeste hay que restar una hora. El meridiano de 180°, conocido como línea internacional de cambio de fecha, marca el cambio de día.
Información extraída de wikipedia
Time zones en programación
Cuando programamos una aplicación que posiblemente sea utilizada en más de un pais debemos procurar el manejo de husos horarios, esto nos deja en manos de las librerías existentes nativas del lenguaje o de algún tercero. Si te ha tocado trabajar con time zones probablemente compartas mi opinion: “Todos los programadores odiamos los time zones”. Esto debido a que muchas veces las librerías son engorrosas de utilizar, o simplemente pensamos que estamos haciendo lo correcto y no es así. Bueno, basta de explicaciones, vamos al grano.
¿Cómo funcionan en Rails?
Los time zones o husos horarios en Rails al igual que muchos otros temas que ya hemos tocado, está atado con la libreriá ActiveSupport. También podemos utilizar la clase Date
nativa de Ruby que contiene la subclase DateTime
pero sencillamente recomiendo utilizar ActiveSupport ya veremos porqué.
Configuración de time_zone por defecto
Rails tiene una variable encargada de manejar el huso horario de nuestra aplicación, independientemente de la ubicación donde se encuentre instalado el servidor o el huso horario que éste tenga en su configuración el Sistema Operativo. ¿Qué permite esto? pues sencillo, la base de datos (todas) por lo general almacenan la información en UTC (+00:00), al utilizar la variable en la configuración de Rails ActiveRecord realizará el cambio de huso horario automáticamente en todas nuestros queries.
¿Donde se encuentra dicha configuración? dentro del directorio config/application.rb
1 # config.time_zone = "Europe/Madrid"
Para cambiar el huso horario basta con buscar el horario de preferencia en la lista de constantes de ActiveSupport o haciendo uso del siguiente rake task rake time:zones:all
.
Vale destacar que podemos especificar el horario únicamente con el nombre
config.time_zone = "Guadalajara"1 configtime_zone="Guadalajara"
Time.zone
Una vez que hayamos especificado el huso horario de nuestra preferencia vamos a poder utilizar la clase Time
, y el método zone
retornará el valor de la configuración.
12345678910111213 [33]pry(main)>require'active_support/all'[57]pry(main)>Timezone="Berlin"=>"Berlin"[58]pry(main)>Timenowin_time_zoneto_s=>"2014-03-13 02:22:48 +0100"[59]pry(main)>DateTimenowin_time_zoneto_s=>"2014-03-13 02:22:55 +0100"[60]pry(main)>Timezonenowto_s=>"2014-03-13 02:23:54 +0100"[61]pry(main)>DateTimenowto_s=>"2014-03-12T20:55:08-04:30"[62]pry(main)>Timenowto_s=>"2014-03-12 20:55:18 -0430"
Podemos ver que se ha guardado “Berlin” como el huso horario de preferencia, esto es exactamente igual que utilizar el config.time_zone
. Luego podemos ver que al utilizar Time.now.in_time_zone.to_s
nos retorna correctamente la hora en en “Berlin”. De igual manera DateTime.now.in_time_zone.to_s
y Time.zone.now.to_s
. También podemos apreciar que DateTime.now.to_s
y Time.now.to_s
son capaces de leer el huso horario pero en este caso el de mi computadora omitiendo nuestra variable de Time.zone
y con esto es que debemos tener cuidado. Se puede cometer el error de utilizar estas variables sin ser las que necesitamos y queremos.
Diferentes métodos
Rails a través de la librería ActiveSupport nos otorga varios métodos que nos facilitan múltiples tareas cómo:
- Fechas pasadas.
- Fechas a futuro.
- Tiempo actual.
- Tiempo en otros formatos.
Fechas Pasadas
[64] pry(main)> 1.day.ago.to_s => "2014-03-12 03:19:57 +0100" [65] pry(main)> 2.hours.ago.to_s => "2014-03-13 01:20:14 +0100"1234 [64]pry(main)>1.day.ago.to_s=>"2014-03-12 03:19:57 +0100"[65]pry(main)>2.hours.ago.to_s=>"2014-03-13 01:20:14 +0100"
El método ago
en conjunto con la fecha nos permite tener una fecha tentativa pasada sea un mes month
, semana week
, día day
u hora hours
.
Fechas a futuro
[66] pry(main)> 3.days.from_now.to_s => "2014-03-16 03:20:32 +0100" [67] pry(main)> 2.weeks.from_now.to_s => "2014-03-27 03:30:18 +0100"1234 [66]pry(main)>3.days.from_now.to_s=>"2014-03-16 03:20:32 +0100"[67]pry(main)>2.weeks.from_now.to_s=>"2014-03-27 03:30:18 +0100"
El método from_now
al igual que el ago
nos permite tener fechas en tiempo futuro sea por año, mes, dia u hora. Este es una función o método que podemos utilizar para crear fechas de caducidad en nuestra aplicación.
Tiempo Actual
[69] pry(main)> Time.current.to_s => "2014-03-13 03:21:08 +0100"12 [69]pry(main)>Timecurrentto_s=>"2014-03-13 03:21:08 +0100"
Este método podríamos decir que es una manera “resumida” de escribir Time.zone.now.to_s
.
Fecha Actual
[68] pry(main)> Date.current.to_s => "2014-03-13" [70] pry(main)> Time.zone.today.to_s => "2014-03-13"1234 [68]pry(main)>Datecurrentto_s=>"2014-03-13"[70]pry(main)>Timezonetodayto_s=>"2014-03-13"
Utilizando el método current
sobre la clase Date
y utilizando Time.zone.today
nos retorna la fecha con el formato año/mes/día de la fecha actual.
Tiempo en otros contactos
[71] pry(main)> Time.zone.now.utc.iso8601.to_s => "2014-03-13T02:21:45Z"12 [71]pry(main)>Timezonenowutciso8601to_s=>"2014-03-13T02:21:45Z"
En este caso utilizamos dos (2) tipos de formatos directamente en la consulta. Aplicamos el huso horario utc
y luego el formato iso8601
que es un standard utilizado en la mayoría de las bases de datos.
Querying
Para utilizar husos horarios dentro de los queries y preguntar si una venta fue creada antes o después de una fecha podemos realizar lo siguiente:
Primero agregamos un objeto en base de datos.
ale.create(precio: '40', nombre: 'mouse') (0.1ms) begin transaction SQL (4.1ms) INSERT INTO "sales" ("created_at", "nombre", "precio", "updated_at") VALUES (?, ?, ?, ?) [["created_at", Thu, 13 Mar 2014 03:10:07 UTC +00:00], ["nombre", "mouse"], ["precio", "40"], ["updated_at", Thu, 13 Mar 2014 03:10:07 UTC +00:00]] (0.5ms) commit transaction => #<Sale id: 1, precio: "40", nombre: "mouse", created_at: "2014-03-13 03:10:07", updated_at: "2014-03-13 03:10:07">12345 alecreate(precio:'40',nombre:'mouse') (0.1ms) begintransaction SQL(4.1ms) INSERTINTO"sales"("created_at","nombre","precio","updated_at")VALUES(?,?,?,?) [["created_at",Thu,13Mar201403:10:07UTC+00:00],["nombre","mouse"],["precio","40"],["updated_at",Thu,13Mar201403:10:07UTC+00:00]] (0.5ms) committransaction=>#<="" 1,="">
Segundo podemos preguntar si una venta se a realizado posterior de la fecha actual y luego anterior a la fecha actual.
irb(main):009:0> s = Sale.where(["created_at > ?", Time.zone.now]) Sale Load (0.2ms) SELECT "sales".* FROM "sales" WHERE (created_at > '2014-03-13 03:11:17.960605') => #<ActiveRecord::Relation []> irb(main):010:0> s = Sale.where(["created_at < ?", Time.zone.now]) Sale Load (0.1ms) SELECT "sales".* FROM "sales" WHERE (created_at < '2014-03-13 03:11:25.425127') => #<ActiveRecord::Relation [#<Sale id: 1, precio: "40", nombre: "mouse", created_at: "2014-03-13 03:10:07", updated_at: "2014-03-13 03:10:07">]>123456 irb(main):009:0>s=Salewhere(["created_at > ?",Timezonenow]) SaleLoad(0.2ms) SELECT"sales"*FROM"sales"WHERE(created_at>'2014-03-13 03:11:17.960605')=>#<="" span="">irb(main):010:0>s=Salewhere(["created_at < ?",Timezonenow]) SaleLoad(0.1ms) SELECT"sales"*FROM"sales"WHERE(created_at<'2014-03-13 03:11:25.425127')=>#<="" created_at:="" updated_at:="" span="" [#
Tambien podemos preguntar si se creó con fechas tentativas:
irb(main):011:0> s = Sale.where(["created_at < ?", 2.weeks.ago.to_s]) Sale Load (0.2ms) SELECT “sales”.* FROM “sales” WHERE (created_at < ’2014-02-27 03:18:38 UTC’) => # irb(main):012:0> s = Sale.where(["created_at < ?", 2.weeks.from_now.to_s]) Sale Load (0.1ms) SELECT “sales”.* FROM “sales” WHERE (created_at < ’2014-03-27 03:18:48 UTC’) => #]>
Una recomendación que hago es que cuando vayan a preguntar en un query con una fecha utilicen siempre los métodos de ActiveSupport.
El deber ser en un api
Si nuestra aplicación tiene un API o funciona mediante un API, se recomienda SIEMPRE como buena práctica enviar la información al cliente en el huso horario UTC +00:00 y dejar en manos de el cliente transformar la información en el formato que requiera el usuario. Por otra parte también se recomienda utilizar el formato iso8601
que mostramos anteriormente.
Pueden leer sobre esto aquí.
Conclusión.
En esta lección aprendimos a utilizar una propiedad más de activesupport
particularmente Time Zones o husos horarios que nos permiten tener aplicaciones que se comporten correctamente a nivel mundial, como podemos realizar consultas con time zones y además las buenas prácticas de el uso de las mismas. Siéntanse libres en consultar cualquier duda a través de los comentarios.
¡Hasta el próximo capítulo!