From 05d83abcf3c7c655288fd84c0ee97f6232831d2c Mon Sep 17 00:00:00 2001 From: frmdstryr Date: Sun, 6 Apr 2025 15:59:15 -0400 Subject: [PATCH] Draft: Implement DST --- src/datetime.zig | 136 ++++++++++++++++++++++++++++++++++++++-------- src/timezones.zig | 92 ++++++++++++++++++++----------- 2 files changed, 174 insertions(+), 54 deletions(-) diff --git a/src/datetime.zig b/src/datetime.zig index a08aa3b..4c9bca7 100644 --- a/src/datetime.zig +++ b/src/datetime.zig @@ -64,6 +64,15 @@ pub const Month = enum(u4) { } }; +pub const Occurance = enum(u3) { + First = 0, + Second = 1, + Third = 2, + Fourth = 3, + Fifth = 4, + Last = 5, +}; + test "month-parse-abbr" { try testing.expectEqual(try Month.parseAbbr("Jan"), .January); try testing.expectEqual(try Month.parseAbbr("Oct"), .October); @@ -216,6 +225,26 @@ pub const Date = struct { }; } + // Create the date for a given year, month, and n'th weekday + pub fn fromNthWeekday(year: u32, month: u32, nth: Occurance, wd: Weekday) !Date { + if (year < MIN_YEAR or year > MAX_YEAR) return error.InvalidDate; + if (month < 1 or month > 12) return error.InvalidDate; + // Since we just validated the ranges we can now savely cast + const start = ymd2ord(@intCast(year), @intCast(month), 1); + const dow = start % 7; + const exp_dow: u32 = @intFromEnum(wd); + const n: u32 = @intFromEnum(nth); + var day = if (exp_dow >= dow) (exp_dow - dow) else (7 + exp_dow - dow); + day += 1 + n * 7; + if (nth == .Last) { + const max = daysInMonth(year, month); + while (day > max) { + day -= 7; + } + } + return Date.create(@intCast(year), @intCast(month), @intCast(day)); + } + // Return a copy of the date pub fn copy(self: Date) !Date { return Date.create(self.year, self.month, self.day); @@ -468,10 +497,15 @@ pub const Date = struct { return @tagName(self.dayOfWeek()); } + // Return the month as an enum + pub fn monthOfYear(self: Date) Month { + assert(self.month >= 1 and self.month <= 12); + return @enumFromInt(self.month); + } + // Return the name of the day of the month, eg "January" pub fn monthName(self: Date) []const u8 { - assert(self.month >= 1 and self.month <= 12); - return @tagName(@as(Month, @enumFromInt(self.month))); + return @tagName(self.monthOfYear()); } // ------------------------------------------------------------------------ @@ -602,6 +636,34 @@ test "date-from-seconds" { // try testing.expectEqual(date.toSeconds(), tmax); } +test "date-from-nth-weekday" { + var date = Date.fromNthWeekday(2025, 4, .First, .Monday); + try testing.expectEqual(try Date.create(2025, 4, 7), date); // First Mon + date = Date.fromNthWeekday(2025, 4, .First, .Tuesday); + try testing.expectEqual(try Date.create(2025, 4, 1), date); // First Tues + date = Date.fromNthWeekday(2025, 4, .First, .Wednesday); + try testing.expectEqual(try Date.create(2025, 4, 2), date); // First Wed + date = Date.fromNthWeekday(2025, 4, .First, .Thursday); + try testing.expectEqual(try Date.create(2025, 4, 3), date); // First Thu + date = Date.fromNthWeekday(2025, 4, .First, .Friday); + try testing.expectEqual(try Date.create(2025, 4, 4), date); // First Fri + date = Date.fromNthWeekday(2025, 4, .First, .Saturday); + try testing.expectEqual(try Date.create(2025, 4, 5), date); // First Sat + date = Date.fromNthWeekday(2025, 4, .First, .Sunday); + try testing.expectEqual(try Date.create(2025, 4, 6), date); // First Sun + + date = Date.fromNthWeekday(2025, 4, .Second, .Monday); + try testing.expectEqual(try Date.create(2025, 4, 14), date); + date = Date.fromNthWeekday(2025, 4, .Second, .Tuesday); + try testing.expectEqual(try Date.create(2025, 4, 8), date); + + date = Date.fromNthWeekday(2025, 4, .Last, .Tuesday); + try testing.expectEqual(try Date.create(2025, 4, 29), date); + + date = Date.fromNthWeekday(2024, 11, .Last, .Sunday); + try testing.expectEqual(try Date.create(2024, 11, 24), date); // Last sunday in november 2024 +} + test "date-day-of-year" { var date = try Date.create(1970, 1, 1); try testing.expect(date.dayOfYear() == 1); @@ -794,17 +856,33 @@ test "date-isocalendar" { } pub const Timezone = struct { + // Function to compute DST offset + pub const DaylightSavings = *const fn (date: Date, time: Time) i32; + offset: i16, // In minutes name: []const u8, + dst: ?DaylightSavings, - // Auto register timezones pub fn create(name: []const u8, offset: i16) Timezone { - const self = Timezone{ .offset = offset, .name = name }; - return self; + return Timezone{ .offset = offset, .name = name, .dst = null }; } - pub fn offsetSeconds(self: Timezone) i32 { - return @as(i32, self.offset) * time.s_per_min; + pub fn createDst(name: []const u8, offset: i16, dst: DaylightSavings) Timezone { + return Timezone{ .offset = offset, .name = name, .dst = dst }; + } + + // Check if two timezones are the same or differ only by their name + pub fn isSame(self: *const Timezone, other: *const Timezone) bool { + return self.offset == other.offset and self.dst == other.dst; + } + + // Calculate the timezone offset in minutes for the given date and time factoring in daylight savings if relevant + pub fn dstOffset(self: Timezone, d: Date, t: Time) i32 { + const mins = @as(i32, self.offset); + if (self.dst) |f| { + return mins + f(d, t); + } + return mins; } }; @@ -1196,7 +1274,7 @@ pub const Datetime = struct { pub fn toTimestamp(self: Datetime) i128 { const ds = self.date.toTimestamp(); const ts = self.time.toTimestamp(); - const zs = self.zone.offsetSeconds() * time.ms_per_s; + const zs = self.zone.dstOffset(self.date, self.time) * time.s_per_min * time.ms_per_s; return ds + ts - zs; } @@ -1208,20 +1286,18 @@ pub const Datetime = struct { } pub fn cmpSameTimezone(self: Datetime, other: Datetime) Order { - assert(self.zone.offset == other.zone.offset); + assert(self.zone.isSame(other.zone)); const r = self.date.cmp(other.date); if (r != .eq) return r; return self.time.cmp(other.time); } pub fn cmp(self: Datetime, other: Datetime) Order { - if (self.zone.offset == other.zone.offset) { + if (self.zone.isSame(other.zone)) { return self.cmpSameTimezone(other); } - // Shift both to utc - const a = self.shiftTimezone(&timezones.UTC); - const b = other.shiftTimezone(&timezones.UTC); - return a.cmpSameTimezone(b); + const shifted = other.shiftTimezone(self.zone); + return self.cmpSameTimezone(shifted); } pub fn gt(self: Datetime, other: Datetime) bool { @@ -1249,7 +1325,7 @@ pub const Datetime = struct { // Return a Datetime.Delta relative to this date pub fn sub(self: Datetime, other: Datetime) Delta { var days = @as(i32, @intCast(self.date.toOrdinal())) - @as(i32, @intCast(other.date.toOrdinal())); - const offset = (self.zone.offset - other.zone.offset) * time.s_per_min; + const offset = (self.zone.dstOffset(self.date, self.time) - other.zone.dstOffset(other.date, other.time)) * time.s_per_min; var seconds = (self.time.totalSeconds() - other.time.totalSeconds()) - offset; var ns = @as(i32, @intCast(self.time.nanosecond)) - @as(i32, @intCast(other.time.nanosecond)); while (seconds > 0 and ns < 0) { @@ -1286,10 +1362,14 @@ pub const Datetime = struct { // Convert to the given timeszone pub fn shiftTimezone(self: Datetime, zone: *const Timezone) Datetime { var dt = - if (self.zone.offset == zone.offset) - (self.copy() catch unreachable) - else - self.shiftMinutes(zone.offset - self.zone.offset); + if (self.zone.isSame(zone)) + (self.copy() catch unreachable) + else if (self.zone.dst == zone.dst) + // Any DST effects whill be the same so just compare the offset directly + self.shiftMinutes(zone.offset - self.zone.offset) + else + // Shift adjusting for any DST effects + self.shiftMinutes(zone.dstOffset(self.date, self.time) - self.zone.dstOffset(self.date, self.time)); dt.zone = zone; return dt; } @@ -1393,10 +1473,11 @@ pub const Datetime = struct { /// e.g. "2023-06-10T14:06:40.015006+08:00" pub fn formatISO8601(self: Datetime, allocator: Allocator, with_micro: bool) ![]const u8 { var sign: u8 = '+'; - if (self.zone.offset < 0) { + const dst_offset = self.zone.dstOffset(self.date, self.time); + if (dst_offset < 0) { sign = '-'; } - const offset = @abs(self.zone.offset); + const offset = @abs(dst_offset); var micro_part_len: u3 = 0; var micro_part: [7]u8 = undefined; @@ -1425,10 +1506,11 @@ pub const Datetime = struct { pub fn formatISO8601Buf(self: Datetime, buf: []u8, with_micro: bool) ![]const u8 { var sign: u8 = '+'; - if (self.zone.offset < 0) { + const dst_offset = self.zone.dstOffset(self.date, self.time); + if (dst_offset < 0) { sign = '-'; } - const offset = @abs(self.zone.offset); + const offset = @abs(dst_offset); var micro_part_len: usize = 0; var micro_part: [7]u8 = undefined; @@ -1673,6 +1755,14 @@ test "readme-example" { } +test "datetime-now-in-ny" { + const allocator = std.testing.allocator; + const now = Datetime.now().shiftTimezone(&timezones.America.New_York); + const now_str = try now.formatHttp(allocator); + defer allocator.free(now_str); + std.log.warn("New york time is: {s}\n", .{now_str}); +} + test "datetime-format-ISO8601" { const allocator = std.testing.allocator; diff --git a/src/timezones.zig b/src/timezones.zig index 27081d7..62ef089 100644 --- a/src/timezones.zig +++ b/src/timezones.zig @@ -6,8 +6,40 @@ const std = @import("std"); -const Timezone = @import("datetime.zig").Timezone; +const datetime = @import("datetime.zig"); +const Timezone = datetime.Timezone; +const Date = datetime.Date; +const Time = datetime.Time; const create = Timezone.create; +const createDst = Timezone.createDst; + +// DST conversion functions +// If date and time is between 2nd Sunday in March at 2am and 2nd Sunday in November at 2am. +// Return a 1 hr offset +pub fn DstMarchAt2ToNovAt2(d: Date, t: Time) i32 { + const offset = 60; + return switch (d.monthOfYear()) { + .January, .February => 0, + .March => blk: { + const cutoff = Date.fromNthWeekday(d.year, d.month, .Second, .Sunday) catch unreachable; + break :blk switch (d.cmp(cutoff)) { + .lt => 0, + .eq => if (t.hour >= 2) offset else 0, + .gt => offset, + }; + }, + else => offset, + .November => blk: { + const cutoff = Date.fromNthWeekday(d.year, d.month, .Second, .Sunday) catch unreachable; + break :blk switch (d.cmp(cutoff)) { + .lt => offset, + .eq => if (t.hour < 2) offset else 0, + .gt => 0, + }; + }, + .December => 0, + }; +} // Timezones pub const Africa = struct { @@ -108,7 +140,7 @@ pub const America = struct { pub const Catamarca = create("America/Catamarca", -180); pub const Cayenne = create("America/Cayenne", -180); pub const Cayman = create("America/Cayman", -300); - pub const Chicago = create("America/Chicago", -360); + pub const Chicago = createDst("America/Chicago", -360, DstMarchAt2ToNovAt2); pub const Chihuahua = create("America/Chihuahua", -420); pub const Coral_Harbour = create("America/Coral_Harbour", -300); pub const Cordoba = create("America/Cordoba", -180); @@ -119,15 +151,15 @@ pub const America = struct { pub const Danmarkshavn = create("America/Danmarkshavn", 0); pub const Dawson = create("America/Dawson", -480); pub const Dawson_Creek = create("America/Dawson_Creek", -420); - pub const Denver = create("America/Denver", -420); - pub const Detroit = create("America/Detroit", -300); + pub const Denver = createDst("America/Denver", -420, DstMarchAt2ToNovAt2); + pub const Detroit = createDst("America/Detroit", -300, DstMarchAt2ToNovAt2); pub const Dominica = create("America/Dominica", -240); pub const Edmonton = create("America/Edmonton", -420); pub const Eirunepe = create("America/Eirunepe", -300); pub const El_Salvador = create("America/El_Salvador", -360); pub const Ensenada = create("America/Ensenada", -480); - pub const Fort_Nelson = create("America/Fort_Nelson", -420); - pub const Fort_Wayne = create("America/Fort_Wayne", -300); + pub const Fort_Nelson = createDst("America/Fort_Nelson", -420, DstMarchAt2ToNovAt2); + pub const Fort_Wayne = createDst("America/Fort_Wayne", -300, DstMarchAt2ToNovAt2); pub const Fortaleza = create("America/Fortaleza", -180); pub const Glace_Bay = create("America/Glace_Bay", -240); pub const Godthab = create("America/Godthab", -180); @@ -152,7 +184,7 @@ pub const America = struct { pub const Vincennes = create("America/Indiana/Vincennes", -300); pub const Winamac = create("America/Indiana/Winamac", -300); }; - pub const Indianapolis = create("America/Indianapolis", -300); + pub const Indianapolis = createDst("America/Indianapolis", -300, DstMarchAt2ToNovAt2); pub const Inuvik = create("America/Inuvik", -420); pub const Iqaluit = create("America/Iqaluit", -300); pub const Jamaica = create("America/Jamaica", -300); @@ -160,15 +192,15 @@ pub const America = struct { pub const Juneau = create("America/Juneau", -540); pub const Kentucky = struct { // FIXME: Name conflict - pub const Louisville_ = create("America/Kentucky/Louisville", -300); - pub const Monticello = create("America/Kentucky/Monticello", -300); + pub const Louisville_ = createDst("America/Kentucky/Louisville", -300, DstMarchAt2ToNovAt2); + pub const Monticello = createDst("America/Kentucky/Monticello", -300, DstMarchAt2ToNovAt2); }; pub const Knox_IN = create("America/Knox_IN", -360); pub const Kralendijk = create("America/Kralendijk", -240); pub const La_Paz = create("America/La_Paz", -240); pub const Lima = create("America/Lima", -300); - pub const Los_Angeles = create("America/Los_Angeles", -480); - pub const Louisville = create("America/Louisville", -300); + pub const Los_Angeles = createDst("America/Los_Angeles", -480, DstMarchAt2ToNovAt2); + pub const Louisville = createDst("America/Louisville", -300, DstMarchAt2ToNovAt2); pub const Lower_Princes = create("America/Lower_Princes", -240); pub const Maceio = create("America/Maceio", -180); pub const Managua = create("America/Managua", -360); @@ -189,14 +221,14 @@ pub const America = struct { pub const Montreal = create("America/Montreal", -300); pub const Montserrat = create("America/Montserrat", -240); pub const Nassau = create("America/Nassau", -300); - pub const New_York = create("America/New_York", -300); + pub const New_York = createDst("America/New_York", -300, DstMarchAt2ToNovAt2); pub const Nipigon = create("America/Nipigon", -300); pub const Nome = create("America/Nome", -540); pub const Noronha = create("America/Noronha", -120); pub const North_Dakota = struct { - pub const Beulah = create("America/North_Dakota/Beulah", -360); - pub const Center = create("America/North_Dakota/Center", -360); - pub const New_Salem = create("America/North_Dakota/New_Salem", -360); + pub const Beulah = createDst("America/North_Dakota/Beulah", -360, DstMarchAt2ToNovAt2); + pub const Center = createDst("America/North_Dakota/Center", -360, DstMarchAt2ToNovAt2); + pub const New_Salem = createDst("America/North_Dakota/New_Salem", -360, DstMarchAt2ToNovAt2); }; pub const Ojinaga = create("America/Ojinaga", -420); pub const Panama = create("America/Panama", -300); @@ -367,7 +399,7 @@ pub const Asia = struct { pub const Atlantic = struct { pub const Azores = create("Atlantic/Azores", -60); - pub const Bermuda = create("Atlantic/Bermuda", -240); + pub const Bermuda = createDst("Atlantic/Bermuda", -240, DstMarchAt2ToNovAt2); pub const Canary = create("Atlantic/Canary", 0); pub const Cape_Verde = create("Atlantic/Cape_Verde", -60); pub const Faeroe = create("Atlantic/Faeroe", 0); @@ -573,9 +605,9 @@ pub const Libya = create("Libya", 120); pub const MET = create("MET", 60); pub const Mexico = struct { - pub const BajaNorte = create("Mexico/BajaNorte", -480); - pub const BajaSur = create("Mexico/BajaSur", -420); - pub const General = create("Mexico/General", -360); + pub const BajaNorte = createDst("Mexico/BajaNorte", -480, DstMarchAt2ToNovAt2); + pub const BajaSur = createDst("Mexico/BajaSur", -420, DstMarchAt2ToNovAt2); + pub const General = createDst("Mexico/General", -360, DstMarchAt2ToNovAt2); }; pub const MST = create("MST", -420); pub const MST7MDT = create("MST7MDT", -420); @@ -640,18 +672,17 @@ pub const UCT = create("UCT", 0); pub const Universal = create("Universal", 0); pub const US = struct { - pub const Alaska = create("US/Alaska", -540); - pub const Aleutian = create("US/Aleutian", -600); + pub const Alaska = createDst("US/Alaska", -540, DstMarchAt2ToNovAt2); + pub const Aleutian = createDst("US/Aleutian", -600, DstMarchAt2ToNovAt2); pub const Arizona = create("US/Arizona", -420); - pub const Central = create("US/Central", -360); - pub const Eastern = create("US/Eastern", -300); - pub const East_Indiana = create("US/East-Indiana", -300); + pub const Central = createDst("US/Central", -360, DstMarchAt2ToNovAt2); + pub const Eastern = createDst("US/Eastern", -300, DstMarchAt2ToNovAt2); + pub const East_Indiana = createDst("US/East-Indiana", -300, DstMarchAt2ToNovAt2); pub const Hawaii = create("US/Hawaii", -600); - pub const Indiana_Starke = create("US/Indiana-Starke", -360); - pub const Michigan = create("US/Michigan", -300); - pub const Mountain = create("US/Mountain", -420); - pub const Pacific = create("US/Pacific", -480); - pub const Pacific_New = create("US/Pacific-New", -480); + pub const Indiana_Starke = createDst("US/Indiana-Starke", -360, DstMarchAt2ToNovAt2); + pub const Michigan = createDst("US/Michigan", -300, DstMarchAt2ToNovAt2); + pub const Mountain = createDst("US/Mountain", -420, DstMarchAt2ToNovAt2); + pub const Pacific = createDst("US/Pacific", -480, DstMarchAt2ToNovAt2); pub const Samoa = create("US/Samoa", -660); }; pub const UTC = create("UTC", 0); @@ -667,8 +698,7 @@ fn findWithinTimezones(comptime Type: type, timezone: []const u8) ?Timezone { } if (@TypeOf(it) == type) { - const Info = @typeInfo(it); - if (@hasDecl(@TypeOf(Info), "Struct")) { + if (@typeInfo(it) == .@"struct") { const found = findWithinTimezones(it, timezone); if (found != null) return found;