From 742e0ae59f8b005533b84fee31d7cf130f872236 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Sun, 20 Jul 2025 15:08:12 +0200 Subject: [PATCH 1/2] Update to Zig 0.15.0-dev.1147+69cf40da6 --- README.md | 44 ++++++++++++++++++++++++++++---- build.zig | 20 ++++++++++----- demos.zig | 46 ++++++++++++++++++++++++++------- src/date/gregorian.zig | 35 +++++++++++++------------ src/datetime.zig | 58 ++++++++++++++++++++++++------------------ src/root.zig | 16 ------------ src/time.zig | 41 +++++++++++++++-------------- 7 files changed, 159 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index d4e9508..a0c627c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Generic Date, Time, and DateTime library. ## Installation ```sh +zig fetch --save "git+https://github.com/clickingbuttons/datetime.git" +# or zig fetch --save "https://github.com/clickingbuttons/datetime/archive/refs/tags/0.14.0.tar.gz" ``` @@ -29,23 +31,53 @@ const datetime = @import("datetime"); test "now" { const date = datetime.Date.now(); - std.debug.print("today's date is {rfc3339}\n", .{ date }); + std.debug.print("today's date is {f}\n", .{date}); const time = datetime.Time.now(); - std.debug.print("today's time is {rfc3339}\n", .{ time }); + std.debug.print("today's time is {f}\n", .{time}); const nanotime = datetime.time.Nano.now(); - std.debug.print("today's nanotime is {rfc3339}\n", .{ nanotime }); + std.debug.print("today's nanotime is {f}\n", .{nanotime}); const dt = datetime.DateTime.now(); - std.debug.print("today's date and time is {rfc3339}\n", .{ dt }); + std.debug.print("today's date and time is {f}\n", .{dt}); const NanoDateTime = datetime.datetime.Advanced(datetime.Date, datetime.time.Nano, false); const ndt = NanoDateTime.now(); - std.debug.print("today's date and nanotime is {rfc3339}\n", .{ ndt }); + std.debug.print("today's date and nanotime is {f}\n", .{ndt}); } ``` +### Formatting Options + +**RFC3339 Format (default):** The `{f}` format specifier outputs RFC3339 format by default: + +```zig +const date = datetime.Date.init(2025, .jul, 20); +const time = datetime.time.Milli.init(15, 30, 45, 123); +const dt = datetime.DateTime.init(2025, .jul, 20, 15, 30, 45, 0, 0); + +std.debug.print("Date: {f}\n", .{date}); // Output: 2025-07-20 +std.debug.print("Time: {f}\n", .{time}); // Output: 15:30:45.123 +std.debug.print("DateTime: {f}\n", .{dt}); // Output: 2025-07-20T15:30:45Z +``` + +**Struct Format (for debugging):** Use the `formatStruct` method for debug-style output: + +```zig +var buf: [256]u8 = undefined; +var writer = std.io.Writer.fixed(&buf); + +try date.formatStruct(&writer); +std.debug.print("Date: {s}\n", .{writer.buffered()}); +// Output: Date{ .year = 2025, .month = .jul, .day = 20 } + +writer = std.io.Writer.fixed(&buf); +try time.formatStruct(&writer); +std.debug.print("Time: {s}\n", .{writer.buffered()}); +// Output: Time{ .hour = 15, .minute = 30, .second = 45, .subsecond = 123 } +``` + Features: - Convert to/from epoch subseconds using world's fastest known algorithm. [^1] - Choose your precision: @@ -63,6 +95,8 @@ In-scope, PRs welcome: ## Why yet another date time library? - I frequently use different precisions for years, subseconds, and UTC offsets. +- Zig standard library [does not have accepted proposal](https://github.com/ziglang/zig/issues/8396). - Andrew [rejected this from stdlib.](https://github.com/ziglang/zig/pull/19549#issuecomment-2062091512) +- [Other implementations](https://github.com/nektro/zig-time/blob/master/time.zig) are outdated and never accepted too. [^1]: [Euclidean Affine Functions by Cassio and Neri.](https://arxiv.org/pdf/2102.06959) diff --git a/build.zig b/build.zig index cda2d3b..a24e3d7 100644 --- a/build.zig +++ b/build.zig @@ -11,19 +11,25 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); const lib_unit_tests = b.addTest(.{ - .root_source_file = entry, - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = entry, + .target = target, + .optimize = optimize, + }), }); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); const demo = b.addTest(.{ .name = "demo", - .root_source_file = b.path("demos.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("demos.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "datetime", .module = lib }, + }, + }), }); - demo.root_module.addImport("datetime", lib); const run_demo = b.addRunArtifact(demo); const test_step = b.step("test", "Run unit tests"); diff --git a/demos.zig b/demos.zig index ee976f9..50e9ad5 100644 --- a/demos.zig +++ b/demos.zig @@ -3,37 +3,65 @@ const datetime = @import("datetime"); test "now" { const date = datetime.Date.now(); - std.debug.print("today's date is {rfc3339}\n", .{ date }); + std.debug.print("today's date is {f}\n", .{date}); const time = datetime.Time.now(); - std.debug.print("today's time is {rfc3339}\n", .{ time }); + std.debug.print("today's time is {f}\n", .{time}); const nanotime = datetime.time.Nano.now(); - std.debug.print("today's nanotime is {rfc3339}\n", .{ nanotime }); + std.debug.print("today's nanotime is {f}\n", .{nanotime}); const dt = datetime.DateTime.now(); - std.debug.print("today's date and time is {rfc3339}\n", .{ dt }); + std.debug.print("today's date and time is {f}\n", .{dt}); const NanoDateTime = datetime.datetime.Advanced(datetime.Date, datetime.time.Nano, false); const ndt = NanoDateTime.now(); - std.debug.print("today's date and nanotime is {rfc3339}\n", .{ ndt }); + std.debug.print("today's date and nanotime is {f}\n", .{ndt}); } test "iterator" { - const from = datetime.Date.now(); + const from = datetime.Date.now(); const to = from.add(.{ .days = 7 }); var i = from; while (i.toEpoch() < to.toEpoch()) : (i = i.add(.{ .days = 1 })) { - std.debug.print("{s} {rfc3339}\n", .{ @tagName(i.weekday()), i }); + std.debug.print("{s} {f}\n", .{ @tagName(i.weekday()), i }); } } test "RFC 3339" { const d1 = try datetime.Date.parseRfc3339("2024-04-27"); - std.debug.print("d1 {rfc3339}\n", .{ d1 }); + std.debug.print("d1 {f}\n", .{d1}); const DateTimeOffset = datetime.datetime.Advanced(datetime.Date, datetime.time.Sec, true); const d2 = try DateTimeOffset.parseRfc3339("2024-04-27T13:03:23-04:00"); - std.debug.print("d2 {rfc3339}\n", .{ d2 }); + std.debug.print("d2 {f}\n", .{d2}); +} + +test "formatting options" { + const date = datetime.Date.init(2025, .jul, 20); + const time = datetime.time.Milli.init(15, 30, 45, 123); + const dt = datetime.DateTime.init(2025, .jul, 20, 15, 30, 45, 0, 0); + + std.debug.print("\n=== RFC3339 Format (default with {{f}}) ===\n", .{}); + std.debug.print("Date: {f}\n", .{date}); + std.debug.print("Time: {f}\n", .{time}); + std.debug.print("DateTime: {f}\n", .{dt}); + + std.debug.print("\n=== Struct Format (for debugging) ===\n", .{}); + + // To get struct format, use the formatStruct method directly + var buf: [256]u8 = undefined; + var writer = std.io.Writer.fixed(&buf); + + try date.formatStruct(&writer); + std.debug.print("Date: {s}\n", .{writer.buffered()}); + + writer = std.io.Writer.fixed(&buf); + try time.formatStruct(&writer); + std.debug.print("Time: {s}\n", .{writer.buffered()}); + + writer = std.io.Writer.fixed(&buf); + try dt.formatStruct(&writer); + std.debug.print("DateTime: {s}\n", .{writer.buffered()}); } diff --git a/src/date/gregorian.zig b/src/date/gregorian.zig index c2e4527..cad314d 100644 --- a/src/date/gregorian.zig +++ b/src/date/gregorian.zig @@ -223,7 +223,7 @@ pub fn Advanced(comptime YearT: type, comptime epoch: Comptime, shift: comptime_ }; } - fn fmtRfc3339(self: Self, writer: anytype) !void { + pub fn fmtRfc3339(self: Self, writer: *std.Io.Writer) !void { if (self.year < 0 or self.year > 9999) return error.Range; if (self.day < 1 or self.day > 99) return error.Range; if (self.month.numeric() < 1 or self.month.numeric() > 12) return error.Range; @@ -236,20 +236,19 @@ pub fn Advanced(comptime YearT: type, comptime epoch: Comptime, shift: comptime_ pub fn format( self: Self, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) (@TypeOf(writer).Error || error{Range})!void { - _ = options; - - if (std.mem.eql(u8, "rfc3339", fmt)) { - try self.fmtRfc3339(writer); - } else { - try writer.print( - "Date{{ .year = {d}, .month = .{s}, .day = .{d} }}", - .{ self.year, @tagName(self.month), self.day }, - ); - } + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + self.fmtRfc3339(writer) catch return error.WriteFailed; + } + + pub fn formatStruct( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + writer.print( + "Date{{ .year = {d}, .month = .{s}, .day = {d} }}", + .{ self.year, @tagName(self.month), self.day }, + ) catch return error.WriteFailed; } }; } @@ -335,9 +334,9 @@ test Gregorian { try std.testing.expectError(error.InvalidCharacter, T.parseRfc3339("2000-01-AD")); var buf: [32]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try d1.fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("1960-01-01", stream.getWritten()); + var writer = std.io.Writer.fixed(&buf); + try d1.fmtRfc3339(&writer); + try std.testing.expectEqualStrings("1960-01-01", writer.buffered()); } const WeekdayInt = IntFittingRange(1, 7); diff --git a/src/datetime.zig b/src/datetime.zig index b9aa963..e9157fb 100644 --- a/src/datetime.zig +++ b/src/datetime.zig @@ -146,8 +146,10 @@ pub fn Advanced(comptime DateT: type, comptime TimeT: type, comptime has_offset: return .{ .date = date, .time = time, .offset = offset }; } - fn fmtRfc3339(self: Self, writer: anytype) !void { - try writer.print("{rfc3339}T{rfc3339}", .{ self.date, self.time }); + fn fmtRfc3339(self: Self, writer: *std.Io.Writer) !void { + try self.date.fmtRfc3339(writer); + try writer.writeByte('T'); + try self.time.fmtRfc3339(writer); if (self.offset == 0) { try writer.writeByte('Z'); } else { @@ -160,17 +162,23 @@ pub fn Advanced(comptime DateT: type, comptime TimeT: type, comptime has_offset: pub fn format( self: Self, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) (@TypeOf(writer).Error || error{Range})!void { - _ = options; - - if (std.mem.eql(u8, "rfc3339", fmt)) { - try self.fmtRfc3339(writer); - } else { - try writer.print("DateTime{{ .date = {}, .time = {} }}", .{ self.date, self.time }); + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + self.fmtRfc3339(writer) catch return error.WriteFailed; + } + + pub fn formatStruct( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + writer.print("DateTime{{ .date = ", .{}) catch return error.WriteFailed; + self.date.formatStruct(writer) catch return error.WriteFailed; + writer.print(", .time = ", .{}) catch return error.WriteFailed; + self.time.formatStruct(writer) catch return error.WriteFailed; + if (comptime has_offset) { + writer.print(", .offset = {d}", .{self.offset}) catch return error.WriteFailed; } + writer.print(" }}", .{}) catch return error.WriteFailed; } }; } @@ -190,21 +198,21 @@ test Advanced { try expectEqual(T.init(1990, .dec, 31, 15, 59, 60, 0, -8 * s_per_hour), try T.parseRfc3339("1990-12-31T15:59:60-08:00")); try expectEqual(T.init(1937, .jan, 1, 12, 0, 27, 870, 20 * s_per_min), try T.parseRfc3339("1937-01-01T12:00:27.87+00:20")); - // negative offset + // negative offset try expectEqual(T.init(1985, .apr, 12, 23, 20, 50, 520, -20 * s_per_min), try T.parseRfc3339("1985-04-12T23:20:50.52-00:20")); try expectEqual(T.init(1985, .apr, 12, 23, 20, 50, 520, -10 * s_per_hour - 20 * s_per_min), try T.parseRfc3339("1985-04-12T23:20:50.52-10:20")); var buf: [32]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); - try T.init(1937, .jan, 1, 12, 0, 27, 870, 20 * s_per_min).fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("1937-01-01T12:00:27.870+00:20", stream.getWritten()); - - // negative offset - stream.reset(); - try T.init(1937, .jan, 1, 12, 0, 27, 870, -20 * s_per_min).fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("1937-01-01T12:00:27.870-00:20", stream.getWritten()); - - stream.reset(); - try T.init(1937, .jan, 1, 12, 0, 27, 870, -1 * s_per_hour - 20 * s_per_min).fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("1937-01-01T12:00:27.870-01:20", stream.getWritten()); + var writer = std.io.Writer.fixed(&buf); + try T.init(1937, .jan, 1, 12, 0, 27, 870, 20 * s_per_min).fmtRfc3339(&writer); + try std.testing.expectEqualStrings("1937-01-01T12:00:27.870+00:20", writer.buffered()); + + // negative offset + writer = std.io.Writer.fixed(&buf); + try T.init(1937, .jan, 1, 12, 0, 27, 870, -20 * s_per_min).fmtRfc3339(&writer); + try std.testing.expectEqualStrings("1937-01-01T12:00:27.870-00:20", writer.buffered()); + + writer = std.io.Writer.fixed(&buf); + try T.init(1937, .jan, 1, 12, 0, 27, 870, -1 * s_per_hour - 20 * s_per_min).fmtRfc3339(&writer); + try std.testing.expectEqualStrings("1937-01-01T12:00:27.870-01:20", writer.buffered()); } diff --git a/src/root.zig b/src/root.zig index 4cdb743..79cab94 100644 --- a/src/root.zig +++ b/src/root.zig @@ -17,22 +17,6 @@ pub const Time = time.Sec; /// * No timezones. pub const DateTime = datetime.Advanced(Date, time.Sec, false); -fn fmtRfc3339Impl( - date_time_or_datetime: Date, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, -) !void { - _ = fmt; - _ = options; - try date_time_or_datetime.toRfc3339(writer); -} - -/// Return a RFC 3339 formatter for a Date, Time, or DateTime type. -pub fn fmtRfc3339(date_time_or_datetime: anytype) std.fmt.Formatter(fmtRfc3339Impl) { - return .{ .data = date_time_or_datetime }; -} - /// Tests EpochSeconds -> DateTime and DateTime -> EpochSeconds fn testEpoch(secs: DateTime.EpochSubseconds, dt: DateTime) !void { const actual_dt = DateTime.fromEpoch(secs); diff --git a/src/time.zig b/src/time.zig index 7b2c9d9..dcc64ef 100644 --- a/src/time.zig +++ b/src/time.zig @@ -145,7 +145,7 @@ pub fn Advanced(decimal_precision: comptime_int) type { return .{ .hour = hour, .minute = minute, .second = second, .subsecond = subsecond }; } - fn fmtRfc3339(self: Self, writer: anytype) !void { + pub fn fmtRfc3339(self: Self, writer: *std.Io.Writer) !void { if (self.hour > 24 or self.minute > 59 or self.second > 60) return error.Range; try writer.print("{d:0>2}:{d:0>2}:{d:0>2}", .{ self.hour, self.minute, self.second }); if (self.subsecond != 0) { @@ -156,20 +156,19 @@ pub fn Advanced(decimal_precision: comptime_int) type { pub fn format( self: Self, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) (@TypeOf(writer).Error || error{Range})!void { - _ = options; - - if (std.mem.eql(u8, "rfc3339", fmt)) { - try self.fmtRfc3339(writer); - } else { - try writer.print( - "Time{{ .hour = {d}, .minute = .{d}, .second = .{d} }}", - .{ self.hour, self.minute, self.second }, - ); - } + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + self.fmtRfc3339(writer) catch return error.WriteFailed; + } + + pub fn formatStruct( + self: Self, + writer: *std.Io.Writer, + ) std.Io.Writer.Error!void { + writer.print( + "Time{{ .hour = {d}, .minute = {d}, .second = {d}, .subsecond = {d} }}", + .{ self.hour, self.minute, self.second, self.subsecond }, + ) catch return error.WriteFailed; } }; } @@ -199,15 +198,15 @@ test Advanced { try expectError(error.Parsing, Milli.parseRfc3339("02:00:0")); // missing second digit var buf: [32]u8 = undefined; - var stream = std.io.fixedBufferStream(&buf); + var writer = std.io.Writer.fixed(&buf); const time = Milli.init(22, 30, 20, 100); - try time.fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("22:30:20.100", stream.getWritten()); + try time.fmtRfc3339(&writer); + try std.testing.expectEqualStrings("22:30:20.100", writer.buffered()); - stream.reset(); + writer = std.io.Writer.fixed(&buf); const time2 = Milli.init(22, 30, 20, 100); - try time2.fmtRfc3339(stream.writer()); - try std.testing.expectEqualStrings("22:30:20.100", stream.getWritten()); + try time2.fmtRfc3339(&writer); + try std.testing.expectEqualStrings("22:30:20.100", writer.buffered()); } /// Time with second precision. From 12e5276d961f6536f42940b7d8a15ed0d182b364 Mon Sep 17 00:00:00 2001 From: Ivan Stepanov Date: Sun, 20 Jul 2025 15:19:12 +0200 Subject: [PATCH 2/2] fix: update zig setup action to v2 and bump minimum Zig version to 0.15.0 --- .github/workflows/publish_docs.yml | 2 +- .github/workflows/test.yml | 2 +- build.zig.zon | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml index fc38e4b..b9b1d13 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/publish_docs.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - - uses: mlugg/setup-zig@v1 + - uses: mlugg/setup-zig@v2 - run: zig build-lib src/root.zig -femit-docs=docs -fno-emit-bin - name: Upload artifact uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb58e50..7131a17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,5 +11,5 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - uses: mlugg/setup-zig@v1 + - uses: mlugg/setup-zig@v2 - run: zig build test diff --git a/build.zig.zon b/build.zig.zon index 3a80936..bf8f6f6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .datetime, - .version = "0.14.0", - .minimum_zig_version = "0.14.0", + .version = "0.15.0-dev.1147+69cf40da6", + .minimum_zig_version = "0.15.0-dev.1147+69cf40da6", .fingerprint = 0x93f3c6cab5757db5, // Changing this has security and trust implications. .paths = .{ "build.zig",