@@ -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
89165pub 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+
204284test "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