Skip to content

Commit b409e41

Browse files
committed
Extend std.time.epoch to calc seconds from epoch from datetime
1 parent 4c2de34 commit b409e41

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

lib/std/time/epoch.zig

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,82 @@ pub const Month = enum(u4) {
8585
}
8686
};
8787

88+
/// Day of month (1 to 31)
89+
pub const Day = u5;
90+
91+
/// Hour of day (0 to 23)
92+
pub const Hour = u5;
93+
94+
/// Minute of hour (0 to 59)
95+
pub const Minute = u6;
96+
97+
/// Second of minute (0 to 59)
98+
pub const Second = u6;
99+
100+
pub const Datetime = struct {
101+
year: Year,
102+
month: Month,
103+
day: Day,
104+
hour: Hour,
105+
minute: Minute,
106+
second: Second,
107+
108+
pub fn asEpochSeconds(d: *const Datetime) !EpochSeconds {
109+
// Validate input ranges
110+
if (d.year < epoch_year) return error.InvalidYear;
111+
if (d.day == 0 or d.day > getDaysInMonth(d.year, d.month)) return error.InvalidDay;
112+
if (d.hour >= 24) return error.InvalidHour;
113+
if (d.minute >= 60) return error.InvalidMinute;
114+
if (d.second >= 60) return error.InvalidSecond;
115+
116+
// Calculate days from epoch to start of the given year
117+
const total_years = d.year - epoch_year;
118+
const total_leap_years = countLeapYearsBetween(epoch_year, d.year);
119+
var total_days = @as(u47, @intCast(total_years)) * 365 + @as(u47, @intCast(total_leap_years));
120+
121+
// Add days for complete months in the given year
122+
var m: Month = .jan;
123+
while (@intFromEnum(m) < @intFromEnum(d.month)) {
124+
total_days += getDaysInMonth(d.year, m);
125+
m = @enumFromInt(@intFromEnum(m) + 1);
126+
}
127+
128+
// Add remaining days (subtract 1 because day is 1-indexed)
129+
total_days += d.day - 1;
130+
131+
// Convert to seconds and add time components
132+
var total_secs: u64 = @as(u64, @intCast(total_days)) * @as(u64, @intCast(secs_per_day));
133+
total_secs += @as(u64, @intCast(d.hour)) * 3600;
134+
total_secs += @as(u64, @intCast(d.minute)) * 60;
135+
total_secs += d.second;
136+
137+
return EpochSeconds{ .secs = total_secs };
138+
}
139+
};
140+
141+
/// Counts the number of leap years in the range [start_year, end_year).
142+
/// The end_year is exclusive.
143+
pub fn countLeapYearsBetween(start_year: Year, end_year: Year) u15 {
144+
// We retrun u15 because `Year` is u16 and leap year is every 4 years.
145+
// (2 ** 16) / 4 = 2 ** 14, so u14 is clearly the best fit. But every 100
146+
// years is also leap year, which will result in very few extra leap years,
147+
// so we are adding 1 bit to make room for that, resulting in u15.
148+
149+
if (end_year <= start_year) return 0;
150+
151+
// Count leap years from year 0 to end_year-1, then subtract those before start_year
152+
const leaps_before_end = countLeapYearsFromZero(end_year - 1);
153+
const leaps_before_start = if (start_year > 0) countLeapYearsFromZero(start_year - 1) else 0;
154+
155+
return leaps_before_end - leaps_before_start;
156+
}
157+
158+
/// Counts leap years from year 0 to the given year (inclusive).
159+
fn countLeapYearsFromZero(y: Year) u15 {
160+
// Divisible by 4, minus centuries (divisible by 100), plus quad-centuries (divisible by 400)
161+
return @as(u15, @intCast((y / 4) - (y / 100) + (y / 400)));
162+
}
163+
88164
/// Get the number of days in the given month and year
89165
pub fn getDaysInMonth(year: Year, month: Month) u5 {
90166
return switch (month) {
@@ -201,6 +277,10 @@ fn testEpoch(secs: u64, expected_year_day: YearAndDay, expected_month_day: Month
201277
try testing.expectEqual(expected_day_seconds.seconds_into_minute, day_seconds.getSecondsIntoMinute());
202278
}
203279

280+
fn testDatetimeToEpochSeconds(epoc_seconds: u64, dt: Datetime) !void {
281+
try testing.expectEqual(epoc_seconds, (try dt.asEpochSeconds()).secs);
282+
}
283+
204284
test "epoch decoding" {
205285
try testEpoch(0, .{ .year = 1970, .day = 0 }, .{
206286
.month = .jan,
@@ -222,3 +302,81 @@ test "epoch decoding" {
222302
.day_index = 0,
223303
}, .{ .hours_into_day = 17, .minutes_into_hour = 11, .seconds_into_minute = 13 });
224304
}
305+
306+
test "datetime to epochseconds" {
307+
// epoc time exactly
308+
try testDatetimeToEpochSeconds(0, .{
309+
.year = 1970,
310+
.month = .jan,
311+
.day = 1,
312+
.hour = 0,
313+
.minute = 0,
314+
.second = 0,
315+
});
316+
317+
// last second of a year
318+
try testDatetimeToEpochSeconds(31535999, .{
319+
.year = 1970,
320+
.month = .dec,
321+
.day = 31,
322+
.hour = 23,
323+
.minute = 59,
324+
.second = 59,
325+
});
326+
327+
// first second of next year
328+
try testDatetimeToEpochSeconds(31536000, .{
329+
.year = 1971,
330+
.month = .jan,
331+
.day = 1,
332+
.hour = 0,
333+
.minute = 0,
334+
.second = 0,
335+
});
336+
337+
// leap year
338+
try testDatetimeToEpochSeconds(68256000, .{
339+
.year = 1972,
340+
.month = .mar,
341+
.day = 1,
342+
.hour = 0,
343+
.minute = 0,
344+
.second = 0,
345+
});
346+
347+
// super far in the future
348+
try testDatetimeToEpochSeconds(16725225600, .{
349+
.year = 2500,
350+
.month = .jan,
351+
.day = 1,
352+
.hour = 0,
353+
.minute = 0,
354+
.second = 0,
355+
});
356+
357+
// invalid input
358+
const dt = Datetime{
359+
.year = 1970,
360+
.month = .feb,
361+
.day = 29, // invalid because it's not leap year
362+
.hour = 0,
363+
.minute = 0,
364+
.second = 0,
365+
};
366+
try testing.expectError(error.InvalidDay, dt.asEpochSeconds());
367+
}
368+
369+
test "countLeapYearsBetween" {
370+
// Between 1970-1980: 1972, 1976 = 2 leap years
371+
try std.testing.expectEqual(@as(u47, 2), countLeapYearsBetween(1970, 1980));
372+
373+
// Between 1970-2000: excludes 2000 which is a leap year (divisible by 400)
374+
try std.testing.expectEqual(@as(u47, 7), countLeapYearsBetween(1970, 2000));
375+
376+
// Between 1970-2001: adds 2000
377+
try std.testing.expectEqual(@as(u47, 8), countLeapYearsBetween(1970, 2001));
378+
379+
// Century test: 1900 is NOT a leap year, 2000 IS
380+
try std.testing.expectEqual(@as(u47, 0), countLeapYearsBetween(1900, 1901));
381+
try std.testing.expectEqual(@as(u47, 1), countLeapYearsBetween(2000, 2001));
382+
}

0 commit comments

Comments
 (0)