Skip to content

Instantly share code, notes, and snippets.

@dk949
Created March 18, 2025 01:40
Show Gist options
  • Save dk949/fbf90bb4c69c75566e47a472cf1fa174 to your computer and use it in GitHub Desktop.
Save dk949/fbf90bb4c69c75566e47a472cf1fa174 to your computer and use it in GitHub Desktop.
A very basic utility to manage date and time in UTC timezone
//! A very basic utility to manage date and time in UTC timezone.
//!
//! Usage:
//!
//! ```zig
//! // The following examples assume that the current date and time is 25th May 1977, at 08:48 PM
//! const now = DateTime.now();
//! std.debug.print("{}\n", .{now}); // 1977-05-25 20:48:00
//! std.debug.print("{%d %B %Y at %I}:{%M %p}\n", .{ now, now }); // 25 May 1977 at 08:48 PM
//! // NOTE: the format string has to be split in two, because `:` is a special character in fmt
//!
//! const start: DateTime = .epoch_start;
//! std.debug.print("{}\n", .{start}); // 1970-01-01 00:00:00
//!
//! const init: DateTime = .{};
//! std.debug.print("{}\n", .{init}); // 0001-01-01 00:00:0001
//!
//! // Intended for use with values from std.time.timestamp and friends
//! const from_timestamp = DateTime.fromTimestamp(1234567890);
//! std.debug.print("{}\n", .{from_timestamp}); // 2009-02-13 23:31:30
//!
//! // NOTE: `fromNanoTimestamp`, `fromMicroTimestamp` and `fromMilliTimestamp`
//! // are also provided for convenience, but they do not increase precision.
//! ```
//!
//! ## Format specifiers
//!
//! These are borrowed from the C `strftime` function. But not all are
//! implemented and no timezone information is preserved.
//!
//! ### Supported specifiers
//!
//! %b The abbreviated month name in english.
//! %B The full month name in english
//! %d The day of the month as a decimal number (range 01 to 31)
//! %D Equivalent to %m/%d/%y.
//! %F Equivalent to %Y-%m-%d (the ISO 8601 date format)
//! %H The hour as a decimal number using a 24-hour clock (range 00 to 23)
//! %I The hour as a decimal number using a 12-hour clock (range 01 to 12)
//! %m The month as a decimal number (range 01 to 12)
//! %M The minute as a decimal number (range 00 to 59)
//! %n A newline character
//! %p Either "AM" or "PM" according to the given time value
//! %P Like %p but in lowercase: "am" or "pm"
//! %r The time in a.m. or p.m. notation (%I:%M:%S %p).
//! %R The time in 24-hour notation (%H:%M)
//! %S The second as a decimal number (range 00 to 60)
//! %t A tab character
//! %T The time in 24-hour notation (%H:%M:%S)
//! %y The year as a decimal number without a century (range 00 to 99).
//! %Y The year as a decimal number including the century.
//! %% A literal '%' character.
const std = @import("std");
const DateTime = @This();
pub const epoch_start = DateTime.fromTimestamp(0);
year: u16 = 1,
month: std.time.epoch.Month = .jan,
day: u5 = 1,
hour: u5 = 0,
minute: u6 = 0,
second: u6 = 0,
pub fn fromTimestamp(timestamp: i64) DateTime {
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = @intCast(timestamp) };
const epoch_days = epoch_seconds.getEpochDay();
const year_and_day = epoch_days.calculateYearDay();
const month_and_day = year_and_day.calculateMonthDay();
const day_seconds = epoch_seconds.getDaySeconds();
return DateTime{
.year = year_and_day.year,
.month = month_and_day.month,
.day = month_and_day.day_index + 1,
.hour = day_seconds.getHoursIntoDay(),
.minute = day_seconds.getMinutesIntoHour(),
.second = day_seconds.getSecondsIntoMinute(),
};
}
pub fn fromNanoTimestamp(timestamp: i128) DateTime {
return fromTimestamp(timestamp / std.time.ns_per_s);
}
pub fn fromMicroTimestamp(timestamp: i64) DateTime {
return fromTimestamp(timestamp / std.time.us_per_s);
}
pub fn fromMilliTimestamp(timestamp: i64) DateTime {
return fromTimestamp(timestamp / std.time.ms_per_s);
}
pub fn now() DateTime {
return fromTimestamp(std.time.timestamp());
}
pub fn format(datetime: @This(), comptime _fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
const fmt = if (_fmt.len > 0) _fmt else "%F %T";
const new_fmt, const kinds = comptime blk: {
var new_fmt: []const u8 = "";
var percent = false;
var kinds: []const Kind = &.{};
for (fmt) |ch| {
if (percent) {
switch (ch) {
'b' => {
new_fmt = new_fmt ++ "{s}";
kinds = kinds ++ &[_]Kind{.b};
},
'B' => {
new_fmt = new_fmt ++ "{s}";
kinds = kinds ++ &[_]Kind{.B};
},
'd' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.d};
},
'D' => {
new_fmt = new_fmt ++ "{:02}/{:02}/{:04}";
kinds = kinds ++ &[_]Kind{.d} ++ &[_]Kind{.m} ++ &[_]Kind{.Y};
},
'F' => {
new_fmt = new_fmt ++ "{:04}-{:02}-{:02}";
kinds = kinds ++ &[_]Kind{.Y} ++ &[_]Kind{.m} ++ &[_]Kind{.d};
},
'H' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.H};
},
'I' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.I};
},
'm' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.m};
},
'M' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.M};
},
'n' => new_fmt = new_fmt ++ "\n",
'p' => {
new_fmt = new_fmt ++ "{s}";
kinds = kinds ++ &[_]Kind{.p};
},
'P' => {
new_fmt = new_fmt ++ "{s}";
kinds = kinds ++ &[_]Kind{.P};
},
'r' => {
new_fmt = new_fmt ++ "{:02}:{:02}:{:02} {s}";
kinds = kinds ++ &[_]Kind{.I} ++ &[_]Kind{.M} ++ &[_]Kind{.S} ++ &[_]Kind{.p};
},
'R' => {
new_fmt = new_fmt ++ "{:02}:{:02}";
kinds = kinds ++ &[_]Kind{.H} ++ &[_]Kind{.M};
},
'S' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.S};
},
't' => new_fmt = new_fmt ++ "\t",
'T' => {
new_fmt = new_fmt ++ "{:02}:{:02}:{:02}";
kinds = kinds ++ &[_]Kind{.H} ++ &[_]Kind{.M} ++ &[_]Kind{.S};
},
'y' => {
new_fmt = new_fmt ++ "{:02}";
kinds = kinds ++ &[_]Kind{.y};
},
'Y' => {
new_fmt = new_fmt ++ "{:04}";
kinds = kinds ++ &[_]Kind{.Y};
},
'%' => new_fmt = new_fmt ++ "%",
else => new_fmt = new_fmt ++ ch,
}
percent = false;
} else if (ch == '%')
percent = true
else {
new_fmt = new_fmt ++ (&ch)[0..1];
}
}
if (percent) new_fmt = new_fmt ++ "%";
break :blk .{ new_fmt, kinds };
};
const Args = ArgTuple(kinds);
var new_args: Args = undefined;
inline for (kinds, 0..) |kind, i| {
new_args[i] = fieldFromKind(kind, datetime);
}
return writer.print(new_fmt, new_args);
}
const Kind = enum { Y, y, m, B, b, d, H, I, M, S, p, P };
fn KindField(comptime kind: Kind) type {
return switch (kind) {
.b => []const u8,
.B => []const u8,
.d => u5,
.H => u5,
.I => u5,
.m => u4,
.M => u6,
.P => []const u8,
.p => []const u8,
.S => u6,
.Y => u16,
.y => u16,
};
}
fn ArgTuple(comptime kinds: []const Kind) type {
comptime var types: [kinds.len]type = undefined;
for (kinds, 0..) |kind, i| {
types[i] = KindField(kind);
}
return std.meta.Tuple(&types);
}
fn fieldFromKind(comptime kind: Kind, datetime: DateTime) KindField(kind) {
return switch (kind) {
.b => monthToAbbref(datetime.month),
.B => monthToName(datetime.month),
.d => datetime.day,
.H => datetime.hour,
.I => datetime.hour % 12,
.m => datetime.month.numeric(),
.M => datetime.minute,
.P => if (datetime.hour < 12) "am" else "pm",
.p => if (datetime.hour < 12) "AM" else "PM",
.S => datetime.second,
.Y => datetime.year,
.y => datetime.year % 100,
};
}
fn monthToName(mon: std.time.epoch.Month) []const u8 {
return switch (mon) {
.jan => "January",
.feb => "February",
.mar => "March",
.apr => "April",
.may => "May",
.jun => "June",
.jul => "July",
.aug => "August",
.sep => "September",
.oct => "October",
.nov => "November",
.dec => "December",
};
}
fn monthToAbbref(mon: std.time.epoch.Month) []const u8 {
return switch (mon) {
.jan => "Jan",
.feb => "Feb",
.mar => "Mar",
.apr => "Apr",
.may => "May",
.jun => "Jun",
.jul => "Jul",
.aug => "Aug",
.sep => "Sep",
.oct => "Oct",
.nov => "Nov",
.dec => "Dec",
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment