From 360f5e54d54ae829b573de4df20ff8f538fc2613 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 7 Jun 2021 23:09:45 -0600 Subject: [PATCH] add functions to decode an epoch timestamp The function added here are an alternative to the libc gmtime function. I looked at various libc implementations to see how it was implemented and this appears to be correct. I reorganized it so that applications can choose which data they need rather than calcualting it all in a single function. The data structures layout the order of operations required to decode various things like the year/month or time of day. --- lib/std/math.zig | 30 +++++++ lib/std/time.zig | 4 + lib/std/time/epoch.zig | 179 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) diff --git a/lib/std/math.zig b/lib/std/math.zig index ac28cbb4e2d2..e62acb9ec48d 100644 --- a/lib/std/math.zig +++ b/lib/std/math.zig @@ -1420,3 +1420,33 @@ test "boolMask" { try runTest(); comptime try runTest(); } + +/// Return the mod of `num` with the smallest integer type +pub fn comptimeMod(num: anytype, denom: comptime_int) UintFromMax(denom - 1) { + return @intCast(UintFromMax(denom - 1), @mod(num, denom)); +} + +/// The smallest unsigned integer type that can represent `max_value`. +pub fn UintFromMax(comptime max_value: comptime_int) type { + std.debug.assert(max_value >= 0); + comptime var bits = 0; + { + comptime var s = max_value; + inline while (s != 0) : (s >>= 1) { + bits += 1; + } + } + return std.meta.Int(.unsigned, bits); +} + +test "UintFromMax" { + try testing.expectEqual(u0, UintFromMax(0)); + try testing.expectEqual(u1, UintFromMax(1)); + try testing.expectEqual(u2, UintFromMax(2)); + try testing.expectEqual(u2, UintFromMax(3)); + try testing.expectEqual(u3, UintFromMax(4)); + try testing.expectEqual(u3, UintFromMax(7)); + try testing.expectEqual(u4, UintFromMax(8)); + try testing.expectEqual(u8, UintFromMax(255)); + try testing.expectEqual(u9, UintFromMax(256)); +} diff --git a/lib/std/time.zig b/lib/std/time.zig index 99304af46ae0..1ef9f77e719a 100644 --- a/lib/std/time.zig +++ b/lib/std/time.zig @@ -292,3 +292,7 @@ test "Timer" { timer.reset(); try testing.expect(timer.read() < time_1); } + +test { + _ = @import("time/epoch.zig"); +} diff --git a/lib/std/time/epoch.zig b/lib/std/time/epoch.zig index 75bddc71c390..e468a38c5312 100644 --- a/lib/std/time/epoch.zig +++ b/lib/std/time/epoch.zig @@ -5,6 +5,9 @@ // and substantial portions of the software. //! Epoch reference times in terms of their difference from //! UTC 1970-01-01 in seconds. +const std = @import("../std.zig"); +const testing = std.testing; +const math = std.math; /// Jan 01, 1970 AD pub const posix = 0; @@ -40,3 +43,179 @@ pub const morphos = amiga; pub const brew = gps; pub const atsc = gps; pub const go = clr; + +/// The type that holds the current year, i.e. 2016 +pub const Year = u16; + +pub const epoch_year = 1970; +pub const secs_per_day: u17 = 24 * 60 * 60; + +pub fn isLeapYear(year: Year) bool { + if (math.comptimeMod(year, 4) != 0) + return false; + if (math.comptimeMod(year, 100) != 0) + return true; + return (0 == math.comptimeMod(year, 400)); +} + +test "isLeapYear" { + try testing.expectEqual(false, isLeapYear(2095)); + try testing.expectEqual(true, isLeapYear(2096)); + try testing.expectEqual(false, isLeapYear(2100)); + try testing.expectEqual(true, isLeapYear(2400)); +} + +pub fn getDaysInYear(year: Year) u9 { + return if (isLeapYear(year)) 366 else 365; +} + +pub const YearLeapKind = enum(u1) { not_leap, leap }; + +pub const Month = enum(u4) { + jan, + feb, + mar, + apr, + may, + jun, + jul, + aug, + sep, + oct, + nov, + dec, + + /// return the numeric calendar value for the given month + /// i.e. jan=1, feb=2, etc + pub fn numeric(self: Month) u4 { + return @enumToInt(self) + 1; + } +}; + +/// Get the number of days in the given month +pub fn getDaysInMonth(leap_year: YearLeapKind, month: Month) u5 { + return switch (month) { + .jan => 31, + .feb => @as(u5, switch (leap_year) { + .leap => 29, + .not_leap => 28, + }), + .mar => 31, + .apr => 30, + .may => 31, + .jun => 30, + .jul => 31, + .aug => 31, + .sep => 30, + .oct => 31, + .nov => 30, + .dec => 31, + }; +} + +pub const YearAndDay = struct { + year: Year, + /// The number of days into the year (0 to 365) + day: u9, + + pub fn calculateMonthDay(self: YearAndDay) MonthAndDay { + var month: Month = .jan; + var days_left = self.day; + const leap_kind: YearLeapKind = if (isLeapYear(self.year)) .leap else .not_leap; + while (true) { + const days_in_month = getDaysInMonth(leap_kind, month); + if (days_left <= days_in_month) + break; + days_left -= days_in_month; + month = @intToEnum(Month, @enumToInt(month) + 1); + } + return .{ .month = month, .day_index = @intCast(u5, days_left) }; + } +}; + +pub const MonthAndDay = struct { + month: Month, + day_index: u5, // days into the month (0 to 30) +}; + +// days since epoch Oct 1, 1970 +pub const EpochDay = struct { + day: u47, // u47 = u64 - u17 (because day = sec(u64) / secs_per_day(u17) + pub fn calculateYearDay(self: EpochDay) YearAndDay { + var year_day = self.day; + var year: Year = epoch_year; + while (true) { + const year_size = getDaysInYear(year); + if (year_day < year_size) + break; + year_day -= year_size; + year += 1; + } + return .{ .year = year, .day = @intCast(u9, year_day) }; + } +}; + +/// seconds since start of day +pub const DaySeconds = struct { + secs: u17, // max is 24*60*60 = 86400 + + /// the number of hours past the start of the day (0 to 11) + pub fn getHoursIntoDay(self: DaySeconds) u5 { + return @intCast(u5, @divTrunc(self.secs, 3600)); + } + /// the number of minutes past the hour (0 to 59) + pub fn getMinutesIntoHour(self: DaySeconds) u6 { + return @intCast(u6, @divTrunc(math.comptimeMod(self.secs, 3600), 60)); + } + /// the number of seconds past the start of the minute (0 to 59) + pub fn getSecondsIntoMinute(self: DaySeconds) u6 { + return math.comptimeMod(self.secs, 60); + } +}; + +/// seconds since epoch Oct 1, 1970 at 12:00 AM +pub const EpochSeconds = struct { + secs: u64, + + /// Returns the number of days since the epoch as an EpochDay. + /// Use EpochDay to get information about the day of this time. + pub fn getEpochDay(self: EpochSeconds) EpochDay { + return EpochDay{ .day = @intCast(u47, @divTrunc(self.secs, secs_per_day)) }; + } + + /// Returns the number of seconds into the day as DaySeconds. + /// Use DaySeconds to get information about the time. + pub fn getDaySeconds(self: EpochSeconds) DaySeconds { + return DaySeconds{ .secs = math.comptimeMod(self.secs, secs_per_day) }; + } +}; + +fn testEpoch(secs: u64, expected_year_day: YearAndDay, expected_month_day: MonthAndDay, expected_day_seconds: struct { + /// 0 to 23 + hours_into_day: u5, + /// 0 to 59 + minutes_into_hour: u6, + /// 0 to 59 + seconds_into_minute: u6, +}) !void { + const epoch_seconds = EpochSeconds{ .secs = secs }; + const epoch_day = epoch_seconds.getEpochDay(); + const day_seconds = epoch_seconds.getDaySeconds(); + const year_day = epoch_day.calculateYearDay(); + try testing.expectEqual(expected_year_day, year_day); + try testing.expectEqual(expected_month_day, year_day.calculateMonthDay()); + try testing.expectEqual(expected_day_seconds.hours_into_day, day_seconds.getHoursIntoDay()); + try testing.expectEqual(expected_day_seconds.minutes_into_hour, day_seconds.getMinutesIntoHour()); + try testing.expectEqual(expected_day_seconds.seconds_into_minute, day_seconds.getSecondsIntoMinute()); +} + +test "epoch decoding" { + try testEpoch(0, .{ .year = 1970, .day = 0 }, .{ + .month = .jan, + .day_index = 0, + }, .{ .hours_into_day = 0, .minutes_into_hour = 0, .seconds_into_minute = 0 }); + try testEpoch(1622924906, .{ .year = 2021, .day = 31 + 28 + 31 + 30 + 31 + 4 }, .{ + .month = .jun, + .day_index = 4, + }, .{ .hours_into_day = 20, .minutes_into_hour = 28, .seconds_into_minute = 26 }); +}