I’m surprised that it’s not in the standard library. I know of a few Zig datetime libraries but sometimes all you want is to dump the current time as ISO 8601. So, I wanted to share this snippet in case anyone was interested. Look for TimeParts
below for the formatting code.
Features:
- Format a millisecond timestamp from
std.time.milliTimestamp()
. - No allocation.
- A packed struct of only 48 bits yet still loosely aligned on 8-bit byte boundary.
- Can format geographical timezones (i.e., merely shifting some hours).
- Easy to extend to support nanoseconds or more use cases.
I also build a crude logger on top of it.
const std = @import("std");
pub threadlocal var context: LogContext = .{};
/// When targeting at the "Windows" subsystem, the Windows OS will not create a
/// console for us, and thus the default console logging would not work. We
/// have to implement a custom logging function anyway.
///
/// Ref: `build.zig`
pub fn log(
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
context.log(level, scope, format, args) catch |e| {
std.log.defaultLog(.err, .log, "Failed to log {any}", .{.{
.level = level,
.scope = scope,
.format = format,
.args = args,
.reason = e,
}});
};
}
const timezone_shift = -5; // -05:00
pub const LogContext = struct {
file: ?std.fs.File = null,
log_console: bool = true,
pub fn log(
me: @This(),
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime fmt: []const u8,
args: anytype,
) !void {
const now: u64 = @intCast(std.time.milliTimestamp());
const shifted = now + timezone_shift * 1000 * 60 * 60;
const time = TimeParts.fromMsTimestamp(shifted);
const level_tag = switch (level) {
.debug => "[DEBUG] ",
.info => "[INFO] ",
.warn => "[WARNING] ",
.err => "[ERROR] ",
};
const scope_tag = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ") ";
const real_fmt = "{} " ++ level_tag ++ scope_tag ++ fmt ++ "\n";
const real_args = .{time} ++ args;
if (me.log_console) std.debug.print(real_fmt, real_args);
if (me.file) |file| try std.fmt.format(file.writer(), real_fmt, real_args);
}
};
const TimeParts = packed struct {
year: u12,
month: u4, // 16
day: u5,
hour: u5,
minute: u6, // 32
second: u6,
millisecond: u10, // 48
/// Not applicable to time previous to epoch.
///
/// Ported from: https://stackoverflow.com/a/11197532/12185226
pub fn fromMsTimestamp(timestamp: u64) @This() {
const ms = timestamp % 1000;
// Re-bias from 1970 to 1601:
// 1970 - 1601 = 369 = 3*100 + 17*4 + 1 years (incl. 89 leap days) =
// (3*100*(365+24/100) + 17*4*(365+1/4) + 1*365)*24*3600 seconds
var sec: u64 = (timestamp / 1000) + 11644473600;
// Remove multiples of 400 years (incl. 97 leap days)
const quadricentennials: u64 = sec / 12622780800; // 400*365.2425*24*3600
sec %= 12622780800;
// Remove multiples of 100 years (incl. 24 leap days), can't be more than 3
// (because multiples of 4*100=400 years (incl. leap days) have been removed)
const centennials: u64 = @min(3, sec / 3155673600); // 100*(365+24/100)*24*3600
sec -= centennials * 3155673600;
// Remove multiples of 4 years (incl. 1 leap day), can't be more than 24
// (because multiples of 25*4=100 years (incl. leap days) have been removed)
const quadrennials: u64 = @min(24, sec / 126230400); // 4*(365+1/4)*24*3600
sec -= quadrennials * 126230400;
// Remove multiples of years (incl. 0 leap days), can't be more than 3
// (because multiples of 4 years (incl. leap days) have been removed)
const annuals: u64 = @min(3, sec / 31536000); // 365*24*3600
sec -= annuals * 31536000;
const year = 1601 + quadricentennials * 400 + centennials * 100 + quadrennials * 4 + annuals;
const leap = (year % 4 == 0) and (year % 100 != 0 or (year % 400 == 0));
const yday = sec / 86400;
sec %= 86400;
const hour = sec / 3600;
sec %= 3600;
const minute = sec / 60;
sec %= 60;
const mday_list: [12]u9 = if (leap)
.{ 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
else
.{ 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
const month = for (mday_list, 0..) |x, i| {
if (yday < x) break i;
} else unreachable;
const mday = if (month == 0) yday else yday - mday_list[month - 1];
return .{
.year = @intCast(year),
.month = @intCast(month),
.day = @intCast(mday),
.hour = @intCast(hour),
.minute = @intCast(minute),
.second = @intCast(sec),
.millisecond = @intCast(ms),
};
}
/// Format into an ISO 8601 time string. Example: 2024-12-31T23:59:59.999Z
pub fn format(me: @This(), comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt;
var tz_buffer: [6]u8 = undefined;
var tz: []const u8 = "Z";
if (timezone_shift != 0) {
tz = try std.fmt.bufPrint(&tz_buffer, "{c}{:0>2}:00", .{
if (timezone_shift > 0) '+' else '-',
timezone_shift,
});
}
var buffer: [30]u8 = undefined;
const slice = try std.fmt.bufPrint(
&buffer,
"{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2},{:0>3}{s}",
.{ me.year, me.month + 1, me.day + 1, me.hour, me.minute, me.second, me.millisecond, tz },
);
try std.fmt.formatBuf(slice, options, writer);
}
};