Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

std: add generic date.Date, time.Time, and date_time.DateTime #19549

Closed
wants to merge 22 commits into from

Conversation

clickingbuttons
Copy link
Contributor

@clickingbuttons clickingbuttons commented Apr 5, 2024

Add Date, Time, and DateTime structs with functions to convert to/from epoch subseconds and to add durations to them.

These types are based off generic types which let the user make their own tradeoffs between memory and precision.

  • Add std.Date, a Gregorian calendar date based off the Unix epoch.
  • Add std.Time, a time with second resolution.
    • You may create a Time type with arbitrary subsecond precision using std.time.TimeAdvanced(precision).
  • Add std.DateTime which contains a std.Date and std.Time.
  • Use new DateTime types in status quo.
    • UEFI
    • Certificate parsing
    • C date and time macros in aro/Compilation.zig

Potential future work:

  • Localization:
    • Parse /etc/localtime on Linux/OSX. Use GetTimeZoneInformation on Windows.
    • Conversion to arbitrary timezone by using timezone database on the user's system. These are rather large (440.7kb zipped, 2096kb unzipped). I do not think these qualify as "dependency zero" to be embedded with Zig. Related ship root SSL certificates along with ziglang.org-vendored tarballs #14168.
  • RFC 3339 parsing + formatting.
  • ISO 8601 parsing + formatting.
  • Add leap second clock. Handle leap seconds using historical leap second table.

Closes #8396.
Prior art #18272.

@clickingbuttons clickingbuttons changed the title Add DateTime Add Generic DateTime Apr 5, 2024
@clickingbuttons
Copy link
Contributor Author

clickingbuttons commented Apr 5, 2024

I just now read #18272 (comment) and would like to give an argument in favor of this change:

Date APIs are necessary for converting epoch timestamps to calendar dates and back. In addition to retrieving epoch seconds from a clock, the standard C library provides a struct tm that many programs rely on. I believe for Zig to compete with C it needs its own struct tm.

Zig is also already using 3 different versions of DateTime internally.

@clickingbuttons clickingbuttons changed the title Add Generic DateTime add generic std.date.Time, std.time.Time, and std.date_time.DateTime Apr 5, 2024
@clickingbuttons clickingbuttons changed the title add generic std.date.Time, std.time.Time, and std.date_time.DateTime std: add generic date.Time, time.Time, and date_time.DateTime Apr 6, 2024
lib/std/date/gregorian.zig Outdated Show resolved Hide resolved
@notcancername
Copy link
Contributor

notcancername commented Apr 6, 2024

The user would not need to bring their own time zone database (unless they are doing embedded development, in which case, they should be aware of the implementation details and bring their own), the OS does this. See #8396 (comment).

lib/std/date_time.zig Outdated Show resolved Hide resolved
lib/std/date_time.zig Outdated Show resolved Hide resolved
@clickingbuttons
Copy link
Contributor Author

clickingbuttons commented Apr 8, 2024

This CI error is a bit outside my comfort zone, but I'll try to tackle it:

lib/std/time.zig:379:13: error: TODO implement airWithOverflow from u8 to u17

Edit: Found workarounds. Opened #19606 and #19607.

lib/std/time.zig Outdated Show resolved Hide resolved
lib/std/time.zig Outdated Show resolved Hide resolved
lib/std/time.zig Outdated Show resolved Hide resolved
lib/std/time.zig Outdated Show resolved Hide resolved
lib/std/time.zig Show resolved Hide resolved
lib/std/time.zig Outdated Show resolved Hide resolved
lib/std/date.zig Outdated Show resolved Hide resolved
lib/std/date/gregorian.zig Outdated Show resolved Hide resolved
lib/std/date/gregorian.zig Outdated Show resolved Hide resolved
@clickingbuttons clickingbuttons changed the title std: add generic date.Time, time.Time, and date_time.DateTime std: add generic date.Date, time.Time, and date_time.DateTime Apr 9, 2024
@FObersteiner
Copy link

FObersteiner commented Apr 10, 2024

Now that RFC3339 parsing/formatting is removed, does it still satisfy the needs described here?

