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}); }