Cuando escribimos nuestra página de sorteos consideramos que sería útil que los usuarios pudieran realizar un sorteo a una fecha y hora determinadas. Por si desean darle un poco de emoción o dar claridad a un sorteo. No es lo mismo que tú publiques unos resultados sino que digas «en esta página ofrecerán los resultados». Es una forma de mostrar que no influyes en el resultado.
Al querer realizar los sorteos a una hora concreta detectamos que era problemática la situación de cara a los usuarios que no usasen la misma zona horaria que nosotros (que es la de España). Una solución sería dar todas las fechas con nuestro horario. Es exigir a los clientes que se adapten a nuestras condiciones. Aunque hay muchos que optan por ella, me parece inadmisible.
Nuestra primera idea fue la de mostrar un desplegable con zonas horarias, mediante una función sencilla:
Se muestra una zona horaria e internamente se guarda un valor numérico: el número de horas de diferencia respecto de la hora de Madrid.
Lisboa(-1)
Madrid(0)
Canarias(-1)
Nueva York(-6)
México DF (-7)
Esta aproximación es una de las más frecuentes. Es sencilla y fácil de tratar. Cuando un usuario de México solicita que su sorteo sea a las 13:00 horas, convertimos el horario al de España (sumamos 7 horas) y lo guardamos (20:00 horas). Luego cuando ese usuario decida consultar el sorteo, volvemos a realizar la conversión inversa, restando siete horas.
Sin embargo la vida no es tan fácil. Quizás el mayor de los problemas es la maligna DST (Daylight Saving Time, Horario de verano en castellano).
Porque cuando nos acercamos a estos días en que se suma o resta una hora, los resultados pueden ser impredecibles.
a) Hay países que no cambian nunca de horario (sabia decisión). Entonces respecto a ellos habrá veces que España tenga X horas de diferencia y el resto del año X +1 horas.
b) Hay países que cambian de horario en fechas diferentes, como sucede con Estados Unidos. No se puede ignorar a Estados Unidos, que tiene más usuarios de castellano que la propia España.
Estados Unidos es aún más complejo por cuanto las elecciones presidenciales pueden cambiar la fecha en que se realice el cambio de hora. Además, tiene potestad para realizar cambios en el día seleccionado para la modificación horaria, sabemos cuando cambiarán el año que viene, pero dentro de tres años puede cambiar de sistema (ya lo han hecho varias veces). Vamos, que es todo un caso a tratar por separado.
c) Hay países que incluso cambian la regla. Muchos países decidieron dejar de cambiar de hora hace algún tiempo. De cara al futuro no es problemático pero si queremos hacer una correcta conversión de fechas, su situación debe ser tenida en cuenta.
d) Hay países que no tienen zonas horarias exactas. Ya sea porque tengan diferencias de cuarto de hora respecto de una zona (como Nepal, +5:45) o Venezuela (-4:30). Además, el caso de Venezuela, en que dicho cambio de zona horaria es muy actual, es problemático para fechas del pasado reciente.
Por todas estas razones, el método de sumar o restar horas, sencillamente no sirve.
Para nuestro caso, al estar desarrollada la página con PHP, tras buscar soluciones posibles por Internet, sin mucho éxito y tratando de hacer alguna cosa bien, optamos por mirar algunas funciones propias de PHP.
Las funciones propias de PHP para zonas horarias en la versión actual (4.4) son las siguientes:
# timezone_abbreviations_list
# timezone_identifiers_list
# timezone_name_from_abbr
# timezone_name_get
# timezone_offset_get
# timezone_open
# timezone_transitions_get
No creemos que sea necesario dar una enumeración de las mismas, máxime cuando se puede consultar la completa documentación online. Lo importante es saber que la forma en que PHP guarda las zonas horarias. Aunque hay posibles conversiones de texto (siempre más fáciles si el texto está en inglés), lo suyo es usar los pares Continente/País que presenta PHP en su lista de zonas horarias soportadas.
Por ejemplo:
Europe/Madrid
America/Mexico_City
America/Buenos_Aires
Atlantic/Canary
La lista de zonas horarias admitidas es extensísima. En muchos casos estas zonas horarias son idénticas, como sucede con Madrid, Berlín o París. De ahí que no tenga mucho sentido mostrar desplegables casi infinitos en sus posibilidades (sólo para la Antártida PHP soporta ¡Ocho zonas horarias!).
Se realiza una selección de países y ciudades representativas. Lo ideal es mostrar junto a dicha ciudad la zona horaria correspondiente para que así un usuario de un lugar recóndito pueda localizar la zona que mejor se adapte a su ubicación:
Desplegable zonas horarias
Por lo tanto, trabajamos con tres valores:
a) El código de zona horaria de PHP: America/Buenos_Aires
b) La descripción para seres humanos de dicha zona: Buenos Aires
c) Un indicativo para usuarios que no encajen exactamente con dicho país: GMT -3
Esto ya sugiere que crear una tabla donde se guarde la información de las zonas horarias puede ser de gran ayuda.
Si te interesa la que usamos en nuestra página, puedes descargártela de aquí, en formato de creación para MySQL (fácilmente adaptable a otras bases de datos).
Antes de continuar una buena tarea sobre la que meditar es cómo vamos a guardar las fechas y horas en nuestra base de datos. Si nos centramos en la ciudad en que vivimos, nos podemos encontrar con que el servidor que alberga nuestra página esté ubicado en otra zona horaria. Cierto es que a veces podemos manipular esta hora, pero en hostings compartidos, a veces esto no es posible.
Así, uno puede encontrase con que:
a) Su zona horaria es de Madrid, España.
b) Quiere guardar una fecha y hora de Canarias, España.
c) El servidor está situado en Los Ángeles, Estados Unidos.
Con lo cual los problemas están casi garantizados.
Tampoco hay que volverse loco ante las diferencias de fechas. Salvo que tengas una situación tan especial como la nuestra, en que necesitamos programar tareas futuras, quizás puedas sobrevivir sin problemas y sin saber nada de todo esto. Pero el conocimiento siempre es útil.
En las tablas de datos es común guardar un registro con la fecha y hora de la última modificación. ¿Debemos convertir esa fecha y hora?
Pues salvo que sean tareas críticas, probablemente no. Las fechas de creación de registro bien se pueden quedar en el horario que tenga el servidor. Sólo aquellas que requieran de ser mostradas en pantalla pueden y deben ser tratadas con mayor consideración. En el caso de sortea2.com, las horas a las que se programan los sorteos. Para ellas no sólo se guardará la fecha y hora, sino también la zona horaria en que se realizaron.
Por lo tanto, por sentido común, no es muy lógico guardarlas en una horario tan poco estándar como el de Madrid. Hay dos opciones principales:
Guardar la hora y decir «son las 14:30 con el horario de Buenos Aires».
Convertirla al estandar GMT (Hora de Greenwich, Inglaterra) e indicar la zona horaria deseada. «son las 16:30, zona horaria preferida Buenos Aires».
Esta segunda opción nos parece mejor porque permite en un momento dado comparar fechas de inmediato. Del otro modo casi siempre habrá que hacer una conversión antes de poder continuar.
Como resumen: se guardan las horas en estandar GMT, sólo en los casos en que no sean necesarias manipulaciones posteriores.
Tras planear el desarrollo de la página, sólo queda un problema: cómo convertir entre dos zonas horarias dadas. No sé cómo lo habrán desarrollado en otros lenguajes de programación, pero en PHP el método es realmente ingenioso y complicado.
Vaya por delante que en internet hay numerosas clases realizadas por otros que explican cómo realizar estos cambios. Hay incluso clases hechas en PEAR que se supone son totalmente estándar. Tras la nefasta experiencia de usar una de ellas y descubrir que estaba mal (no distinguía los cambios de hora), decidí seguir el camino difícil y propio.
Siempre es más fácil usar algo ya hecho y que funcione. Y uno puede vivir dignamente sin conocer las transiciones de PHP.
Pero si quieres continuar, las transiciones son un array de fechas correspondiente a cada una de las zonas horarias. Es un array monstruoso en que se muestran los días del año en que se produce un cambio de hora para dicha zona horaria. Por ejemplo, para la zona horaria Europe/Madrid dice:
( [0] => Array ( [ts] => -1661734800 [time] => 1917-05-05T23:00:00+0000 [offset] => 3600 [isdst] => 1 [abbr] => WEST )
Que traducido al cristiano es algo así como:
- A la fecha y hora UNIX (segundos desde el 1 de enero de 1970) (correspondiente a GMT, hora estandar de Greenwich) es la de -1661734800).
- Que se corresponde con la fecha y hora «en inglés» de 1917-05-05 23:00:00+0000.
- Se produjo un cambio de hora que deja a la hora de Madrid respecto de la de GMT en 3600 segundos (una hora más).
- Dejando la zona horaria en GMT+1.
- Quedando en la zona horaria la WEST.
El listado de transiciones de la zona horaria de Madrid nos avisa de cada cambio de hora que se ha producido en la historia, desde el verano de 1917 hasta el año 2037. Si se necesita tratar fechas anteriores o posteriores, aparte de considerar la opción del suicidio, habría que tirar de enciclopedias y realizar un tedioso trabajo manual.
Este extensísimo array es la pieza fundamental para realizar conversiones entre zonas horarias en PHP.
Veamos por ahora cómo obtener el fichero de transiciones para una zona horaria:
$timezone = «Europe/Madrid»;
$timezoneinfo = new DateTimeZone($timezone);
$arraytime = $timezoneinfo->getTransitions();
En la primera línea definimos la zona horaria a tratar, según las zonas horarias que PHP entiende.
En la segunda creamos un objeto de tipo zona horaria, relativo a dicha zona inicial.
En la tercera obtenemos las transiciones: cuándo se produjeron los cambios de hora respecto de GMT para esa zona horaria.
Es curioso ver el fichero de transiciones. Ahora solemos cambiar de hora los sábados de 2:00 am a 3:00 am, pero en 1917 decidieron hacerlo a las 0:00 horas. Se nota que se trasnochaba menos por aquel entonces. Durante la II Guerra Mundial hubo cambios de hora a las 11:00 de la noche. Luego se volvió a las 12:00 y en los ochenta se pasó a hacerlo a la 1:00 am.
Como podemos ver, y evitemos dispersarnos, los cambios de hora son una verdadera pesadilla en lo que al horario de verano se refiere.
Con el array de transiciones ya es más o menos fácil. Si tenemos una fecha en formato GMT, junto con una zona horaria deseada y deseamos convertirla a la hora propiamente dicha, basta con:
Tomamos la hora en GMT y recorremos el array de transiciones, hasta que encontramos una hora superior a la nuestra.
Por ejemplo, si tenemos las 20:00 del 15 de Enero de 2012, y el horario deseado es el de Madrid, vemos que el último cambio de horas se produce el 30 de octubre del 2011. Y que entonces España tendrá una hora más que la hora GMT. Esto sucederá así hasta el 25 de Marzo del 2012, por lo que efectivamente, si queremos convertir esa hora en GMT a hora de Madrid, debemos sumarle una hora (3600 segundos) más.
[110] => Array ( [ts] => 1319936400 [time] => 2011-10-30T01:00:00+0000 [offset] => 3600 [isdst] => [abbr] => CET )
[111] => Array ( [ts] => 1332637200 [time] => 2012-03-25T01:00:00+0000 [offset] => 7200 [isdst] => 1 [abbr] => CEST )
Este proceso es mucho más delicado si la hora está en el mismo borde del cambio de hora. Por ejemplo, si queremos convertir las 01:00 horas del 25 de marzo de 2012, de horario GMT al horario de Madrid.
Para afinar completamente la regla es la siguiente:
- Partimos de una hora.
- Recorremos las transiciones hasta pasarnos de dicha hora.
- Miramos el registro anterior. Obtenemos la diferencia de segundos.
- Se la sumamos a la hora que teníamos. Si nos hemos pasado de la siguiente transición, tomamos esa siguiente, sino (lo habitual) nos quedamos con la que teníamos.
La verdad es que esto se entiende mejor con el código que escrito con palabras:
$timezoneinfo =
new DateTimeZone($timezone);
$arraytime = $timezoneinfo
->getTransitions();
$i = –1;
foreach ($arraytime as $transicion)
{
$i ++;
if ($transicion[ts] + $arraytime[$i][offset] >= $gmttime)
{
$indice = $i –1;
break;
}
}
$newtime = $gmttime + $arraytime[$indice][offset];
Nos recorremos el array de transiciones. Cuando la transición + las horas de diferencia sean mayores que nuestra fecha de partida, quiere decir que hemos llegado y es el momento de tomar ese valor, sumarlo a la fecha que teníamos y voilá, ya hemos convertido la fecha.
Básicamente ya hemos expuesto todo el proceso. Lo único necesario son dos funciones: una que parta de una zona horaria y una hora expresada en dicha zona horaria y la transforme en GMT y otra que partiendo de una hora en GMT y una zona horaria, realice la transformación inversa.
Pueden servir las dos siguientes funciones:
function of_gmttolocaltime ($gmttime, $timezone)
{
$timezoneinfo =
new DateTimeZone($timezone);
$arraytime = $timezoneinfo
->getTransitions();
$i = –1;
foreach ($arraytime as $transicion)
{
$i ++;
if ($transicion[ts] + $arraytime[$i][offset] >= $gmttime)
{
$indice = $i –1;
break;
}
}
$newtime = $gmttime + $arraytime[$indice][offset];
return $newtime;
}
/**
* Entrada. Una hora en formato UNIX de una zona horaria, y el nombre de la zona horaria.
* Salida. La hora convertida a GMT.
*/
function of_localtimetogmt ($localtime, $timezone)
{
$timezoneinfo =
new DateTimeZone($timezone);
$arraytime = $timezoneinfo->getTransitions();
$i = –1;
foreach ($arraytime as $transicion)
{
$i ++;
if ($transicion[ts] – $arraytime[$i][offset]>= $localtime)
{
$indice = $i –1;
break;
}
}
$newtime = $localtime – $arraytime[$indice][offset];
return $newtime;
}
Las horas siempre se expresan en horario UNIX. ¡Bastante quebradero de cabeza dan las zonas horarias para sufrir aún más con los formatos locales de fecha y hora!
Creo que ha quedado todo bien expresado pero si tienes alguna duda o aclaración déjala en los comentarios. Las correcciones también son bienvenidas.
Actualización. Tras dos años usando este sistema hemos visto bastantes claroscuros que exponemos en esta otra entrada: problemas con las zonas horarias y PHP.
.