Skip to content

Commit

Permalink
add functions to decode an epoch timestamp
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
marler8997 committed Jun 8, 2021
1 parent 7462b0e commit 360f5e5
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
30 changes: 30 additions & 0 deletions lib/std/math.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
4 changes: 4 additions & 0 deletions lib/std/time.zig
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,7 @@ test "Timer" {
timer.reset();
try testing.expect(timer.read() < time_1);
}

test {
_ = @import("time/epoch.zig");
}
179 changes: 179 additions & 0 deletions lib/std/time/epoch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
}

0 comments on commit 360f5e5

Please sign in to comment.