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 .

Escribe un comentario:



21 comentarios for “Zonas horarias y PHP”

  1. Paco dice:

    Y todo esto como se coloca?
    Que yo apenas sabía php y viendo esto ya puedo decir que no se nada xD
    Me podrías decir si poniendo el primer quote solamente te diria si madrid tiene horario de invierno o de verano?

    Gracias

  2. Paco dice:

    Ya lo encontré!

    Cambiando Madrid por la opción de cada usuario.

    Gracias igualmente, te agrego a favoritos que igual me sirve en un futuro^^

  3. El cambio de horario de verano produce tantos quebraderos de cabeza que creo que deberían suprimirlo de inmediato. Cuando de trata de convertir una hora GMT a local no hay problema, proque el cambio de hora en invierno o verano se produce siempre a la misma hora GMT, las 01:00. Pero en la conversión inversa (local a GMT) esto no es así: centrándonos en el huso horario de la península (GMT+1) el cambio de horario de invierno a verano se produce a las 02:00 local, y el de verano a invierno a las 03:00 local. Este último con el agravante de que ese día, como se retrasa una hora (a las 03:00 son las 02:00) hay horas locales que se repiten en el día, es decir, el intervalo de 02:00 a 02:59 se repite dos veces: en la primera pasada por las 02:00 local, la hora GMT es 00:00, mientras que en la segunda pasada por las 02:00 local, la hora GMT es 01:00. Esto hace imposible determinar la hora GMT exacta correspondiente a las 02:00 horas locales del día de cambio de horario de verano a invierno (último domingo de octubre).

  4. shaggylish dice:

    Hola, yo tuve un problema parecido hace algún tiempo que solucioné en mi web.

    Os pongo el artículo donde lo explico aquí:
    http://shaggylish.co.cc/89/zonas-horarias-con-php-y-javascript.html

    Como resumen os cuento que uso php y javascript.

    Todo el código es totalmente libre para que lo copieis y probeis.

    Un saludo

  5. Daniel dice:

    Que kilombo! jaja, y encima aca ahora hoy dos horas distintas en el país (Argentina), yo creo que estaria bueno también que el usuario elija qué hora es en donde está ahora (y que lo pueda cambiar luego), si es que no es algo muy critico el programa claro… y de ahi se va procesando, yo ultimamente me re pierdo… mando un mail o un comentario y sale cualquier hora jaja… no se si la toman de mi compu, o la del servidor, o dependiendo de mi zona horaria, pero no todos los sitios dan a elegir la zona horaria… porque encima como cambio de hora en Bs. As. y acá no, entonces sigo eligiendo Bs. As. pero ellos elegiran otra cosa…

    la verdad que me pierdo

  6. Daniel dice:

    en mi comentario anterior salio que lo escribi a las 5:56 pm, mi compu tenia 14:56 (se me cambia sola la hora, y eso que desactivé que se sincronice), pero en realidad eran las 13:56 xD (y encima técnicamente no debería ser esa hora)… bueno tampoco digo que haya que ser tan exactos que hasta en un comentario salga bien la hora para todos… pero que es un problema el tema del horario, lo es :|

  7. bernabe dice:

    Respuesta a Daniel:

    Que aquí tus comentarios salgan con una hora disinta a la tuya no tiene realmente importancia, lo que pasa es que tenemos configurado wordpress con la GMT+1, hora de España, que es donde estamos actualmente.

    Fue realmente dificil para nosotros conseguir este método para que nuestra página de sorteos avanzados (http://www.sortea2.com/sorteos-avanzados) programase los sorteos bien. Lo que más problemas daba era que un sorteo fuera programado y que hubiese un cambio de horario de verano/invierno por medio, cosa que hacía que ya se liase todo. De esta manera se toman todas las horas correctamente.

    Desde la página de sorteos avanzados se permite seleccionar la zona horaria que mejor convenga al usuario, para que el sorteo se haga en la hora de ese usuario.

  8. bernabe dice:

    Respuesta a shaggylish:

    Tu sistema está muy bien, hace que se detecte la zona horaria automáticamente sin necesidad de que el usuario tenga que elegirla.

    Lo que pasa es que tu sistema solo serviría para obtener la zona horaria, no sirve para poder programar tareas en el futuro ni nada de eso.

    Independientemente, creo que ambos sistemas se complementarían bastante bien y se agradece tu aportación.

    Saludos.

  9. […] El gestionar zonas horarias cada vez es de lo más simple, gracias a barrapunto.com, descubro que existe una forma sencilla de realizar la obtención, manipulación, formateo de las zonas horarias de una manera muy sencilla. La información desde el blog de Sortea2. [Enlace] […]

  10. SauronZ dice:

    Hola!
    Ante todo gracias por compartir la info.

    Solo tengo un problem.

    La clase datetimezone es una clase creada por vosotros?

    Lo digo porque no la veo como clase de php.

    Es de PEAR o de algun otro repositorio?

    En caso de ser vuestra, podrias compartirla?

    Un saludo y gracias.

  11. Daniel dice:

    si no obvio la hora de un comentario tampoco es muy importante… gracias por la info ;)
    no uso php pero me interesó el tema a ver si puedo llevar un poco la lógica a otros lenguajes o encontrar clases similares

  12. pacob dice:

    La clase datetimezone viene a partir de PHP 5.2. Aquí tienes información sobre dicha clase para ser usada en versiones inferiores de PHP.

  13. […] – Zonas horarias y php […]

  14. jeison dice:

    hola!! quisiera saber de que forma utilizan esta solución, veo que tienen una tabla timezone pero no me queda claro de que manera la utilizan, si pudieran describir el proceso de la solución se los agradeceria.

    Gracias

    Jeison

  15. Jeison dice:

    hola!! quisiera saber como validan que el usuario no seleccione una hora que ya paso en una zona horaria determinada, me explico a continiación con el sgute ejemplo:

    El usuario que realiza el sorteo se encuentra en Bogota (Colombia) y va a realizar el sorteo para usuarios que se encuentran en madrid, la diferencia horaria es de +6 horas respecto a Bogota, para el ejemplo tomemos la fecha 09/03/2009

    el usuario en bogota tiene hora de las 12m, en madrid son las 6pm, este usuario de bogotá asigna hora del sorteo 3 pm del 09/03/2009 para la zona horaria de Madrid, cuando las 3pm hora de madrid ya han pasado, ya madrid tiene las 6pm, luego el usuario asigno un sorteo en una hora que ya pertenece al pasado. algunos pueden pensar que es responsabilidad del usuario que asigna el sorteo fijarse de que sea en una hora valida. Pero la experiencia demuestra q los pasos obvios para el desarrollador no lo son para los usuarios. Si se les ocurre la forma de validar esto les agraadesco que lo compartan.

    gracias
    Jeison

  16. pacob dice:

    Lo que hacemos no es validar las fechas en el estilo tradicional, sino respecto de GMT + 0. Se introduce una hora + zona horaria y el par se convierte a GMT + 0.
    Es sobre esa hora sobre la que se comprueba si ya ha pasado o no.
    Como indicamos aquí, no miramos para nada la hora del sistema o la zona horaria de la IP que realiza el acceso, es la selección del usuario lo que cuenta.

    Espero que te sirva Jeison.

  17. Jeison dice:

    hola, pacob gracias por tu respuesta, ya estoy implementando la solución y tengo en cuenta tu sugerencia.

    Jeison

  18. damian zoanni dice:

    function convert($serverTime, $userOffset) {
    $serverOffset = date(‘Z’, time()); $gmtTime = $serverTime – $serverOffset;
    $userTime = $gmtTime + $userOffset * (60*60);
    return $userTime;
    }
    //venezuela (GMT-04:30)
    $ahora = date(«H:i», $this->convert( date(‘U’) , ‘-4.5’) );
    echo $ahora;

  19. Naitsir dice:

    muchas gracias por el post, una gran ayuda para realizar conversiones entre zonas horarias, pero ahora tengo una enorme duda!!!

    la lista que muestras con los países y los pares continente/país de PHP es insuficiente para lo que necesito :( me podrías dar alguna sugerencia como puedo obtener los países asociados a estos pares???
    por ejemplo, tengo estos países y ciudades: Spain, Spain – Canary Islands, Svalbard and Jan Mayen, Sweden, Switzerland, etc, etc. cómo puedo saber a qué par de PHP se asocian??

    y tengo otra duda, haciendo pruebas me extraña como se comportan algunas horas. por ejemplo para los países q se encuentran en GMT-3 tenemos los pares America/Buenos_Aires, America/Bahia, America/Santiago (según la lista que nos muestras), al pertenecer los 3 a GMT-3 no deberían devolverme la misma hora?? me devuelve horas diferentes, y lo mas extraño es que America/Bahia, me devuelve la misma hora que America/La_Paz. Estoy un poco confundido con esto :S

  20. pacob dice:

    Damian, las horas entre ciudades en la misma zona horaria pueden diferir si algunas tienen el cambio de hora «para ahorrar energía» y otras no, o si unas lo adoptan antes que otras. Es por eso que se establecen estas distinciones entre ciudades, a veces sin que haya diferencias reales.

    En Europa está claro, en la occidental casi todas comparten horario pero hay uno «diferente» para Madrid, París, Berlín y Copenhague, cuando en realidad siempre tienen el mismo horario.

    Para obtener listados completos de países (en nuestra página hemos hecho una selección o el desplegable sería enorme) lo mejor es la propia página de PHP: Lista de zonas horarias soportadas por PHP

  21. Bernardo dice:

    $ Fechaserver = time(); $fecha= $fechaserver – 14400; Esto resta las 4 horas que tengo de diferencia cone l servidor.
    Medio arcaico pero me funciona perfecto.

    Espero sirva.