这是indexloc提供的服务,不要输入任何密码
Skip to content
Open
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
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ Generic Date, Time, and DateTime library.

## Installation
```sh
zig fetch --save "https://github.com/clickingbuttons/datetime/archive/refs/tags/0.14.0.tar.gz"
zig fetch --save "git+https://github.com/clickingbuttons/datetime.git"
# or
zig fetch --save "https://github.com/clickingbuttons/datetime/archive/refs/tags/0.15.1.tar.gz"
```

### build.zig
Expand All @@ -29,23 +31,47 @@ 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):** The `{any}` format specifier can be used for debug-style output:

```zig
std.debug.print("Date: {any}\n", .{date});
// Output: .{ .year = 2025, .month = .jul, .day = 20 }

std.debug.print("Time: {any}\n", .{time});
// Output: .{ .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 +89,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)
16 changes: 9 additions & 7 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ 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 = lib,
});
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.1",
.minimum_zig_version = "0.15.1",
.fingerprint = 0x93f3c6cab5757db5, // Changing this has security and trust implications.
.paths = .{
"build.zig",
Expand Down
34 changes: 25 additions & 9 deletions demos.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,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});
}

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("{t} {f}\n", .{ 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 with {{any}}) ===\n", .{});
std.debug.print("Date: {any}\n", .{date});
std.debug.print("Time: {any}\n", .{time});
std.debug.print("DateTime: {any}\n", .{dt});
}
28 changes: 10 additions & 18 deletions src/date/gregorian.zig
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,10 @@ 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,
) (std.Io.Writer.Error || error{Range})!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 +239,9 @@ 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;
}
};
}
Expand Down Expand Up @@ -335,9 +327,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
49 changes: 23 additions & 26 deletions src/datetime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,13 @@ 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 });
pub fn fmtRfc3339(
self: Self,
writer: *std.Io.Writer,
) (std.Io.Writer.Error || error{Range})!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 +165,9 @@ 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;
}
};
}
Expand All @@ -190,21 +187,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.consumeAll();
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.consumeAll();
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
34 changes: 13 additions & 21 deletions src/time.zig
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ 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,
) (std.Io.Writer.Error || error{Range})!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) {
Expand All @@ -156,20 +159,9 @@ 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;
}
};
}
Expand Down Expand Up @@ -199,15 +191,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.consumeAll();
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.
Expand Down