El control del tiempo es una parte importante de la programación en muchas aplicaciones actuales. Desde la detección del cumpleaños de nuestros usuarios, hasta la medida del tiempo de reacción ante un estímulo en investigación, pasando por la monitorización de un sistema. Estas son algunas de las aplicaciones que podremos encontrar, aunque no las únicas. En esta serie de posts quiero hacer un recorrido por algunas necesidades básicas de tiempo que podemos tener en nuestros programas en lenguaje C desde un punto de vista práctico, y desde cero.
Nota: Aunque muchas de las cosas que podréis ver aquí están enfocadas a la multiplataforma, algunas serán sólo para sistemas POSIX por lo que sistemas operativos como Windows no podrán ejecutar dichos programas. De todas formas, lo especificaré.
He querido recopilar aquí funciones, llamadas a sistema y estructuras necesarias para diferentes tareas, con ejemplos detallados. Tal vez algunas sean cuestiones triviales para muchos, tal vez otras no lo sean tanto.
Necesidades básicas de tiempo
Aquí reúno las que yo creo que son necesidades básicas y cuestiones interesantes a tratar cuando hablamos de tiempo en nuestros programas.
Conceptos previos
En las siguientes líneas veremos algunas definiciones interesantes que nos ayudarán a comprender el resto del post.
Unix Epoch
Unix Epoch, o época Unix, es una fecha de referencia utilizada en sistemas informáticos. Esta fecha data el 1 de Enero de 1970 a las 00:00:00. Igual que el calendario gregoriano que solemos utilizar toma su referencia en el nacimiento de Cristo.
Y al igual que existen fechas Después de Cristo (D.C., o Anno Domini A.D., también B.C, before Christ, en inglés) Antes de Cristo (A.C., o ante Christum natum), si usamos Unix Epoch como referencia, podemos utilizar valores positivos para fechas después de nuestra referencia y negativos para fechas anteriores.
Normalmente, para representar una fecha u hora, utilizaremos el número de segundos transcurridos desde ese momento (en otros sistemas se usan milésimas de segundo o 1/60s, el caso es hacer incrementos fijos en intervalos fijos), por lo que, al estar todo codificado en un número podremos operar con él de forma sencilla (computacionalmente hablando). Por ejemplo, si tuviéramos día, mes, año, hora, minuto y segundo, pasado un segundo del 31/12/2016 a las 23:59:59 tendríamos el 01/01/2017 a las 00:00:00 es decir cambian todos los números, y tendríamos que establecer muchas condiciones (que deberíamos ejecutar cada segundo). En lugar de eso, al tener sólo un número, tendríamos 1483228799 y pasaría a 1483228800, una operación mucho más simple. Por otro lado, podríamos hacer una conversión a varios tipos de calendario, cambiar de zona horaria o calcular fácilmente diferencia de fechas, por poner algunos ejemplos. Este valor, lo conoceremos como Tiempo Unix.
Longitud de palabra
Con esto me quiero referir al tamaño de la variable utilizada para almacenar el tiempo unix. Dado que contar segundos durante mucho tiempo nos puede dar números muy grandes, necesitamos un tamaño lo suficientemente grande como para cubrir nuestras necesidades. Por ejemplo, si tenemos una variable de 16bit y queremos almacenar segundos, como mucho podremos almacenar 65535 segundos y 1 día tiene 86400 segundos, por lo que no podemos llegar muy lejos. Aunque utilizando 32bits, tenemos unos 136 años de margen, y podemos habilitar fechas anteriores y posteriores (dejando 68 años para anteriores y 68 años para posteriores), aunque si recordamos que nuestra referencia es el Unix Epoch (1/1/1970), 68 años más tarde será el 2038 y esa fecha, tendremos un problema, algo así como el famoso efecto 2000, aunque puede que casi todo ya esté basado en una palabra de 64bits.
Conocer la fecha y hora actuales
Empezamos con un ejemplo muy sencillo, tan sencillo que algunos me vais a matar, vamos a sacar el tiempo Unix por pantalla:
Aunque esto nos dice poco, sólo un número de segundos. Ahora nos toca hacer cuentas, para averiguar día, mes, año, hora, minuto y segundo. Aunque la biblioteca time, ya tiene utilidades para eso.
Primero veremos la forma más inmediata, y menos complicada, utilizando la función ctime().
Aunque, bueno, el día de la semana y el mes están en inglés y a lo mejor no queremos eso para nuestra aplicación. De todas formas, es una función rápida para representar rápido la fecha y hora actuales.
Desglosar la fecha y hora actuales
Ahora nos gustaría poder representar la fecha y la hora a nuestro gusto, con números o con letras, pero a nuestro gusto. Para ello, vamos a ver más herramientas que nos da la biblioteca time.
En concreto, una estructura llamada struct tm (recordemos que en C, a no ser que usemos typedef, los structs tienen apellido y nombre). De hecho, la estructura struct tm consta de lo siguiente:
- tm_sec : segundos, y no los segundos de la marca de tiempo Unix, sino segundos del 0 al 59 tal y como los conocemos, aunque puede llegar al 61 para permitir leap seconds.
- tm_min : minutos, desde 0 a 59.
- tm_hour : horas, desde 0 a 23.
- tm_mday : día del mes, desde el 1 al 31 (dependiendo del mes).
- tm_mon : el mes actual, siendo Enero el 0 y Diciembre el 11. Importante, si cogemos los datos de esta variable, tendremos que sumar 1 al mes. Aunque otras veces nos vendrá bien que empiecen en 0.
- tm_year : El año, a partir de 1900, por lo que 1900 será el año 0, de ahí en adelante. El 2016 será el 106.
- tm_wday : El día de la semana, siendo 0 el Domingo y 6 el Sábado. (wday = week day)
- tm_yday : El número de días desde el 1 de Enero, en un rango entre 0 y 365 (sería el 31 de diciembre de un año bisiesto). (yday = year day)
- tm_isdst : nos dice si en la zona horaria actual existe cambio de hora entre invierno y verano. (dst = daylight saving time)
Después de esta parrafada de manual, ¿ qué podemos hacer con esta estructura tan grande ? Es decir, ¿cómo transformamos una marca de tiempo Unix en una estructura así? Vamos a ver dos funciones para ahorrarnos hacer cuentas y conseguir nuestra fecha y hora desglosadas ( nota: para los ejemplos, vamos a acceder directamente a los elementos del struct tm, así podemos ver algo):
localtime()
Recibe un puntero a una marca de tiempo, y nos devuelve un puntero a un struct tm, centrado en la zona horaria actual. Es decir, nos dará la hora local.
gmtime()
Y lo mismo que sacamos la hora desglosada local, podemos sacarla centrada en GMT. Del código anterior sólo variará la llamada a localtime() por gmtime(), podéis probarlo vosotros mismos.
Formatear la fecha y hora actuales
He preferido separar desglosar de formatear porque aquí vamos a hacer las cosas un poco más sencillas. Primero hablaremos de asctime(), esta función, a partir de una variable de tiempo desglosado (struct tm), escribirá en una cadena de caracteres la fecha y hora en el formato preferido del sistema (dependiendo del país del que seamos, o tengamos configurado):
Pero, sobre todo, vamos a hablar de la función strftime() que creará una cadena de caracteres con los datos que le digamos a partir de una estructura de tiempo desglosado (struct tm). Vamos a verlo con un pequeño ejemplo:
En definitiva, es como un printf(), en el que ponemos una cadena de formato "%d/%m/%Y %H:%M:%S" y cada una de esas palabras claves (caracteres precedidos de %) se transformará en el valor extraído de la estructura ahoralocal (nuestro struct tm).
Los primeros dos parámetros son la variable donde la vamos a meter, es decir, una cadena de caracteres y el tamaño de la misma. Esto es muy importante para que no ocurran desbordamientos ni comportamientos no deseados en nuestro programa.
Aunque, si estamos creando un programa en el que el usuario elige su propio formato de fecha, lo más normal será que no sepamos cuánto tamaño darle a la cadena de caracteres, y tampoco vamos a darle un tamaño muy grande para desaprovechar la memoria, pues bien, podemos hacer lo siguiente:
Es decir, esta función mystrftime() reservará dinámicamente espacio (inicialmente 10 bytes, pero podemos modificar TAMBASE para que sea más o menos), y si vemos que la cadena con la hora no cabe (strftime() devuelve 0), reserva 10bytes más y prueba otra vez.
TAMBASE deberá ser lo suficientemente grande como para no estar todo el tiempo en el bucle y lo suficientemente pequeño como para no desperdiciar memoria. Un valor coherente puede ser 128, nuestras cadenas generalmente serán mucho más pequeñas, y gastar 128 bytes no es tanto, bueno, si estamos en Arduino o similares sí. Un mal valor sería 1 porque si nuestra cadena es de 20 caracteres, tendría que dar 20 iteraciones lo cual gasta excesiva CPU en comparación con el ahorro de memoria que supone.
Cadenas de formato habituales
No voy a copiar y pegar aquí el manual, además, dependiendo de la biblioteca que estemos usando puede haber más o menos, pero pondré aquí las palabras clave más comunes (para mí):
- %d : día del mes, desde el 01 al 31. Además, en formato dos cifras, por lo que el día 1 será 01.
- %m : mes en formato numérico y en dos cifras, del 01 al 12
- %Y : año completo, las 4 cifras (1996, 2016, 2058...)
- %H : hora, en dos cifras, desde 00 a 23
- %M : minuto, en dos cifras, desde 00 a 59
- %S : segundo, en dos cifras, desde 00 a 61
- %F : Es lo mismo que %Y-%m-%d (fecha en formato ISO 8601)
- %T : Es lo mismo que %H:%M:%S
- %w : día de la semana, desde 0 (domingo) a 6 (sábado)
- %W : número de la semana con respecto al año
- %p : AM (Ante Meridiem) o PM (Post Meridiem)
- %a, %A : nombre del día de la semana abreviado y completo respectivamente
- %b, %B : nombre del mes abreviado y completo respectivamente
- %Z : zona horaria
Y si queremos escribir el símbolo %, tenemos que poner "%%". Todo esto nos puede dar mucho juego.
Formatear la fecha y hora actuales: localización
Como pudimos ver en la lista anterior, hay algunas palabras clave que, o se basan directamente en el sistema inglés o americano, o son dependientes de la localización del sistema. El caso más evidente es el nombre de los meses o los días de la semana (aunque también lo podemos ver en los texto AM, PM, en %c o representación de la fecha preferida del sistema y en otros casos más.
Como ejemplo, os remito al ejemplo anterior de strftime(), a representar "%a %d/%B/%Y %H:%M:%S", normalmente saldrá el día de la semana y el mes en inglés, lo cual puede estar bien en muchas ocasiones, pero en otras, nos puede complicar la vida. Ahora vamos a hacer lo siguiente:
Lo único nuevo es incluir locale.h y llamar a setlocale(). De hecho, con esta línea, lo que hacemos es definir la localización para tiempo (LC_TIME), a la localización por defecto del sistema. Si lo tenemos en español, veremos "lun 14/marzo/2016″, aunque podemos representarlo en cualquier idioma que tengamos instalado en el ordenador (es_ES.utf8, en_EN.utf8, de_DE.utf8, fr_FR.utf8...), en Linux podemos ver un listado de localizaciones instaladas ejecutando:
Aunque en ocasiones, no queremos, o no podemos usar una locale española o inglesa. Una española puede que no esté instalada en el sistema, y nuestro programa esté en español, y una inglesa nos puede servir para representar fecha y hora en algún formato estándar de fecha para hablar con un sistema remoto o con otro programa. Para eso tengo algunas funciones que pueden resultar útiles (no he inventado nada nuevo, pero a veces vienen bien, y las podéis completar con más tipos de datos, como abreviaturas de días y de meses):
Huso horario del sistema
El huso horario viene dado por la variable de entorno TZ y si no está definida, coge esa variable de la configuración de nuestro sistema. Si queremos saber nuestra zona horaria, tenemos que centrarnos en tres variables globales:
- tzname, que nos devolverá el nombre de la zona. En realidad es un array de dos cadenas de caracteres uno con el nombre corto y otro el nombre largo.
- timezone, es el número de segundos de desfase con respecto a GMT. Es decir, si estamos en GMT+1, la diferencia será de -3600, GMT+2 será -7200, y así sucesivamente.
- daylight, nos dirá si la zona actual tiene cambio de horario de verano. Como tm_isdst de un struct tm.
Para que se carguen los valores de estas variables, bastará con llamar a localtime() o a tzset().
¡Vamos a ver sus valores en nuestro sistema!
Y, por supuesto, podemos cambiar el huso horario actual, definiendo una zona en la variable de entorno TZ y llamando a tzset():
Esto nos devolverá lo siguiente:
tzname = MSK MSD
timezone = -10800
daylight = 1
Esto nos da algunas posibilidades, por ejemplo podemos, escribir la hora de varias zonas horarias diferentes:
O, por ejemplo, podemos calcular los segundos de desfase (y por tanto, las horas dividiendo entre 3600) que hay entre dos zonas horarias. Por ejemplo:
Versiones reentrantes de funciones comunes
Las funciones que hemos visto hasta ahora, asctime(), ctime(), localtime(), tienen un comportamiento clásico. Como vemos, devuelven un puntero. asctime() y ctime() devuelven un puntero a char, por tanto una cadena de caracters. Por otro lado, localtime(), devuelve un puntero a struct tm. Dicho puntero, es una referencia estática a memoria (es más, no tenemos que liberar la estructura con free()). El problema es que varias llamadas a las funciones reescriben la estructura.
El caso más claro, es que una aplicación multihilo que utilice estas funciones en varios de sus hilos, irá sobreescribiendo la información en memoria y entre los propios hilos irán machacando la información. Otro ejemplo que podemos hacer fácilmente es el siguiente:
Que nos devolverá algo como esto:
T1 : Sun Mar 13 21:33:07 2016
T1 : Sun Mar 13 21:33:09 2016
T2 : Sun Mar 13 21:33:09 2016
Pero, ¿no debería dar el segundo T1 igual que el primero? Este es el gran problema, la zona de memoria donde se almacenan tiempo1 y tiempo2 es la misma y eso a veces puede ser un problema.
Para ello están las versiones reentrantes de las funciones. Disponemos de asctime_r(), localtime_r(), gmtime_r() y ctime_r(). Estas funciones, en lugar de devolver un puntero a una zona estática de memoria, requieren que tengamos una variable donde almacenarlo. Es decir, localtime() nos devuelve un puntero a un struct tm, en cambio localtime_r() nos pide un struct tm donde escribir (pedido por referencia). Vamos a verlo con el ejemplo anterior:
En los próximos episodios
Tenemos que ver otras estructuras para consultar el tiempo con diferentes precisiones, cómo comprobar que una fecha y hora son correctas, cálculos relacionados con las fechas como edades, tiempo que queda para un evento, manipulación del tiempo, generación de marcas de tiempo y muchas más cosas.
Nota: Sí, soy whovian
Foto principal: aussiegall
Foto planta: Frost Museum
Foto husos horarios: De TimeZonesBoy - Trabajo propio, CC BY-SA 4.0,