Time is the hardest problem in programming that nobody admits is hard. Leap years. Daylight saving transitions. Timezone offsets that are not whole hours. Months with 28, 29, 30, or 31 days. Calendar systems that disagree on when the year starts. The list of edge cases is infinite, and every one of them has caused a production outage somewhere.
We could have punted. We could have told FLIN developers to handle time with raw integers and format strings. Instead, across Sessions 014, 015, and 209, we built a complete time system into the language -- 26 functions, natural duration syntax, timezone-aware operations, and formatting that handles every locale FLIN targets. No moment.js. No date-fns. No Luxon. Just now, today, and a handful of methods that do exactly what you expect.
The Problem With Time in Every Other Language
JavaScript's Date object is a masterclass in poor API design. Creating a date for January 15, 2026 requires new Date(2026, 0, 15) -- months are zero-indexed (January is 0), but days are one-indexed (the 15th is 15). Adding seven days to a date requires extracting milliseconds, adding 7 24 60 60 1000, and constructing a new Date. Formatting a date for display requires either toLocaleDateString() with its inconsistent browser implementations, or a third-party library.
Python's datetime module is better but requires importing three different classes: datetime for timestamps, timedelta for durations, and timezone for timezone handling. Ruby's Time class is excellent but language-specific. Go's time.Parse("2006-01-02", s) uses a magic reference date that nobody can remember.
FLIN takes the best ideas from all of these and makes them available with zero boilerplate.
Current Time: Four Keywords
right_now = now // Current timestamp (UTC)
today_start = today // Today at 00:00:00
yesterday_start = yesterday // Yesterday at 00:00:00
tomorrow_start = tomorrow // Tomorrow at 00:00:00These are not functions. They are language keywords that resolve to time values. now returns the current UTC timestamp with millisecond precision. today returns midnight of the current day. yesterday and tomorrow return midnight of the adjacent days.
Why keywords instead of functions? Because now is used so frequently that the two-character savings -- now vs now() -- adds up across an entire codebase. And because making them keywords communicates intent: these are foundational concepts, not utility functions.
Time Components
Every time value exposes its components as properties:
t = nowt.year // 2026 t.month // 3 (March) t.day // 26 t.hour // 14 t.minute // 30 t.second // 45 t.millisecond // 123 t.day_of_week // 3 (Wednesday, 0 = Sunday) t.day_of_year // 85 t.week_of_year // 13 t.is_weekend // false t.is_leap_year // false ```
Months are one-indexed. January is 1, December is 12. Days of the week start from Sunday (0) through Saturday (6). These conventions match ISO 8601 for months and the common US convention for weekdays. We considered using Monday as day 0 (the ISO convention), but the majority of JavaScript developers -- FLIN's primary migration audience -- expect Sunday as 0.
is_weekend and is_leap_year are convenience properties that eliminate the kind of boolean logic developers get wrong. is_weekend returns true for Saturday (6) and Sunday (0). is_leap_year implements the full Gregorian leap year rule (divisible by 4, except centuries, except centuries divisible by 400).
Duration Syntax: The Design That Writes Itself
The crown jewel of FLIN's time system is the duration syntax. Instead of multiplying integers by magic constants, you write durations as natural English:
1.second
5.minutes
2.hours
7.days
4.weeks
3.months
1.yearThese are not methods on numbers. They are duration literals recognized by the parser and type checker. The expression 7.days has type duration, not int or float. You cannot add a duration to a string. You cannot multiply a duration by a list. The type system prevents nonsense operations.
Duration arithmetic with time values reads like English:
// When does the subscription expire?
expires = now + 30.days// When was 2 hours ago? two_hours_ago = now - 2.hours
// How long until the meeting? meeting = parse_time("2026-03-26T16:00:00Z") wait = meeting - now print("Meeting in {wait.hours} hours and {wait.minutes} minutes")
// Chaining durations total = 1.hour + 30.minutes + 45.seconds ```
The implementation is straightforward. Durations are stored internally as a number of milliseconds. 1.second is 1,000 milliseconds. 5.minutes is 300,000 milliseconds. Adding a duration to a time value produces a new time value. Subtracting two time values produces a duration. The type checker enforces these rules at compile time.
The tricky part is months and years. Unlike seconds, minutes, hours, and days, months and years are not fixed-length. February has 28 or 29 days. A "month from now" on January 31 could be February 28, February 29, or March 3, depending on the year and your interpretation. FLIN follows the convention used by most date libraries: adding a month advances the month number by one, and if the resulting day does not exist, it is clamped to the last day of the month.
// January 31 + 1 month = February 28 (or 29 in leap years)
jan31 = parse_time("2026-01-31")
feb = jan31 + 1.month
print(feb.format("YYYY-MM-DD")) // "2026-02-28"Time Comparison
a = 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") ) // true ```
is_same_day compares year, month, and day, ignoring the time component. This is essential for calendar applications, where "today's events" means everything from midnight to midnight, regardless of the exact timestamp.
is_between is inclusive on both ends. A time value equal to either boundary returns true. This matches the intuitive expectation: if the meeting is between Monday and Friday, meetings on Monday and Friday are included.
Time Formatting
Formatting is where most time libraries either shine or collapse. FLIN uses a token-based format string inspired by Moment.js (the most widely known format) with some additions from Unicode CLDR:
t = 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"
// Standard formats t.iso // "2026-12-31T14:30:45.000Z" t.unix // 1798800645 (seconds) t.unix_millis // 1798800645000 (milliseconds) ```
The format tokens:
| Token | Meaning | Example |
|---|---|---|
YYYY | Four-digit year | 2026 |
MM | Two-digit month | 12 |
DD | Two-digit day | 31 |
HH | 24-hour hour | 14 |
hh | 12-hour hour | 02 |
mm | Minutes | 30 |
ss | Seconds | 45 |
A | AM/PM | PM |
MMMM | Full month name | December |
MMM | Short month name | Dec |
dddd | Full weekday name | Thursday |
ddd | Short weekday name | Thu |
D | Day without padding | 31 |
M | Month without padding | 12 |
Month and weekday names are locale-dependent. In a French-language application, MMMM produces "decembre" (with proper accents in the actual output), and dddd produces "jeudi". The locale is set at the application level, not per-function call.
Time Parsing
// ISO 8601 (auto-detected)
t1 = parse_time("2026-12-31")
t2 = parse_time("2026-12-31T14:30:00Z")
t3 = parse_time("2026-12-31T14:30:00+01:00")// Custom format t4 = parse_time("Dec 31, 2026", "MMM D, YYYY") t5 = parse_time("31/12/2026", "DD/MM/YYYY") ```
The single-argument form of parse_time auto-detects ISO 8601 formats. It handles dates with and without times, with and without timezone offsets, with and without milliseconds. If the string does not match any known format, it returns none instead of crashing.
The two-argument form accepts the same tokens as format, used in reverse. This is symmetrical: if t.format("DD/MM/YYYY") produces "31/12/2026", then parse_time("31/12/2026", "DD/MM/YYYY") produces the same time value. Symmetry between formatting and parsing is a property that surprisingly few time libraries guarantee.
Time Manipulation
t = nowt.start_of_day // Today at 00:00:00 t.end_of_day // Today at 23:59:59.999 t.start_of_week // Monday at 00:00:00 t.start_of_month // 1st of this month at 00:00:00 t.start_of_year // January 1st at 00:00:00 ```
These manipulation methods are essential for reporting queries. "Show me all orders this month" translates to order.created_at.is_after(now.start_of_month). "Show me this week's activity" translates to activity.timestamp.is_after(now.start_of_week).
start_of_week uses Monday as the start of the week (ISO 8601 convention). This is configurable at the application level for locales where the week starts on Sunday or Saturday.
Relative Time
past = 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"
// Relative to a specific time event_time = parse_time("2026-03-26T10:00:00Z") event_time.relative_to(now) // "4 hours ago" ```
from_now produces human-readable relative time strings. The output scales with the duration: "just now" for less than a minute, "5 minutes ago" for minutes, "3 hours ago" for hours, "2 days ago" for days, "3 months ago" for months, "1 year ago" for years.
The strings are locale-sensitive. In French: "il y a 3 heures", "dans 2 jours", "il y a 1 an". In English: "3 hours ago", "in 2 days", "1 year ago".
Timezone Handling
Timezone handling was the most technically challenging aspect of the time system. FLIN stores all time values in UTC internally. Display-time conversion to local timezones happens explicitly:
// All times are UTC internally
utc_now = now
print(utc_now.format("HH:mm")) // UTC time// Convert to a specific timezone 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 or GMT+2 in summer) print(new_york.format("HH:mm")) // EST (GMT-5 or GMT-4 in summer) ```
The timezone database is embedded in the FLIN runtime (compiled from the IANA tzdata). This means timezone conversions work offline, without network access, and produce consistent results regardless of the host operating system's timezone configuration.
Daylight saving time transitions are handled correctly. When Paris switches from CET to CEST in March, in_timezone("Europe/Paris") automatically adjusts the offset. When New York switches from EST to EDT, the offset changes from -5 to -4. The developer does not need to know when these transitions occur -- the timezone database handles it.
Real-World Example: Event Scheduling
Putting it all together, here is how FLIN's time system handles a real-world use case -- scheduling an event with timezone-aware display:
// Create an event in UTC
event = {
title: "Product Launch",
start: parse_time("2026-04-15T18:00:00Z"),
duration: 2.hours
}// Display for different audiences end_time = event.start + event.duration
print("For Abidjan: {event.start.in_timezone('Africa/Abidjan').format('MMMM D, h:mm A')} to {end_time.in_timezone('Africa/Abidjan').format('h:mm A')}") // "For Abidjan: April 15, 6:00 PM to 8:00 PM"
print("For Paris: {event.start.in_timezone('Europe/Paris').format('MMMM D, h:mm A')} to {end_time.in_timezone('Europe/Paris').format('h:mm A')}") // "For Paris: April 15, 8:00 PM to 10:00 PM"
// Check if the event is upcoming
{if event.start.is_after(now)}
No imports. No timezone library. No date formatting library. No relative time library. Six different time operations -- parsing, arithmetic, timezone conversion, formatting, comparison, and relative display -- all built into the language.
Implementation: Chrono Under the Hood
FLIN's time system is built on Rust's chrono crate, the most battle-tested date-time library in the Rust ecosystem. We chose chrono over the newer time crate because chrono has better timezone support and more extensive formatting options.
The internal representation is a 64-bit Unix timestamp in milliseconds. This gives us a range from approximately 290 million years in the past to 290 million years in the future, with millisecond precision. More than enough for any web application.
// Internal representation
#[derive(Clone, Copy, Debug)]
pub struct FlinTime {
millis: i64, // Unix timestamp in milliseconds
}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(); // Token-based formatting using chrono's strftime self.format_with_tokens(dt, pattern) } } ```
The token-based formatting is a custom implementation that translates FLIN format tokens (YYYY, MM, DD) to chrono's strftime tokens (%Y, %m, %d). We wrote our own instead of exposing chrono's strftime directly because strftime's tokens are cryptic (who remembers that %B is the full month name?) and inconsistent across platforms.
Twenty-Six Functions, Zero Dependencies
The complete time API:
- 4 current-time keywords:
now,today,yesterday,tomorrow - 12 component properties:
year,month,day,hour,minute,second,millisecond,day_of_week,day_of_year,week_of_year,is_weekend,is_leap_year - 7 duration constructors:
second,minutes,hours,days,weeks,months,year - 4 comparison methods:
is_before,is_after,is_same_day,is_between - 5 manipulation methods:
start_of_day,end_of_day,start_of_week,start_of_month,start_of_year - 3 formatting outputs:
format,iso,unix - 2 parsing functions:
parse_time(one and two argument forms) - 2 relative display methods:
from_now,relative_to - 1 timezone conversion:
in_timezone
Twenty-six functions that replace moment.js (288KB minified), date-fns (75KB), and Luxon (67KB). All compiled into the FLIN binary at near-zero cost.
---
This is Part 74 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO built a timezone-aware time system into a programming language.
Series Navigation: - [73] Math, Statistics, and Geometry Functions - [74] Time and Timezone Functions (you are here) - [75] HTTP Client Built Into the Language - [76] Security Functions: Crypto, JWT, Argon2