El tiempo es el problema más difícil en programación que nadie admite que es difícil. Años bisiestos. Transiciones de horario de verano. Desfases de zona horaria que no son horas completas. Meses con 28, 29, 30 o 31 días. Sistemas de calendario que no están de acuerdo sobre cuándo empieza el año. La lista de casos extremos es infinita, y cada uno de ellos ha causado una caída en producción en algún lugar.
Podríamos haber eludido el problema. Podríamos haber dicho a los desarrolladores de FLIN que manejaran el tiempo con enteros sin procesar y cadenas de formato. En cambio, a lo largo de las Sesiones 014, 015 y 209, construimos un sistema de tiempo completo en el lenguaje -- 26 funciones, sintaxis natural de duración, operaciones conscientes de zona horaria, y formato que maneja cada localización que FLIN apunta. Sin moment.js. Sin date-fns. Sin Luxon. Solo now, today, y un puñado de métodos que hacen exactamente lo que esperas.
El problema con el tiempo en todos los demás lenguajes
El objeto Date de JavaScript es una clase magistral de mal diseño de API. Crear una fecha para el 15 de enero de 2026 requiere new Date(2026, 0, 15) -- los meses tienen índice cero (enero es 0), pero los días tienen índice uno (el 15 es 15). Agregar siete días a una fecha requiere extraer milisegundos, sumar 7 <em> 24 </em> 60 <em> 60 </em> 1000 y construir un nuevo Date. Formatear una fecha para visualización requiere toLocaleDateString() con sus implementaciones inconsistentes entre navegadores, o una biblioteca de terceros.
El módulo datetime de Python es mejor pero requiere importar tres clases diferentes: datetime para marcas de tiempo, timedelta para duraciones y timezone para manejo de zona horaria. La clase Time de Ruby es excelente pero específica del lenguaje. El time.Parse("2006-01-02", s) de Go usa una fecha de referencia mágica que nadie puede recordar.
FLIN toma las mejores ideas de todos estos y las hace disponibles sin ningún boilerplate.
Tiempo actual: cuatro palabras clave
flinright_now = now // Marca de tiempo actual (UTC)
today_start = today // Hoy a las 00:00:00
yesterday_start = yesterday // Ayer a las 00:00:00
tomorrow_start = tomorrow // Mañana a las 00:00:00Estas no son funciones. Son palabras clave del lenguaje que se resuelven en valores de tiempo. now devuelve la marca de tiempo UTC actual con precisión de milisegundos. today devuelve la medianoche del día actual. yesterday y tomorrow devuelven la medianoche de los días adyacentes.
¿Por qué palabras clave en lugar de funciones? Porque now se usa con tanta frecuencia que el ahorro de dos caracteres -- now vs now() -- se acumula en todo un código base. Y porque hacerlas palabras clave comunica intención: son conceptos fundamentales, no funciones utilitarias.
Componentes de tiempo
Cada valor de tiempo expone sus componentes como propiedades:
flint = now
t.year // 2026
t.month // 3 (marzo)
t.day // 26
t.hour // 14
t.minute // 30
t.second // 45
t.millisecond // 123
t.day_of_week // 3 (miércoles, 0 = domingo)
t.day_of_year // 85
t.week_of_year // 13
t.is_weekend // false
t.is_leap_year // falseLos meses tienen índice uno. Enero es 1, diciembre es 12. Los días de la semana empiezan desde domingo (0) hasta sábado (6). Estas convenciones coinciden con ISO 8601 para meses y la convención estadounidense común para días de la semana. Consideramos usar lunes como día 0 (la convención ISO), pero la mayoría de los desarrolladores JavaScript -- el principal público de migración de FLIN -- esperan domingo como 0.
is_weekend e is_leap_year son propiedades de conveniencia que eliminan el tipo de lógica booleana que los desarrolladores calculan mal. is_weekend devuelve true para sábado (6) y domingo (0). is_leap_year implementa la regla completa de año bisiesto gregoriano (divisible por 4, excepto los siglos, excepto los siglos divisibles por 400).
Sintaxis de duración: el diseño que se escribe solo
La joya de la corona del sistema de tiempo de FLIN es la sintaxis de duración. En lugar de multiplicar enteros por constantes mágicas, escribes duraciones como inglés natural:
flin1.second
5.minutes
2.hours
7.days
4.weeks
3.months
1.yearEstos no son métodos en números. Son literales de duración reconocidos por el parser y el verificador de tipos. La expresión 7.days tiene tipo duration, no int o float. No puedes sumar una duración a una cadena. No puedes multiplicar una duración por una lista. El sistema de tipos previene operaciones sin sentido.
La aritmética de duración con valores de tiempo se lee como inglés:
flin// ¿Cuándo expira la suscripción?
expires = now + 30.days
// ¿Cuándo fue hace 2 horas?
two_hours_ago = now - 2.hours
// ¿Cuánto falta para la reunión?
meeting = parse_time("2026-03-26T16:00:00Z")
wait = meeting - now
print("Reunión en {wait.hours} horas y {wait.minutes} minutos")
// Encadenar duraciones
total = 1.hour + 30.minutes + 45.secondsLa implementación es directa. Las duraciones se almacenan internamente como número de milisegundos. 1.second son 1,000 milisegundos. 5.minutes son 300,000 milisegundos. Sumar una duración a un valor de tiempo produce un nuevo valor de tiempo. Restar dos valores de tiempo produce una duración. El verificador de tipos impone estas reglas en tiempo de compilación.
La parte complicada son los meses y años. A diferencia de segundos, minutos, horas y días, los meses y años no tienen longitud fija. Febrero tiene 28 o 29 días. "Un mes a partir de ahora" el 31 de enero podría ser el 28 de febrero, el 29 de febrero o el 3 de marzo, dependiendo del año y tu interpretación. FLIN sigue la convención usada por la mayoría de las bibliotecas de fechas: sumar un mes avanza el número de mes en uno, y si el día resultante no existe, se fija al último día del mes.
flin// 31 de enero + 1 mes = 28 de febrero (o 29 en años bisiestos)
jan31 = parse_time("2026-01-31")
feb = jan31 + 1.month
print(feb.format("YYYY-MM-DD")) // "2026-02-28"Comparación de tiempo
flina = parse_time("2026-03-26T10:00:00Z")
b = parse_time("2026-03-26T14:00:00Z")
a.is_before(b) // true
a.is_after(b) // false
a.is_same_day(b) // true
a.is_between(
parse_time("2026-03-01"),
parse_time("2026-03-31")
) // trueis_same_day compara año, mes y día, ignorando el componente de tiempo. Esto es esencial para aplicaciones de calendario, donde "eventos de hoy" significa todo desde medianoche hasta medianoche, independientemente de la marca de tiempo exacta.
is_between es inclusivo en ambos extremos. Un valor de tiempo igual a cualquiera de los límites devuelve true. Esto coincide con la expectativa intuitiva: si la reunión es entre lunes y viernes, las reuniones del lunes y del viernes están incluidas.
Formato de tiempo
El formato es donde la mayoría de las bibliotecas de tiempo brillan o colapsan. FLIN usa una cadena de formato basada en tokens inspirada en Moment.js (el formato más ampliamente conocido) con algunas adiciones del Unicode CLDR:
flint = parse_time("2026-12-31T14:30:45Z")
t.format("YYYY-MM-DD") // "2026-12-31"
t.format("HH:mm:ss") // "14:30:45"
t.format("MMMM D, YYYY") // "December 31, 2026"
t.format("dddd") // "Thursday"
t.format("MMM D") // "Dec 31"
t.format("h:mm A") // "2:30 PM"
// Formatos estándar
t.iso // "2026-12-31T14:30:45.000Z"
t.unix // 1798800645 (segundos)
t.unix_millis // 1798800645000 (milisegundos)Los tokens de formato:
| Token | Significado | Ejemplo |
|---|---|---|
YYYY | Año de cuatro dígitos | 2026 |
MM | Mes de dos dígitos | 12 |
DD | Día de dos dígitos | 31 |
HH | Hora formato 24h | 14 |
hh | Hora formato 12h | 02 |
mm | Minutos | 30 |
ss | Segundos | 45 |
A | AM/PM | PM |
MMMM | Nombre completo del mes | December |
MMM | Nombre corto del mes | Dec |
dddd | Nombre completo del día | Thursday |
ddd | Nombre corto del día | Thu |
D | Día sin relleno | 31 |
M | Mes sin relleno | 12 |
Los nombres de meses y días de la semana dependen de la localización. En una aplicación en francés, MMMM produce "decembre" (con los acentos correctos en la salida real), y dddd produce "jeudi". La localización se configura a nivel de aplicación, no por llamada a función.
Análisis de tiempo
flin// ISO 8601 (autodetectado)
t1 = parse_time("2026-12-31")
t2 = parse_time("2026-12-31T14:30:00Z")
t3 = parse_time("2026-12-31T14:30:00+01:00")
// Formato personalizado
t4 = parse_time("Dec 31, 2026", "MMM D, YYYY")
t5 = parse_time("31/12/2026", "DD/MM/YYYY")La forma de un argumento de parse_time autodetecta formatos ISO 8601. Maneja fechas con y sin horas, con y sin desfases de zona horaria, con y sin milisegundos. Si la cadena no coincide con ningún formato conocido, devuelve none en lugar de fallar.
La forma de dos argumentos acepta los mismos tokens que format, usados a la inversa. Esto es simétrico: si t.format("DD/MM/YYYY") produce "31/12/2026", entonces parse_time("31/12/2026", "DD/MM/YYYY") produce el mismo valor de tiempo. La simetría entre formato y análisis es una propiedad que sorprendentemente pocas bibliotecas de tiempo garantizan.
Manipulación de tiempo
flint = now
t.start_of_day // Hoy a las 00:00:00
t.end_of_day // Hoy a las 23:59:59.999
t.start_of_week // Lunes a las 00:00:00
t.start_of_month // 1 de este mes a las 00:00:00
t.start_of_year // 1 de enero a las 00:00:00Estos métodos de manipulación son esenciales para consultas de reportes. "Muéstrame todos los pedidos de este mes" se traduce a order.created_at.is_after(now.start_of_month). "Muéstrame la actividad de esta semana" se traduce a activity.timestamp.is_after(now.start_of_week).
start_of_week usa lunes como inicio de semana (convención ISO 8601). Esto es configurable a nivel de aplicación para localizaciones donde la semana comienza en domingo o sábado.
Tiempo relativo
flinpast = now - 3.hours
past.from_now // "3 hours ago"
future = now + 2.days
future.from_now // "in 2 days"
old = parse_time("2025-01-01")
old.from_now // "1 year ago"
// Relativo a un tiempo específico
event_time = parse_time("2026-03-26T10:00:00Z")
event_time.relative_to(now) // "4 hours ago"from_now produce cadenas de tiempo relativo legibles por humanos. La salida escala con la duración: "just now" para menos de un minuto, "5 minutes ago" para minutos, "3 hours ago" para horas, "2 days ago" para días, "3 months ago" para meses, "1 year ago" para años.
Las cadenas son sensibles a la localización. En francés: "il y a 3 heures", "dans 2 jours", "il y a 1 an". En inglés: "3 hours ago", "in 2 days", "1 year ago".
Manejo de zonas horarias
El manejo de zonas horarias fue el aspecto técnicamente más desafiante del sistema de tiempo. FLIN almacena todos los valores de tiempo en UTC internamente. La conversión a zonas horarias locales para visualización ocurre explícitamente:
flin// Todos los tiempos son UTC internamente
utc_now = now
print(utc_now.format("HH:mm")) // Hora UTC
// Convertir a una zona horaria específica
abidjan = utc_now.in_timezone("Africa/Abidjan")
paris = utc_now.in_timezone("Europe/Paris")
new_york = utc_now.in_timezone("America/New_York")
print(abidjan.format("HH:mm")) // GMT+0
print(paris.format("HH:mm")) // CET (GMT+1 o GMT+2 en verano)
print(new_york.format("HH:mm")) // EST (GMT-5 o GMT-4 en verano)La base de datos de zonas horarias está incrustada en el runtime de FLIN (compilada desde el tzdata de IANA). Esto significa que las conversiones de zona horaria funcionan sin conexión, sin acceso a la red, y producen resultados consistentes independientemente de la configuración de zona horaria del sistema operativo anfitrión.
Las transiciones de horario de verano se manejan correctamente. Cuando París cambia de CET a CEST en marzo, in_timezone("Europe/Paris") ajusta automáticamente el desfase. Cuando Nueva York cambia de EST a EDT, el desfase cambia de -5 a -4. El desarrollador no necesita saber cuándo ocurren estas transiciones -- la base de datos de zonas horarias lo maneja.
Ejemplo del mundo real: programación de eventos
Poniendo todo junto, así es como el sistema de tiempo de FLIN maneja un caso de uso del mundo real -- programar un evento con visualización consciente de zona horaria:
flin// Crear un evento en UTC
event = {
title: "Product Launch",
start: parse_time("2026-04-15T18:00:00Z"),
duration: 2.hours
}
// Mostrar para diferentes audiencias
end_time = event.start + event.duration
print("Para Abiyán: {event.start.in_timezone('Africa/Abidjan').format('MMMM D, h:mm A')} a {end_time.in_timezone('Africa/Abidjan').format('h:mm A')}")
// "Para Abiyán: April 15, 6:00 PM a 8:00 PM"
print("Para París: {event.start.in_timezone('Europe/Paris').format('MMMM D, h:mm A')} a {end_time.in_timezone('Europe/Paris').format('h:mm A')}")
// "Para París: April 15, 8:00 PM a 10:00 PM"
// Verificar si el evento es próximo
{if event.start.is_after(now)}
<Badge variant="info">{event.start.from_now}</Badge>
// "in 20 days"
{else if end_time.is_after(now)}
<Badge variant="success">Happening now</Badge>
{else}
<Badge variant="muted">{event.start.from_now}</Badge>
// "20 days ago"
{/if}Sin importaciones. Sin biblioteca de zonas horarias. Sin biblioteca de formato de fechas. Sin biblioteca de tiempo relativo. Seis operaciones de tiempo diferentes -- análisis, aritmética, conversión de zona horaria, formato, comparación y visualización relativa -- todas integradas en el lenguaje.
Implementación: Chrono bajo el capó
El sistema de tiempo de FLIN está construido sobre el crate chrono de Rust, la biblioteca de fecha y hora más probada en batalla del ecosistema Rust. Elegimos chrono sobre el crate más nuevo time porque chrono tiene mejor soporte de zonas horarias y opciones de formato más extensas.
La representación interna es una marca de tiempo Unix de 64 bits en milisegundos. Esto nos da un rango de aproximadamente 290 millones de años en el pasado a 290 millones de años en el futuro, con precisión de milisegundos. Más que suficiente para cualquier aplicación web.
rust// Representación interna
#[derive(Clone, Copy, Debug)]
pub struct FlinTime {
millis: i64, // Marca de tiempo Unix en milisegundos
}
impl FlinTime {
pub fn now() -> Self {
Self { millis: Utc::now().timestamp_millis() }
}
pub fn format(&self, pattern: &str) -> String {
let dt = Utc.timestamp_millis_opt(self.millis).unwrap();
// Formato basado en tokens usando strftime de chrono
self.format_with_tokens(dt, pattern)
}
}El formato basado en tokens es una implementación personalizada que traduce los tokens de formato de FLIN (YYYY, MM, DD) a los tokens strftime de chrono (%Y, %m, %d). Escribimos la nuestra en lugar de exponer el strftime de chrono directamente porque los tokens de strftime son crípticos (¿quién recuerda que %B es el nombre completo del mes?) e inconsistentes entre plataformas.
Veintiséis funciones, cero dependencias
La API de tiempo completa:
- 4 palabras clave de tiempo actual:
now,today,yesterday,tomorrow - 12 propiedades de componentes:
year,month,day,hour,minute,second,millisecond,day_of_week,day_of_year,week_of_year,is_weekend,is_leap_year - 7 constructores de duración:
second,minutes,hours,days,weeks,months,year - 4 métodos de comparación:
is_before,is_after,is_same_day,is_between - 5 métodos de manipulación:
start_of_day,end_of_day,start_of_week,start_of_month,start_of_year - 3 salidas de formato:
format,iso,unix - 2 funciones de análisis:
parse_time(formas de uno y dos argumentos) - 2 métodos de visualización relativa:
from_now,relative_to - 1 conversión de zona horaria:
in_timezone
Veintiséis funciones que reemplazan moment.js (288KB minificado), date-fns (75KB) y Luxon (67KB). Todas compiladas en el binario de FLIN a un costo casi nulo.
Esta es la Parte 74 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA construyeron un sistema de tiempo consciente de zonas horarias en un lenguaje de programación.
Navegación de la serie: - [73] Funciones matemáticas, estadísticas y de geometría - [74] Funciones de tiempo y zona horaria (estás aquí) - [75] Cliente HTTP integrado en el lenguaje - [76] Funciones de seguridad: Crypto, JWT, Argon2