By the way, I don't see any problems down the line if datetime comes with a UTC offset field. This is the most basic information you need for timezone handling, while it does not carry any of the more complex details. Timezone rules exist independent of any date&time, however, when applied to a date&time, they result in just and offset, specific to that date&time. So since the offset is specific, I think it would actually help a timezone wrapper around this datetime implementation to do its job.

@clickingbuttons
Copy link
Contributor Author

Yes, it still satisfies the needs. The offset was added to aid the UEFI implementation, but that implementation can simply add the offset. The RFC 3339 implementation was just something I thought was nice to have and easy to add.

@clickingbuttons clickingbuttons requested a review from Vexu April 10, 2024 23:37
/// Allows leap seconds.
second: Second = 0,
/// Milliseconds, microseconds, or nanoseconds.
subsecond: Subsecond = 0,
Copy link
Member

Choose a reason for hiding this comment

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

What's the justification for this additional complexity over always using nanoseconds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Saving bytes and allowing finer precision than nanoseconds.

It's the same argument for templating the Year in Date.

Copy link
Contributor Author

@clickingbuttons clickingbuttons Apr 11, 2024

Choose a reason for hiding this comment

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

Here are the sizes:

comptime {
    assert(@sizeOf(Time(0)) == 3);
    assert(@sizeOf(Time(3)) == 6);
    assert(@sizeOf(Time(6)) == 8);
    assert(@sizeOf(Time(9)) == 8);
}

If you think that calling Time(precision) is cumbersome I can rename Time -> TimeAdvanced and then write:

