Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions lib/std/http/Client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1063,7 +1063,7 @@ pub const Request = struct {
try w.writeAll("\r\n");
}

pub const ReceiveHeadError = http.Reader.HeadError || ConnectError || error{
pub const ReceiveHeadError = http.Reader.HeadError || RequestError || error{
/// Server sent headers that did not conform to the HTTP protocol.
///
/// To find out more detailed diagnostics, `http.Reader.head_buffer` can be
Expand Down Expand Up @@ -1394,6 +1394,8 @@ pub const ConnectTcpError = Allocator.Error || error{
HostLacksNetworkAddresses,
UnexpectedConnectFailure,
TlsInitializationFailed,
AddressInUse,
SystemResources,
};

/// Reuses a `Connection` if one matching `host` and `port` is already open.
Expand Down Expand Up @@ -1440,6 +1442,8 @@ pub fn connectTcpOptions(client: *Client, options: ConnectTcpOptions) ConnectTcp
error.NameServerFailure => return error.NameServerFailure,
error.UnknownHostName => return error.UnknownHostName,
error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses,
error.AddressInUse => return error.AddressInUse,
error.SystemResources => return error.SystemResources,
else => return error.UnexpectedConnectFailure,
};
errdefer stream.close();
Expand Down Expand Up @@ -1562,8 +1566,6 @@ pub fn connectProxied(
};
}

pub const ConnectError = ConnectTcpError || RequestError;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConnectError is same as RequestError. Is deleting ConnectError the right move? Would it be better to leave it in with a doc comment about how it's being deprecated and what to use instead?


