Parsing timestamps and generating RFC3339 dates in Zig

important: date and time are complex and the code shown here is basic and not suitable for cases that require absolute correctness. I used it for setting the <updated> element of aolium's atom feeds (obviously, not mission critical)

When it comes to date and time handling, Zig's standard library pretty much has 1 capability: returning a unix timestamp via std.time.timestamp() (along with the milliTimestamp, microTimestamp and nanoTimestamp variants).

Timestamps are useful, but sometimes you need a bit more structure, such as:

pub const DateTime = struct {
  year: u16,
  month: u8,
  day: u8,
  hour: u8,
  minute: u8,
  second: u8,
};

How can we go from a timestamp to the above structure? I found a C implementation on the German Wikipedia page and converted it to Zig (and English), here's the result:

pub fn fromTimestamp(ts: u64) DateTime {
  const SECONDS_PER_DAY = 86400;
  const DAYS_PER_YEAR = 365;
  const DAYS_IN_4YEARS = 1461;
  const DAYS_IN_100YEARS = 36524;
  const DAYS_IN_400YEARS = 146097;
  const DAYS_BEFORE_EPOCH = 719468;

  const seconds_since_midnight: u64 = @rem(ts, SECONDS_PER_DAY);
  var day_n: u64 = DAYS_BEFORE_EPOCH + ts / SECONDS_PER_DAY;
  var temp: u64 = 0;

  temp = 4 * (day_n + DAYS_IN_100YEARS + 1) / DAYS_IN_400YEARS - 1;
  var year: u16 = @intCast(100 * temp);
  day_n -= DAYS_IN_100YEARS * temp + temp / 4;

  temp = 4 * (day_n + DAYS_PER_YEAR + 1) / DAYS_IN_4YEARS - 1;
  year += @intCast(temp);
  day_n -= DAYS_PER_YEAR * temp + temp / 4;

  var month: u8 = @intCast((5 * day_n + 2) / 153);
  const day: u8 = @intCast(day_n - (@as(u64, @intCast(month)) * 153 + 2) / 5 + 1);

  month += 3;
  if (month > 12) {
    month -= 12;
    year += 1;
  }

  return DateTime{
    .year = year,
    .month = month,
    .day = day,
    .hour = @intCast(seconds_since_midnight / 3600),
    .minute = @intCast(seconds_since_midnight % 3600 / 60),
    .second = @intCast(seconds_since_midnight % 60)
  };
}

Which can be called on any timestamp, but using std.time.timestamp() would look like:

const dt = fromTimestamp(@intCast(std.time.timestamp()));

One of the reasons we might want a more structured date/time representation is to format it. Obviously, date and time formatting can get very complicated, but thankfully RFC3339 is both simple and widely used, so supporting even just that one format can be a pretty big win:

pub fn toRFC3339(dt: DateTime) [20]u8 {
  var buf: [20]u8 = undefined;
  _ = std.fmt.formatIntBuf(buf[0..4], dt.year, 10, .lower, .{.width = 4, .fill = '0'});
  buf[4] = '-';
  paddingTwoDigits(buf[5..7], dt.month);
  buf[7] = '-';
  paddingTwoDigits(buf[8..10], dt.day);
  buf[10] = 'T';

  paddingTwoDigits(buf[11..13], dt.hour);
  buf[13] = ':';
  paddingTwoDigits(buf[14..16], dt.minute);
  buf[16] = ':';
  paddingTwoDigits(buf[17..19], dt.second);
  buf[19] = 'Z';

  return buf;
}

fn paddingTwoDigits(buf: *[2]u8, value: u8) void {
  switch (value) {
    0 => buf.* = "00".*,
    1 => buf.* = "01".*,
    2 => buf.* = "02".*,
    3 => buf.* = "03".*,
    4 => buf.* = "04".*,
    5 => buf.* = "05".*, 
    6 => buf.* = "06".*,
    7 => buf.* = "07".*,
    8 => buf.* = "08".*,
    9 => buf.* = "09".*,
    // todo: optionally can do all the way to 59 if you want
    else => _ = std.fmt.formatIntBuf(buf, value, 10, .lower, .{}),
  }
}

Leave a Comment

All comments are reviewed before being made public.