Civil calendar + token-based date/time parse/format + calendar arithmetic.
Correct for dates before 1970 (Howard Hinnant's days-from-civil algorithm,
signed i64 epoch-day math) — unlike a u64-seconds-since-epoch core, which
silently floors any pre-1970 date to 1970-01-01.
- Pure-
stdcivil-calendar core (no external dependency) chosen for exactly this pre-epoch correctness reason. - Model after: Howard Hinnant chrono civil algorithms (https://howardhinnant.github.io/date_algorithms.html); a strftime-like token vocabulary for parse/format.
- Platform: any (pure logic, no OS calls, no
std.timetimestamps). Role: util. Concurrency: reentrant (no shared state). Allocation:format/formatIsoDatetake an allocator (output is an owned, variable-length string); everything else is allocation-free.
Provenance: original work of the zig-libs authors (MIT). No third-party code.
- Civil core —
ymdToEpochDay/epochDayToYmd/isoWeekday, leap-year + days-in-month, name tables. - Token I/O —
parse(string + format →DateParts) andformat(DateParts+ format → string). A parse→format reshuffle never round-trips through an epoch timestamp, so it has no lower-year limit.
YYYY 4-digit year YY 2-digit year (00-69→2000s, 70-99→1900s)
MM/M month (2 / 1-2 digit) MMM short month name MMMM full month name
DD/D day (2 / 1-2 digit)
hh/h hour 24h (2 / 1-2) ii/i hour 12h (needs A/a)
mm/m minute ss/s second
A/a AM/PM upper/lower ZZ UTC offset ±HH:MM (parses literal Z as +00:00)
EEEE full day name EEE/EE/E short day name
e day-of-week 1-7 (Mon=1)
[text] literal [*] wildcard — skip until the next token
date_tokens: [_]DateTokenDoc carries this table as data (token/meaning/
example), for callers that want to render a reference/diagnostic UI.
const datefmt = @import("datefmt");
const DateParts = struct {
year: i32 = 1970, month: u32 = 1, day: u32 = 1,
hour: u32 = 0, minute: u32 = 0, second: u32 = 0,
off_min: ?i32 = null, // set by a ZZ token; null if the format has none
};
// Civil core
fn ymdToEpochDay(year: i32, month: u32, day: u32) i64;
fn epochDayToYmd(epoch_day: i64) DateParts;
fn isoWeekday(epoch_day: i64) u32; // Mon=1 … Sun=7
fn partsToUnix(p: DateParts) i64;
fn unixToParts(unix: i64) DateParts;
fn isLeapYear(year: i32) bool;
fn daysInMonth(year: i32, month: u32) u32;
fn validate(p: DateParts) bool;
// Token I/O
fn parse(input, fmt) ParseError!DateParts;
fn format(alloc, parts, fmt) ![]u8; // caller frees
// Arithmetic (pre-1970 safe)
fn addDays(parts, n: i64) DateParts;
fn addMonths(parts, n: i32) DateParts; // clamps day (Jan 31 +1mo → Feb 28/29)
fn addYears(parts, n: i32) DateParts;
fn diffInDays(a, b) i64;
fn diffInMonths(a, b) i32;
fn diffInYears(a, b) i32;
fn startOfDay/endOfDay/startOfMonth/endOfMonth/startOfYear/endOfYear(parts) DateParts;
fn nthWeekdayOfMonth(year, month, weekday, n) ?DateParts; // DST-boundary primitive
// Strict ISO + validation helpers
fn parseIsoDate(s) ParseError!DateParts; // strict YYYY-MM-DD
fn formatIsoDate(alloc, parts) ![]const u8;
fn firstInvalidFormatChar(fmt) ?usize; // diagnostic: offset of first bad tokenParseError = error{ InvalidFormat, InvalidDate, InvalidTime, InvalidFormatString, TooManyTokens }.
zig build test-datefmt
zig build test-datefmt -Doptimize=ReleaseFast
zig fmt --check modules/datefmt
- Locale-aware month/day names — the name tables are English
Jan…Dec/Mon…Sunonly; no locale table or i18n hook. - ISO-week-date (
YYYY-Www-D, ISO 8601 week numbering) — not in the token vocabulary;isoWeekdaygives weekday-in-week only, not week-of-year. - Timezone-aware formatting/conversion (IANA tz database, DST rules,
offset lookup by zone name) —
ZZonly carries a raw UTC-offset literal through parse/format; there is no zone database. Intentionally a separate module (tz), not pulled in here. - Duration/period types (e.g. ISO 8601
PnYnMnDparsing) — arithmetic here takes/returns plain day/month/year counts, no duration value type. - RFC 2822 / RFC 3339 canned formats — only the generic token engine; no named-format shortcuts (a caller composes the token string itself).