/// Connect to `host:port` using the specified protocol. This will reuse a
/// connection if one is already open.
///
Expand All @@ -1576,7 +1578,7 @@ pub fn connect(
host: []const u8,
port: u16,
protocol: Protocol,
) ConnectError!*Connection {
) ConnectTcpError!*Connection {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConnectError is the same as RequestError. Both of which are a larger collection of errors than ConnectTcpError is. But this function only returns errors found in ConnectTcpError, so this change will make the possible returned error types more clear.

const proxy = switch (protocol) {
.plain => client.http_proxy,
.tls => client.https_proxy,
Expand Down
158 changes: 158 additions & 0 deletions lib/std/time/epoch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,82 @@ pub const Month = enum(u4) {
}
};

/// Day of month (1 to 31)
pub const Day = u5;

/// Hour of day (0 to 23)
pub const Hour = u5;

/// Minute of hour (0 to 59)
pub const Minute = u6;

/// Second of minute (0 to 59)
pub const Second = u6;

pub const Datetime = struct {
year: Year,
month: Month,
day: Day,
hour: Hour,
minute: Minute,
second: Second,

pub fn asEpochSeconds(d: *const Datetime) !EpochSeconds {
// Validate input ranges
if (d.year < epoch_year) return error.InvalidYear;
if (d.day == 0 or d.day > getDaysInMonth(d.year, d.month)) return error.InvalidDay;
if (d.hour >= 24) return error.InvalidHour;
if (d.minute >= 60) return error.InvalidMinute;
if (d.second >= 60) return error.InvalidSecond;

// Calculate days from epoch to start of the given year
const total_years = d.year - epoch_year;
const total_leap_years = countLeapYearsBetween(epoch_year, d.year);
var total_days = @as(u47, @intCast(total_years)) * 365 + @as(u47, @intCast(total_leap_years));

// Add days for complete months in the given year
var m: Month = .jan;
while (@intFromEnum(m) < @intFromEnum(d.month)) {
total_days += getDaysInMonth(d.year, m);
m = @enumFromInt(@intFromEnum(m) + 1);
}

// Add remaining days (subtract 1 because day is 1-indexed)
total_days += d.day - 1;

// Convert to seconds and add time components
var total_secs: u64 = @as(u64, @intCast(total_days)) * @as(u64, @intCast(secs_per_day));
total_secs += @as(u64, @intCast(d.hour)) * 3600;
total_secs += @as(u64, @intCast(d.minute)) * 60;
total_secs += d.second;

return EpochSeconds{ .secs = total_secs };
}
};

/// Counts the number of leap years in the range [start_year, end_year).
/// The end_year is exclusive.
pub fn countLeapYearsBetween(start_year: Year, end_year: Year) u15 {
// We retrun u15 because `Year` is u16 and leap year is every 4 years.
// (2 ** 16) / 4 = 2 ** 14, so u14 is clearly the best fit. But every 100
// years is also leap year, which will result in very few extra leap years,
// so we are adding 1 bit to make room for that, resulting in u15.

if (end_year <= start_year) return 0;

// Count leap years from year 0 to end_year-1, then subtract those before start_year
const leaps_before_end = countLeapYearsFromZero(end_year - 1);
const leaps_before_start = if (start_year > 0) countLeapYearsFromZero(start_year - 1) else 0;

return leaps_before_end - leaps_before_start;
}

/// Counts leap years from year 0 to the given year (inclusive).
fn countLeapYearsFromZero(y: Year) u15 {
// Divisible by 4, minus centuries (divisible by 100), plus quad-centuries (divisible by 400)
return @as(u15, @intCast((y / 4) - (y / 100) + (y / 400)));
}

/// Get the number of days in the given month and year
pub fn getDaysInMonth(year: Year, month: Month) u5 {
return switch (month) {
Expand Down Expand Up @@ -201,6 +277,10 @@ fn testEpoch(secs: u64, expected_year_day: YearAndDay, expected_month_day: Month
try testing.expectEqual(expected_day_seconds.seconds_into_minute, day_seconds.getSecondsIntoMinute());
}

fn testDatetimeToEpochSeconds(epoc_seconds: u64, dt: Datetime) !void {
try testing.expectEqual(epoc_seconds, (try dt.asEpochSeconds()).secs);
}

test "epoch decoding" {
try testEpoch(0, .{ .year = 1970, .day = 0 }, .{
.month = .jan,
Expand All @@ -222,3 +302,81 @@ test "epoch decoding" {
.day_index = 0,
}, .{ .hours_into_day = 17, .minutes_into_hour = 11, .seconds_into_minute = 13 });
}

test "datetime to epochseconds" {
// epoc time exactly
try testDatetimeToEpochSeconds(0, .{
.year = 1970,
.month = .jan,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
});

// last second of a year
try testDatetimeToEpochSeconds(31535999, .{
.year = 1970,
.month = .dec,
.day = 31,
.hour = 23,
.minute = 59,
.second = 59,
});

// first second of next year
try testDatetimeToEpochSeconds(31536000, .{
.year = 1971,
.month = .jan,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
});

// leap year
try testDatetimeToEpochSeconds(68256000, .{
.year = 1972,
.month = .mar,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
});

// super far in the future
try testDatetimeToEpochSeconds(16725225600, .{
.year = 2500,
.month = .jan,
.day = 1,
.hour = 0,
.minute = 0,
.second = 0,
});

// invalid input
const dt = Datetime{
.year = 1970,
.month = .feb,
.day = 29, // invalid because it's not leap year
.hour = 0,
.minute = 0,
.second = 0,
};
try testing.expectError(error.InvalidDay, dt.asEpochSeconds());
}

test "countLeapYearsBetween" {
// Between 1970-1980: 1972, 1976 = 2 leap years
try std.testing.expectEqual(@as(u47, 2), countLeapYearsBetween(1970, 1980));

// Between 1970-2000: excludes 2000 which is a leap year (divisible by 400)
try std.testing.expectEqual(@as(u47, 7), countLeapYearsBetween(1970, 2000));

// Between 1970-2001: adds 2000
try std.testing.expectEqual(@as(u47, 8), countLeapYearsBetween(1970, 2001));

// Century test: 1900 is NOT a leap year, 2000 IS
try std.testing.expectEqual(@as(u47, 0), countLeapYearsBetween(1900, 1901));
try std.testing.expectEqual(@as(u47, 1), countLeapYearsBetween(2000, 2001));
}
Loading