Posts con la categoría ‘PHP’

Zonas horarias con PHP

Escrito en PHP, Tutoriales

Hace un par de años publicamos un artículo titulado Zonas horarias y PHP en el que explicábamos los entresijos del sistema que habíamos empleado para la programación de los sorteos que se realicen fuera de España.

En ese artículo se narra un mecanismo que proporciona el lenguaje de PHP para realizar los cambios de horario de forma eficaz y abarcando todas las opciones posibles. Sin embargo recientemente tuvimos que revisar todo el sistema porque había muchas quejas de usuarios que tenían problemas con sus sorteos programados que se realizaban a horas diferentes a las que ellos habían definido.

Al analizar la situación más en detalle pudimos ver que hay bastantes problemas en el sistema que indicamos en el artículo «Zonas horarias y PHP» que no tienen fácil solución.

Por un lado está el problema de que todo el empaquetado de zonas horarias y fechas de cambio está dentro de la instalación de PHP. Si un gobierno decide cambiar esa fecha, como ha sucedido recientemente con Argentina, la información de cambio está obsoleta y es sencillamente equivocada. PHP soluciona el problema creando una modificación a sus programas pero esa modificación se incluye en la versión más moderna de todas.

Ahora bien, uno no puede actualizarse la versión de PHP cada día. Sobre todo si se tiene la página en un hosting compartido, la actualización de versión la dictan los proveedores del servicio, uno no puede realizar dicho cambio. En nuestro caso, tenemos que vivir con una versión desfasada que tiene errores en las conversiones de hora para sorteos de Argentina y hemos tenido que implementar soluciones alternativas un poco artificiales. Oficialmente con la versión de PHP instalada, cuando queremos saber la hora actual de Buenos Aires no da una que está equivocada en dos horas.

Hace unas semanas se produjo el cambio de hora y también pudimos ver cómo horas antes de dicho cambio cuando se consultaba la hora estándar del sistema (mediante time(), tras establecer la zona horaria), la UTC (antigua de Greenwich) el sistema estaba dando un dato erróneo en una hora.

En conclusión, dar una actualización a la opinión que expresábamos hace dos años. No se puede establecer un sistema de cambios horarios que sea eficaz al 100% mediante PHP. La única opción es actualizar las versiones casi de continuo o crear un sistema artesanal donde ir indicando las diferencias horarias. El problema no es exclusivo de PHP y la culpa no es de ellos sino de muchos gobiernos que arbitrariamente modifican los políticas sobre diferencias horarias, no se ciñen a lo previsto años antes sino que realizan muchos cambios con efecto inmediato. En algunos casos también hay medidas de tipo político, como algunas regiones que pretenden distinguirse del resto del país con un cambio de zona horaria. Estos cambios no se reflejan automáticamente en las funciones de PHP.


Escrito por .

Zonas horarias y PHP

Escrito en PHP, Tutoriales

Introducción. Métodos sencillos que no funcionan

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.


Zonas horarias y PHP

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:

    Buenos Aires (GMT -3)

    La Paz (GMT -4)
    Caracas (GMT -4.5)
    Montreal (GMT -5)

Desplegable zonas horarias

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).


Zonas horarias. ¿Qué convertimos?

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.


Zonas horarias y PHP. Las transiciones.

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.

Zonas horarias y PHP. Funciones de conversión.

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.


Escrito por .