Fechas y horas en C: time_t, struct tm, strftime y zonas horarias

Fechas y horas en C con <time.h>: pasar de segundos a fecha, formatear con strftime, convertir entre zonas horarias, sumar días y calcular diferencias sin errores, medir cuánto tarda algo y las trampas de struct tm.

C maneja el tiempo con dos representaciones de <time.h>: time_t, un número de segundos desde el 1 de enero de 1970 a medianoche en UTC (la época Unix), y struct tm, con el año, el mes, el día y la hora por separado. Casi todo consiste en pasar de una a otra.

De segundos a fecha y de fecha a texto

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t = 1798761600;
    struct tm utc;
    if (gmtime_r(&t, &utc) == nullptr) return 1;

    char texto[64];
    strftime(texto, sizeof texto, "%Y-%m-%d %H:%M:%S", &utc);
    printf("%lld segundos = %s UTC\n", (long long) t, texto);

    strftime(texto, sizeof texto, "%d/%m/%Y, día %j del año", &utc);
    printf("%s\n", texto);

    static const char *dias[] = {"domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"};
    static const char *meses[] = {"enero", "febrero", "marzo", "abril", "mayo", "junio", "julio",
                                  "agosto", "septiembre", "octubre", "noviembre", "diciembre"};
    printf("Es %s, %d de %s de %d\n", dias[utc.tm_wday], utc.tm_mday, meses[utc.tm_mon], utc.tm_year + 1900);
    return 0;
}
salida1798761600 segundos = 2027-01-01 00:00:00 UTC
01/01/2027, día 001 del año
Es viernes, 1 de enero de 2027

El ejemplo usa una fecha fija para que la salida sea siempre la misma; la hora actual se obtiene con time(nullptr). Tres trampas de struct tm que todo el mundo pisa alguna vez: el año se cuenta desde 1900 (tm_year vale 127 en 2027), los meses van de 0 a 11 y los días de la semana empiezan en domingo, con el 0. strftime escribe con formato sin pasarse del búfer, pero los nombres de días y meses (%A, %B) salen en el idioma del locale, que en muchos servidores y contenedores es el inglés: por eso el programa usa sus propios nombres.

  • FormatoDaEjemplo
  • %Y-%m-%dfecha ISO 86012027-01-01
  • %H:%M:%Shora de 24 h00:00:00
  • %d/%m/%Yfecha al estilo español01/01/2027
  • %jdía del año001
  • %z, %Zdiferencia con UTC y nombre de la zona-0600 CST

Zonas horarias

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static void mostrar(time_t t, const char *zona)
{
    setenv("TZ", zona, 1);
    tzset();
    struct tm local;
    localtime_r(&t, &local);
    char texto[64];
    strftime(texto, sizeof texto, "%Y-%m-%d %H:%M %z %Z", &local);
    printf("%-20s %s\n", zona, texto);
}

int main(void)
{
    time_t t = 1798761600;
    mostrar(t, "UTC");
    mostrar(t, "America/Mexico_City");
    mostrar(t, "America/Bogota");
    mostrar(t, "Europe/Madrid");
    return 0;
}
salidaUTC                  2027-01-01 00:00 +0000 UTC
America/Mexico_City  2026-12-31 18:00 -0600 CST
America/Bogota       2026-12-31 19:00 -0500 -05
Europe/Madrid        2027-01-01 01:00 +0100 CET

El mismo instante, el mismo time_t, es una hora distinta en cada sitio. localtime_r convierte según la zona del sistema, que se cambia con la variable de entorno TZ y tzset. La información de cada zona (incluidos los cambios de horario de verano) viene de la base de datos tzdata del sistema, que hay que mantener actualizada. Usa siempre las versiones con _r (gmtime_r, localtime_r): las de sin _r devuelven un puntero a un struct tm compartido que la siguiente llamada sobrescribe, y no son seguras con hilos.

Sumar días y calcular diferencias

#include <stdio.h>
#include <time.h>

int main(void)
{
    struct tm f = {.tm_year = 2026 - 1900, .tm_mon = 1, .tm_mday = 26, .tm_hour = 12};
    f.tm_mday += 5;
    timegm(&f);                      /* normaliza f: el 31 de febrero pasa a marzo */
    char texto[32];
    strftime(texto, sizeof texto, "%Y-%m-%d", &f);
    printf("26 de febrero + 5 días = %s\n", texto);

    struct tm inicio = {.tm_year = 2026 - 1900, .tm_mon = 9, .tm_mday = 30};
    struct tm navidad = {.tm_year = 2026 - 1900, .tm_mon = 11, .tm_mday = 25};
    double segundos = difftime(timegm(&navidad), timegm(&inicio));
    printf("Del 30 de octubre a Navidad: %.0f días\n", segundos / 86400);
    return 0;
}
salida26 de febrero + 5 días = 2026-03-03
Del 30 de octubre a Navidad: 56 días

El truco para sumar días es cambiar el campo y dejar que la conversión lo normalice: el 31 de febrero se convierte en el 3 de marzo. timegm convierte un struct tm en UTC a time_t y normaliza de paso; existe en glibc, musl, macOS y los BSD, y C23 lo ha incorporado al estándar. La versión local, mktime, es estándar desde siempre, pero aplica la zona horaria y el horario de verano, y en los días del cambio de hora un día no tiene 86,400 segundos. Para fechas sin hora, trabajar en UTC evita esas sorpresas.

Medir cuánto tarda algo

#include <stdio.h>
#include <time.h>

int main(void)
{
    struct timespec a, b;
    clock_gettime(CLOCK_MONOTONIC, &a);

    volatile double suma = 0;
    for (int i = 0; i < 10000000; i++) suma += i * 0.5;

    clock_gettime(CLOCK_MONOTONIC, &b);
    double ms = (b.tv_sec - a.tv_sec) * 1e3 + (b.tv_nsec - a.tv_nsec) / 1e6;
    printf("Diez millones de sumas: %s\n", ms > 0 ? "medido" : "error");
    printf("(el tiempo exacto cambia en cada ejecución)\n");
    return 0;
}
salidaDiez millones de sumas: medido
(el tiempo exacto cambia en cada ejecución)

Para medir intervalos se usa un reloj monótono, que nunca va hacia atrás, y no la hora del sistema, que puede cambiar en mitad de la medida si se sincroniza con un servidor de hora. clock_gettime con CLOCK_MONOTONIC es de POSIX; C11 añadió timespec_get, que en C23 admite también un reloj monótono, aunque no todas las bibliotecas lo implementan. (El programa no imprime los milisegundos para que la salida sea comprobable; en el tuyo, imprime ms.)

Un último aviso: en sistemas donde time_t es de 32 bits, el contador se desborda el 19 de enero de 2038, el llamado problema del año 2038. En Linux de 64 bits time_t ya es de 64 bits, pero en sistemas embebidos y formatos de archivo antiguos sigue siendo un riesgo real.

Todo el código de esta página se compiló con GCC 15.3 (C23, -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.