Skip to content

Commit

Permalink
std.time: include leap seconds in timezone projection
Browse files Browse the repository at this point in the history
  • Loading branch information
Vexu committed Dec 14, 2023
1 parent ea54ee9 commit 043150c
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 38 deletions.
38 changes: 26 additions & 12 deletions lib/std/time.zig
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ pub const DateTime = struct {

/// UNIX timestamp in nanoseconds
nano_timestamp: i128,
tt: TimeZone.TimeType,

/// The time zone to which `nano_timestamp` was projected to.
tz_projection: TimeZone.Projection,

pub const Weekday = enum(u3) {
monday = 1,
Expand Down Expand Up @@ -181,6 +183,9 @@ pub const DateTime = struct {
};

/// Get current date and time in the system's local time.
/// On non-Windows systems this loads the system's local time
/// time zone database from the filesystem and may allocate
/// so when called repeated `nowTz` should be preferred.
pub fn now(allocator: std.mem.Allocator) LocalTimeError!DateTime {
const nano_timestamp = nanoTimestamp();

Expand Down Expand Up @@ -215,8 +220,13 @@ pub const DateTime = struct {
return fromTimestamp(nano_timestamp, tz.project(@intCast(@divFloor(nano_timestamp, ns_per_s))));
}

pub fn nowTz(tz: *const TimeZone) DateTime {
const nano_timestamp = nanoTimestamp();
return fromTimestamp(nano_timestamp, tz.project(@intCast(@divFloor(nano_timestamp, ns_per_s))));
}

/// Convert UNIX timestamp in nanoseconds to a DateTime.
pub fn fromTimestamp(nano_timestamp: i128, tt: TimeZone.TimeType) DateTime {
pub fn fromTimestamp(nano_timestamp: i128, tz_projection: TimeZone.Projection) DateTime {
// Ported from musl, which is licensed under the MIT license:
// https://git.musl-libc.org/cgit/musl/tree/COPYRIGHT

Expand All @@ -228,7 +238,7 @@ pub const DateTime = struct {
const days_per_4y = 365 * 4 + 1;

const seconds_unadjusted: i64 = @intCast(@divFloor(nano_timestamp, ns_per_s));
const seconds = seconds_unadjusted + tt.offset - leapoch;
const seconds = seconds_unadjusted + tz_projection.offset - leapoch;
var days = @divTrunc(seconds, 86400);
var rem_seconds = @as(i32, @truncate(@rem(seconds, 86400)));
if (rem_seconds < 0) {
Expand Down Expand Up @@ -295,7 +305,7 @@ pub const DateTime = struct {
.second = @intCast(@rem(rem_seconds, 60)),
.nanosecond = @intCast(@rem(nano_timestamp, ns_per_s)),
.nano_timestamp = nano_timestamp,
.tt = tt,
.tz_projection = tz_projection,
};
}

Expand Down Expand Up @@ -372,7 +382,7 @@ pub const DateTime = struct {
'H' => try std.fmt.formatInt(date.hour, 10, .lower, .{ .width = 2, .fill = '0' }, writer),
'M' => try std.fmt.formatInt(date.minute, 10, .lower, .{ .width = 2, .fill = '0' }, writer),
'S' => try std.fmt.formatInt(date.second, 10, .lower, .{ .width = 2, .fill = '0' }, writer),
's' => try std.fmt.formatInt(date.millisecond, 10, .lower, .{ .width = 3, .fill = '0' }, writer),
's' => try std.fmt.formatInt(@divFloor(date.nanosecond, ns_per_ms), 10, .lower, .{ .width = 3, .fill = '0' }, writer),
'j' => try std.fmt.formatInt(date.year_day, 10, .lower, .{ .width = 3, .fill = '0' }, writer),
'p' => if (date.hour < 12) {
try writer.writeAll("AM");
Expand All @@ -384,15 +394,15 @@ pub const DateTime = struct {
begin = i + 1;
},
'z' => {
const sign = "+-"[@intFromBool(date.tt.offset < 0)];
const sign = "+-"[@intFromBool(date.tz_projection.offset < 0)];
try writer.writeByte(sign);
const abs = @abs(date.tt.offset);
const abs = @abs(date.tz_projection.offset);
const hours = @divFloor(abs, 3600);
const minutes = @rem(@divFloor(abs, 60), 60);
try std.fmt.formatInt(hours, 10, .lower, .{ .width = 2, .fill = '0' }, writer);
try std.fmt.formatInt(minutes, 10, .lower, .{ .width = 2, .fill = '0' }, writer);
},
'Z' => try writer.writeAll(date.tt.name()),
'Z' => try writer.writeAll(date.tz_projection.name()),
else => @compileError("Unknown format character: " ++ [_]u8{fmt[i]}),
}
}
Expand All @@ -406,8 +416,13 @@ pub const DateTime = struct {
}
};

const null_projection: TimeZone.Projection = .{
.offset = 0,
.name_data = "TEST\x00\x00".*,
};

test "DateTime basic usage" {
const dt = DateTime.fromTimestamp(1560870105 * ns_per_s, TimeZone.TimeType.UTC);
const dt = DateTime.fromTimestamp(1560870105 * ns_per_s, null_projection);

try testing.expect(dt.second == 45);
try testing.expect(dt.minute == 1);
Expand All @@ -420,16 +435,15 @@ test "DateTime basic usage" {
}

test "DateTime.format all" {
const dt = DateTime.fromTimestamp(1560816105 * ns_per_s, TimeZone.TimeType.UTC);
const dt = DateTime.fromTimestamp(1560816105 * ns_per_s, null_projection);
var buf: [100]u8 = undefined;
const result = try std.fmt.bufPrint(&buf, "{%a %A %b %B %m %d %y %Y %I %p %H%c%M%c%S.%s %j %%}", .{dt});
try testing.expectEqualStrings("Tue Tuesday Jun June 06 18 19 2019 12 AM 00:01:45.000 169 %", result);
}

test "DateTime.format no format" {
const EEST: TimeZone.TimeType = .{
const EEST: TimeZone.Projection = .{
.offset = 10800,
.flags = 0,
.name_data = "EEST\x00\x00".*,
};
const dt = DateTime.fromTimestamp(1560870105 * ns_per_s, EEST);
Expand Down
68 changes: 42 additions & 26 deletions lib/std/time/TimeZone.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ pub const TimeType = struct {
pub fn utIndicator(self: TimeType) bool {
return (self.flags & 0x04) > 0;
}

pub const UTC = TimeType{
.offset = 0,
.flags = 0,
.name_data = "UTC\x00\x00\x00".*,
};
};

pub const Leapsecond = struct {
Expand Down Expand Up @@ -224,28 +218,50 @@ pub fn deinit(self: *TimeZone, allocator: std.mem.Allocator) void {
allocator.free(self.timetypes);
}

pub const Projection = struct {
/// Offset from UNIX timestamp including leap seconds.
offset: i32,
name_data: [6:0]u8,

pub fn name(self: *const TimeType) [:0]const u8 {
return std.mem.sliceTo(&self.name_data, 0);
}
};

/// Project UTC timestamp to this time zone.
pub fn project(self: TimeZone, seconds: i64) TimeType {
var left: usize = 0;
var right: usize = self.transitions.len;

// Adapted from std.sort.binarySearch.
var mid: usize = 0;
while (left < right) {
// Avoid overflowing in the midpoint calculation
mid = left + (right - left) / 2;
// Compare the key with the midpoint element
switch (std.math.order(seconds, self.transitions[mid].ts)) {
.eq => return self.transitions[mid].timetype.*,
.gt => left = mid + 1,
.lt => right = mid,
pub fn project(self: TimeZone, seconds: i64) Projection {
const index = index: {
var left: usize = 0;
var right: usize = self.transitions.len;

// Adapted from std.sort.binarySearch.
var mid: usize = 0;
while (left < right) {
// Avoid overflowing in the midpoint calculation
mid = left + (right - left) / 2;
// Compare the key with the midpoint element
switch (std.math.order(seconds, self.transitions[mid].ts)) {
.eq => break :index mid,
.gt => left = mid + 1,
.lt => right = mid,
}
}
}
if (self.transitions[mid].ts > seconds) {
return self.transitions[mid - 1].timetype.*;
} else {
return self.transitions[mid].timetype.*;
}
break :index mid - @intFromBool(self.transitions[mid].ts > seconds);
};
const leap_adjustment = leap: {
var i = self.leapseconds.len;
if (i == 0) break :leap 0;
while (i > 0) {
i -= 1;
if (self.leapseconds[i].occurrence < seconds) break;
}
break :leap self.leapseconds[i].correction;
};
const tt = self.transitions[index].timetype;
return .{
.offset = tt.offset + leap_adjustment,
.name_data = tt.name_data,
};
}

test project {
Expand Down

0 comments on commit 043150c

Please sign in to comment.