/// Time with second resolution
pub const Time = TimeAdvanced(0);
/// Time with millisecond resolution
pub const TimeMillis = TimeAdvanced(3);
/// Time with microsecond precision.
/// Note: This is the same size `TimeNano`. If you want the extra precision use that instead.
pub const TimeMicro = TimeAdvanced(6
/// Time with nanosecond resolution
pub const TimeNanos = TimeAdvanced(9);

This would be more consistent with the default Date and DateTime types.

Edit: I talked myself into this. Let me know what you think.

lib/std/date/gregorian.zig Outdated Show resolved Hide resolved
@Vexu
Copy link
Member

Vexu commented Apr 11, 2024

The CI might get fixed by rebasing on master.

@clickingbuttons clickingbuttons force-pushed the datetime branch 2 times, most recently from 6af17cc to cd88d7b Compare April 11, 2024 16:33
@andrewrk
Copy link
Member

The proposal this implements is not accepted.

I suggest to maintain a third party date/time Zig package, and then at some point you can suggest to upstream it if you wish.

Also Avoid Redundant Names in Fully-Qualified Namespaces

@clickingbuttons
Copy link
Contributor Author

clickingbuttons commented Apr 17, 2024

The proposal this implements is not accepted.

A public (albeit inconvenient) implementation of Date already exists, so I considered the proposal at least partially accepted. Was #9040 was never meant to be public?

zig/lib/std/time/epoch.zig

Lines 42 to 186 in 12191c8

/// The type that holds the current year, i.e. 2016
pub const Year = u16;
pub const epoch_year = 1970;
pub const secs_per_day: u17 = 24 * 60 * 60;
pub fn isLeapYear(year: Year) bool {
if (@mod(year, 4) != 0)
return false;
if (@mod(year, 100) != 0)
return true;
return (0 == @mod(year, 400));
}
test isLeapYear {
try testing.expectEqual(false, isLeapYear(2095));
try testing.expectEqual(true, isLeapYear(2096));
try testing.expectEqual(false, isLeapYear(2100));
try testing.expectEqual(true, isLeapYear(2400));
}
pub fn getDaysInYear(year: Year) u9 {
return if (isLeapYear(year)) 366 else 365;
}
pub const YearLeapKind = enum(u1) { not_leap, leap };
pub const Month = enum(u4) {
jan = 1,
feb,
mar,
apr,
may,
jun,
jul,
aug,
sep,
oct,
nov,
dec,
/// return the numeric calendar value for the given month
/// i.e. jan=1, feb=2, etc
pub fn numeric(self: Month) u4 {
return @intFromEnum(self);
}
};
/// Get the number of days in the given month
pub fn getDaysInMonth(leap_year: YearLeapKind, month: Month) u5 {
return switch (month) {
.jan => 31,
.feb => @as(u5, switch (leap_year) {
.leap => 29,
.not_leap => 28,
}),
.mar => 31,
.apr => 30,
.may => 31,
.jun => 30,
.jul => 31,
.aug => 31,
.sep => 30,
.oct => 31,
.nov => 30,
.dec => 31,
};
}
pub const YearAndDay = struct {
year: Year,
/// The number of days into the year (0 to 365)
day: u9,
pub fn calculateMonthDay(self: YearAndDay) MonthAndDay {
var month: Month = .jan;
var days_left = self.day;
const leap_kind: YearLeapKind = if (isLeapYear(self.year)) .leap else .not_leap;
while (true) {
const days_in_month = getDaysInMonth(leap_kind, month);
if (days_left < days_in_month)
break;
days_left -= days_in_month;
month = @as(Month, @enumFromInt(@intFromEnum(month) + 1));
}
return .{ .month = month, .day_index = @as(u5, @intCast(days_left)) };
}
};
pub const MonthAndDay = struct {
month: Month,
day_index: u5, // days into the month (0 to 30)
};
// days since epoch Oct 1, 1970
pub const EpochDay = struct {
day: u47, // u47 = u64 - u17 (because day = sec(u64) / secs_per_day(u17)
pub fn calculateYearDay(self: EpochDay) YearAndDay {
var year_day = self.day;
var year: Year = epoch_year;
while (true) {
const year_size = getDaysInYear(year);
if (year_day < year_size)
break;
year_day -= year_size;
year += 1;
}
return .{ .year = year, .day = @as(u9, @intCast(year_day)) };
}
};
/// seconds since start of day
pub const DaySeconds = struct {
secs: u17, // max is 24*60*60 = 86400
/// the number of hours past the start of the day (0 to 23)
pub fn getHoursIntoDay(self: DaySeconds) u5 {
return @as(u5, @intCast(@divTrunc(self.secs, 3600)));
}
/// the number of minutes past the hour (0 to 59)
pub fn getMinutesIntoHour(self: DaySeconds) u6 {
return @as(u6, @intCast(@divTrunc(@mod(self.secs, 3600), 60)));
}
/// the number of seconds past the start of the minute (0 to 59)
pub fn getSecondsIntoMinute(self: DaySeconds) u6 {
return math.comptimeMod(self.secs, 60);
}
};
/// seconds since epoch Oct 1, 1970 at 12:00 AM
pub const EpochSeconds = struct {
secs: u64,
/// Returns the number of days since the epoch as an EpochDay.
/// Use EpochDay to get information about the day of this time.
pub fn getEpochDay(self: EpochSeconds) EpochDay {
return EpochDay{ .day = @as(u47, @intCast(@divTrunc(self.secs, secs_per_day))) };
}
/// Returns the number of seconds into the day as DaySeconds.
/// Use DaySeconds to get information about the time.
pub fn getDaySeconds(self: EpochSeconds) DaySeconds {
return DaySeconds{ .secs = math.comptimeMod(self.secs, secs_per_day) };
}
};

I suggest to maintain a third party date/time Zig package, and then at some point you can suggest to upstream it if you wish.

zig zen reads:

  • Only one obvious way to do things.
  • Incremental improvements.

I believe this PR creates one obvious way of using Date and Time in the stdlib. Currently for contributors there are four: std.time.epoch.*, std.crypto.Certificate.Date, std.uefi.Time, or roll your own.

I consider this an incremental improvement to the existing std.time.epoch.* types. It fixes a bug in the UEFI implementation and improves performance of all 3.

Moving forward

If you believe this change goes too far beyond status quo I'm happy to leave #8396 open and remove the following:

  • fn adds.
  • Hardcode Year = i16.
  • Remove Time.subseconds or hardcode to nanosecond for UEFI.

Those changes will likely make this PR a net negative in terms of LOC and offer no additional functionality than what's already in std.time.epoch.*.

@clickingbuttons
Copy link
Contributor Author

It's with great sorrow I publish yet another datetime library.

I really hope the stdlib will have general purpose date and time parsing upon 1.0.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposal: DateTime in std.time
6 participants