fediglam/src/util/DateTime.zig

140 lines
4.8 KiB
Zig

const DateTime = @This();
const std = @import("std");
const epoch = std.time.epoch;
pub const Duration = struct {
seconds: i64 = 0,
};
seconds_since_epoch: i64,
// Tries the following methods for parsing, in order:
// 1. treats the string as a RFC 3339 DateTime
// 2. treats the string as the number of seconds since epoch
pub fn parse(str: []const u8) !DateTime {
return if (parseRfc3339(str)) |v|
v
else |_| if (std.fmt.parseInt(i64, str, 10)) |v|
DateTime{ .seconds_since_epoch = v }
else |_|
error.UnknownFormat;
}
pub const JsonParseAs = []const u8;
pub fn add(self: DateTime, duration: Duration) DateTime {
return DateTime{
.seconds_since_epoch = self.seconds_since_epoch + duration.seconds,
};
}
pub fn sub(self: DateTime, duration: Duration) DateTime {
return DateTime{
.seconds_since_epoch = self.seconds_since_epoch - duration.seconds,
};
}
// TODO: Validate non-numeric aspects of datetime
// TODO: Don't panic on bad string
// TODO: Make seconds optional (see ActivityStreams 2.0 spec §2.3)
// TODO: Handle times before 1970
pub fn parseRfc3339(str: []const u8) !DateTime {
const year_num = try std.fmt.parseInt(u16, str[0..4], 10);
const month_num = try std.fmt.parseInt(std.meta.Tag(epoch.Month), str[5..7], 10);
const day_num = @as(i64, try std.fmt.parseInt(u9, str[8..10], 10));
const hour_num = @as(i64, try std.fmt.parseInt(u5, str[11..13], 10));
const minute_num = @as(i64, try std.fmt.parseInt(u6, str[14..16], 10));
const second_num = @as(i64, try std.fmt.parseInt(u6, str[17..19], 10));
const is_leap_year = epoch.isLeapYear(year_num);
const leap_days_preceding_epoch = comptime epoch.epoch_year / 4 - epoch.epoch_year / 100 + epoch.epoch_year / 400;
const leap_days_preceding_year = year_num / 4 - year_num / 100 + year_num / 400 - leap_days_preceding_epoch - if (is_leap_year) @as(i64, 1) else 0;
const epoch_day = (year_num - epoch.epoch_year) * 365 + leap_days_preceding_year + year_day: {
var days_preceding_month: i64 = 0;
var month_i: i64 = 1;
while (month_i < month_num) : (month_i += 1) {
days_preceding_month += epoch.getDaysInMonth(if (is_leap_year) .leap else .not_leap, @intToEnum(epoch.Month, month_i));
}
break :year_day days_preceding_month + day_num;
};
const day_second = (hour_num * 60 + minute_num) * 60 + second_num;
return DateTime{
.seconds_since_epoch = epoch_day * epoch.secs_per_day + day_second,
};
}
const is_test = @import("builtin").is_test;
const test_utils = struct {
pub threadlocal var test_now_timestamp: i64 = 1356076800;
};
pub usingnamespace if (is_test) test_utils else struct {};
pub fn now() DateTime {
if (comptime is_test) return .{ .seconds_since_epoch = test_utils.test_now_timestamp };
return .{ .seconds_since_epoch = std.time.timestamp() };
}
pub fn isAfter(lhs: DateTime, rhs: DateTime) bool {
return lhs.seconds_since_epoch > rhs.seconds_since_epoch;
}
pub fn epochSeconds(value: DateTime) std.time.epoch.EpochSeconds {
return .{ .secs = std.math.cast(u64, value.seconds_since_epoch) orelse 0 };
}
pub fn year(value: DateTime) std.time.epoch.Year {
return value.epochSeconds().getEpochDay().calculateYearDay().year;
}
pub fn month(value: DateTime) std.time.epoch.Month {
return value.epochSeconds().getEpochDay().calculateYearDay().calculateMonthDay().month;
}
pub fn day(value: DateTime) u9 {
return value.epochSeconds().getEpochDay().calculateYearDay().calculateMonthDay().day_index;
}
pub fn hour(value: DateTime) u5 {
return value.epochSeconds().getDaySeconds().getHoursIntoDay();
}
pub fn minute(value: DateTime) u6 {
return value.epochSeconds().getDaySeconds().getMinutesIntoHour();
}
pub fn second(value: DateTime) u6 {
return value.epochSeconds().getDaySeconds().getSecondsIntoMinute();
}
const array_len = 20;
pub fn toCharArray(value: DateTime) [array_len]u8 {
var buf: [array_len]u8 = undefined;
_ = std.fmt.bufPrint(&buf, "{}", .{value}) catch unreachable;
return buf;
}
pub fn toCharArrayZ(value: DateTime) [array_len + 1:0]u8 {
var buf: [array_len + 1:0]u8 = undefined;
_ = std.fmt.bufPrintZ(&buf, "{}", .{value}) catch unreachable;
return buf;
}
pub fn format(value: DateTime, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
if (comptime std.ascii.eqlIgnoreCase(fmt, "rfc3339") or fmt.len == 0) {
return std.fmt.format(
writer,
"{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}Z",
.{ value.year(), value.month().numeric(), value.day(), value.hour(), value.minute(), value.second() },
);
} else @compileError("Unknown DateTime format " ++ fmt);
}
pub fn jsonStringify(value: DateTime, _: std.json.StringifyOptions, writer: anytype) !void {
try std.fmt.format(writer, "\"{rfc3339}\"", .{value});
}