Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/publish_docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```

Expand All @@ -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:
Expand All @@ -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)
20 changes: 13 additions & 7 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
46 changes: 37 additions & 9 deletions demos.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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()});
}
35 changes: 17 additions & 18 deletions src/date/gregorian.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
};
}
Expand Down Expand Up @@ -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);
Expand Down
58 changes: 33 additions & 25 deletions src/datetime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
};
}
Expand All @@ -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());
}
16 changes: 0 additions & 16 deletions src/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading