From ee3171cb52c5d4767f18f8c8e340842e709b16fc Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 7 Jun 2021 23:09:45 -0600 Subject: [PATCH 1/4] add functions to decode an epoch timestamp The code added here is alternative to the libc gmtime function. This function takes a unix epoch timestamp and decodes it into things like the year/day/time/etc. 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 | 5 ++ lib/std/time.zig | 4 + lib/std/time/epoch.zig | 179 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 188 insertions(+) diff --git a/lib/std/math.zig b/lib/std/math.zig index ac28cbb4e2d2..89fbaaf6c51c 100644 --- a/lib/std/math.zig +++ b/lib/std/math.zig @@ -1420,3 +1420,8 @@ 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) IntFittingRange(0, denom - 1) { + return @intCast(IntFittingRange(0, denom - 1), @mod(num, denom)); +} 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 }); +} From 87bd2b7f0c96f1a550a5f2fc9ab74819e9fe4ef1 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 8 Jun 2021 08:14:11 -0600 Subject: [PATCH 2/4] set Month.jan to 1 instead of 0 to avoid +1 in conversion to numeric --- lib/std/time/epoch.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/std/time/epoch.zig b/lib/std/time/epoch.zig index e468a38c5312..81e7577aa303 100644 --- a/lib/std/time/epoch.zig +++ b/lib/std/time/epoch.zig @@ -72,7 +72,7 @@ pub fn getDaysInYear(year: Year) u9 { pub const YearLeapKind = enum(u1) { not_leap, leap }; pub const Month = enum(u4) { - jan, + jan = 1, feb, mar, apr, @@ -88,7 +88,7 @@ pub const Month = enum(u4) { /// 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; + return @enumToInt(self); } }; From 31499746397b3e465cd506050276a18c543aa878 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Wed, 9 Jun 2021 08:22:37 -0600 Subject: [PATCH 3/4] add another test --- lib/std/time/epoch.zig | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/std/time/epoch.zig b/lib/std/time/epoch.zig index 81e7577aa303..bd8aaf8cc22b 100644 --- a/lib/std/time/epoch.zig +++ b/lib/std/time/epoch.zig @@ -124,7 +124,7 @@ pub const YearAndDay = struct { 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) + if (days_left < days_in_month) break; days_left -= days_in_month; month = @intToEnum(Month, @enumToInt(month) + 1); @@ -214,8 +214,19 @@ test "epoch decoding" { .month = .jan, .day_index = 0, }, .{ .hours_into_day = 0, .minutes_into_hour = 0, .seconds_into_minute = 0 }); + + try testEpoch(31535999, .{ .year = 1970, .day = 364 }, .{ + .month = .dec, + .day_index = 30, + }, .{ .hours_into_day = 23, .minutes_into_hour = 59, .seconds_into_minute = 59 }); + 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 }); + + try testEpoch(1625159473, .{ .year = 2021, .day = 31 + 28 + 31 + 30 + 31 + 30 }, .{ + .month = .jul, + .day_index = 0, + }, .{ .hours_into_day = 17, .minutes_into_hour = 11, .seconds_into_minute = 13 }); } From 73cce7e31cbe9b2e826c183fb83d6b2ebcb6746e Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Mon, 30 Aug 2021 08:27:59 -0600 Subject: [PATCH 4/4] remove unnecessary comptimeMod --- lib/std/time/epoch.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/std/time/epoch.zig b/lib/std/time/epoch.zig index bd8aaf8cc22b..3e1be03cabcf 100644 --- a/lib/std/time/epoch.zig +++ b/lib/std/time/epoch.zig @@ -51,11 +51,11 @@ 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) + if (@mod(year, 4) != 0) return false; - if (math.comptimeMod(year, 100) != 0) + if (@mod(year, 100) != 0) return true; - return (0 == math.comptimeMod(year, 400)); + return (0 == @mod(year, 400)); } test "isLeapYear" { @@ -165,7 +165,7 @@ pub const DaySeconds = struct { } /// 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)); + return @intCast(u6, @divTrunc(@mod(self.secs, 3600), 60)); } /// the number of seconds past the start of the minute (0 to 59) pub fn getSecondsIntoMinute(self: DaySeconds) u6 {