Skip to content

Commit 010ca69

Browse files
authored
add functions to decode an epoch timestamp (#9040)
* 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. * set Month.jan to 1 instead of 0 to avoid +1 in conversion to numeric * add another test * remove unnecessary comptimeMod
1 parent 85f065a commit 010ca69

File tree

3 files changed

+199
-0
lines changed

3 files changed

+199
-0
lines changed

lib/std/math.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,3 +1415,8 @@ test "boolMask" {
14151415
try runTest();
14161416
comptime try runTest();
14171417
}
1418+
1419+
/// Return the mod of `num` with the smallest integer type
1420+
pub fn comptimeMod(num: anytype, denom: comptime_int) IntFittingRange(0, denom - 1) {
1421+
return @intCast(IntFittingRange(0, denom - 1), @mod(num, denom));
1422+
}

lib/std/time.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,7 @@ test "Timer" {
285285
timer.reset();
286286
try testing.expect(timer.read() < time_1);
287287
}
288+
289+
test {
290+
_ = @import("time/epoch.zig");
291+
}

lib/std/time/epoch.zig

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! Epoch reference times in terms of their difference from
22
//! UTC 1970-01-01 in seconds.
3+
const std = @import("../std.zig");
4+
const testing = std.testing;
5+
const math = std.math;
36

47
/// Jan 01, 1970 AD
58
pub const posix = 0;
@@ -35,3 +38,190 @@ pub const morphos = amiga;
3538
pub const brew = gps;
3639
pub const atsc = gps;
3740
pub const go = clr;
41+
42+
/// The type that holds the current year, i.e. 2016
43+
pub const Year = u16;
44+
45+
pub const epoch_year = 1970;
46+
pub const secs_per_day: u17 = 24 * 60 * 60;
47+
48+
pub fn isLeapYear(year: Year) bool {
49+
if (@mod(year, 4) != 0)
50+
return false;
51+
if (@mod(year, 100) != 0)
52+
return true;
53+
return (0 == @mod(year, 400));
54+
}
55+
56+
test "isLeapYear" {
57+
try testing.expectEqual(false, isLeapYear(2095));
58+
try testing.expectEqual(true, isLeapYear(2096));
59+
try testing.expectEqual(false, isLeapYear(2100));
60+
try testing.expectEqual(true, isLeapYear(2400));
61+
}
62+
63+
pub fn getDaysInYear(year: Year) u9 {
64+
return if (isLeapYear(year)) 366 else 365;
65+
}
66+
67+
pub const YearLeapKind = enum(u1) { not_leap, leap };
68+
69+
pub const Month = enum(u4) {
70+
jan = 1,
71+
feb,
72+
mar,
73+
apr,
74+
may,
75+
jun,
76+
jul,
77+
aug,
78+
sep,
79+
oct,
80+
nov,
81+
dec,
82+
83+
/// return the numeric calendar value for the given month
84+
/// i.e. jan=1, feb=2, etc
85+
pub fn numeric(self: Month) u4 {
86+
return @enumToInt(self);
87+
}
88+
};
89+
90+
/// Get the number of days in the given month
91+
pub fn getDaysInMonth(leap_year: YearLeapKind, month: Month) u5 {
92+
return switch (month) {
93+
.jan => 31,
94+
.feb => @as(u5, switch (leap_year) {
95+
.leap => 29,
96+
.not_leap => 28,
97+
}),
98+
.mar => 31,
99+
.apr => 30,
100+
.may => 31,
101+
.jun => 30,
102+
.jul => 31,
103+
.aug => 31,
104+
.sep => 30,
105+
.oct => 31,
106+
.nov => 30,
107+
.dec => 31,
108+
};
109+
}
110+
111+
pub const YearAndDay = struct {
112+
year: Year,
113+
/// The number of days into the year (0 to 365)
114+
day: u9,
115+
116+
pub fn calculateMonthDay(self: YearAndDay) MonthAndDay {
117+
var month: Month = .jan;
118+
var days_left = self.day;
119+
const leap_kind: YearLeapKind = if (isLeapYear(self.year)) .leap else .not_leap;
120+
while (true) {
121+
const days_in_month = getDaysInMonth(leap_kind, month);
122+
if (days_left < days_in_month)
123+
break;
124+
days_left -= days_in_month;
125+
month = @intToEnum(Month, @enumToInt(month) + 1);
126+
}
127+
return .{ .month = month, .day_index = @intCast(u5, days_left) };
128+
}
129+
};
130+
131+
pub const MonthAndDay = struct {
132+
month: Month,
133+
day_index: u5, // days into the month (0 to 30)
134+
};
135+
136+
// days since epoch Oct 1, 1970
137+
pub const EpochDay = struct {
138+
day: u47, // u47 = u64 - u17 (because day = sec(u64) / secs_per_day(u17)
139+
pub fn calculateYearDay(self: EpochDay) YearAndDay {
140+
var year_day = self.day;
141+
var year: Year = epoch_year;
142+
while (true) {
143+
const year_size = getDaysInYear(year);
144+
if (year_day < year_size)
145+
break;
146+
year_day -= year_size;
147+
year += 1;
148+
}
149+
return .{ .year = year, .day = @intCast(u9, year_day) };
150+
}
151+
};
152+
153+
/// seconds since start of day
154+
pub const DaySeconds = struct {
155+
secs: u17, // max is 24*60*60 = 86400
156+
157+
/// the number of hours past the start of the day (0 to 11)
158+
pub fn getHoursIntoDay(self: DaySeconds) u5 {
159+
return @intCast(u5, @divTrunc(self.secs, 3600));
160+
}
161+
/// the number of minutes past the hour (0 to 59)
162+
pub fn getMinutesIntoHour(self: DaySeconds) u6 {
163+
return @intCast(u6, @divTrunc(@mod(self.secs, 3600), 60));
164+
}
165+
/// the number of seconds past the start of the minute (0 to 59)
166+
pub fn getSecondsIntoMinute(self: DaySeconds) u6 {
167+
return math.comptimeMod(self.secs, 60);
168+
}
169+
};
170+
171+
/// seconds since epoch Oct 1, 1970 at 12:00 AM
172+
pub const EpochSeconds = struct {
173+
secs: u64,
174+
175+
/// Returns the number of days since the epoch as an EpochDay.
176+
/// Use EpochDay to get information about the day of this time.
177+
pub fn getEpochDay(self: EpochSeconds) EpochDay {
178+
return EpochDay{ .day = @intCast(u47, @divTrunc(self.secs, secs_per_day)) };
179+
}
180+
181+
/// Returns the number of seconds into the day as DaySeconds.
182+
/// Use DaySeconds to get information about the time.
183+
pub fn getDaySeconds(self: EpochSeconds) DaySeconds {
184+
return DaySeconds{ .secs = math.comptimeMod(self.secs, secs_per_day) };
185+
}
186+
};
187+
188+
fn testEpoch(secs: u64, expected_year_day: YearAndDay, expected_month_day: MonthAndDay, expected_day_seconds: struct {
189+
/// 0 to 23
190+
hours_into_day: u5,
191+
/// 0 to 59
192+
minutes_into_hour: u6,
193+
/// 0 to 59
194+
seconds_into_minute: u6,
195+
}) !void {
196+
const epoch_seconds = EpochSeconds{ .secs = secs };
197+
const epoch_day = epoch_seconds.getEpochDay();
198+
const day_seconds = epoch_seconds.getDaySeconds();
199+
const year_day = epoch_day.calculateYearDay();
200+
try testing.expectEqual(expected_year_day, year_day);
201+
try testing.expectEqual(expected_month_day, year_day.calculateMonthDay());
202+
try testing.expectEqual(expected_day_seconds.hours_into_day, day_seconds.getHoursIntoDay());
203+
try testing.expectEqual(expected_day_seconds.minutes_into_hour, day_seconds.getMinutesIntoHour());
204+
try testing.expectEqual(expected_day_seconds.seconds_into_minute, day_seconds.getSecondsIntoMinute());
205+
}
206+
207+
test "epoch decoding" {
208+
try testEpoch(0, .{ .year = 1970, .day = 0 }, .{
209+
.month = .jan,
210+
.day_index = 0,
211+
}, .{ .hours_into_day = 0, .minutes_into_hour = 0, .seconds_into_minute = 0 });
212+
213+
try testEpoch(31535999, .{ .year = 1970, .day = 364 }, .{
214+
.month = .dec,
215+
.day_index = 30,
216+
}, .{ .hours_into_day = 23, .minutes_into_hour = 59, .seconds_into_minute = 59 });
217+
218+
try testEpoch(1622924906, .{ .year = 2021, .day = 31 + 28 + 31 + 30 + 31 + 4 }, .{
219+
.month = .jun,
220+
.day_index = 4,
221+
}, .{ .hours_into_day = 20, .minutes_into_hour = 28, .seconds_into_minute = 26 });
222+
223+
try testEpoch(1625159473, .{ .year = 2021, .day = 31 + 28 + 31 + 30 + 31 + 30 }, .{
224+
.month = .jul,
225+
.day_index = 0,
226+
}, .{ .hours_into_day = 17, .minutes_into_hour = 11, .seconds_into_minute = 13 });
227+
}

0 commit comments

Comments
 (0)