From 10c6d7675d18e91790e982b63922b409d6bacb21 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 06:53:42 -0600 Subject: [PATCH 01/10] add EnvConfig struct for zig 0.16 migration Add new EnvConfig struct to replace ?*const std.process.EnvMap parameter. This struct has two optional fields: tz and tzdir, which are the only env vars used in the codebase (TZ and TZDIR). Amp-Thread-ID: https://ampcode.com/threads/T-019c1943-dfe8-738f-bb29-33f5535caba6 Co-authored-by: Amp --- src/zeit.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/zeit.zig b/src/zeit.zig index 7e89060..47c3223 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -13,6 +13,11 @@ pub const Nanoseconds = i128; pub const Milliseconds = i128; pub const Seconds = i64; +pub const EnvConfig = struct { + tz: ?[]const u8 = null, + tzdir: ?[]const u8 = null, +}; + const ns_per_us = std.time.ns_per_us; const ns_per_ms = std.time.ns_per_ms; const ns_per_s = std.time.ns_per_s; From 58fce9c45f4b22311aedc181b8f688c926afa2e7 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 06:54:19 -0600 Subject: [PATCH 02/10] update local() signature for Zig 0.16 Change local(alloc, maybe_env) to local(alloc, io, env) to use the new EnvConfig struct and Zig 0.16's io parameter for file readers. - Replace ?*const std.process.EnvMap with EnvConfig parameter - Use env.tz instead of env.get("TZ") - Pass io to f.reader() as required by Zig 0.16 - Update localFromEnv call to pass io parameter Amp-Thread-ID: https://ampcode.com/threads/T-019c1944-707c-729d-bd53-54f720ca9a4b Co-authored-by: Amp --- src/zeit.zig | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index 47c3223..d0ab315 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -35,23 +35,21 @@ pub const utc: TimeZone = .{ .fixed = .{ .is_dst = false, } }; -pub fn local(alloc: std.mem.Allocator, maybe_env: ?*const std.process.EnvMap) !TimeZone { +pub fn local(alloc: std.mem.Allocator, io: std.Io, env: EnvConfig) !TimeZone { switch (builtin.os.tag) { .windows => { const win = try timezone.Windows.local(alloc); return .{ .windows = win }; }, else => { - if (maybe_env) |env| { - if (env.get("TZ")) |tz| { - return localFromEnv(alloc, tz, env); - } + if (env.tz) |tz| { + return localFromEnv(alloc, io, tz, env); } const f = try std.fs.cwd().openFile("/etc/localtime", .{}); defer f.close(); var io_buffer: [2048]u8 = undefined; - var reader = f.reader(&io_buffer); + var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; }, } From 1870585abb82b40e836760a34ee562583f475e42 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 06:55:08 -0600 Subject: [PATCH 03/10] update localFromEnv and loadTimeZone for Zig 0.16 Update localFromEnv() to accept std.Io and EnvConfig parameters. Update loadTimeZone() to accept std.Io and EnvConfig, replacing the optional EnvMap parameter. Change env.get("TZDIR") to env.tzdir. Fix f.reader() calls to use the new two-argument API. Amp-Thread-ID: https://ampcode.com/threads/T-019c1944-ff40-7226-a919-97166bf2902d Co-authored-by: Amp --- src/zeit.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index d0ab315..ba3ce5d 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -62,8 +62,9 @@ pub fn local(alloc: std.mem.Allocator, io: std.Io, env: EnvConfig) !TimeZone { // 3. A relative path, prefixed with ':' fn localFromEnv( alloc: std.mem.Allocator, + io: std.Io, tz: []const u8, - env: *const std.process.EnvMap, + env: EnvConfig, ) !TimeZone { assert(tz.len != 0); // TZ is empty string @@ -75,20 +76,21 @@ fn localFromEnv( const f = try std.fs.cwd().openFile(tz[1..], .{}); defer f.close(); var io_buffer: [1024]u8 = undefined; - var reader = f.reader(&io_buffer); + var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; } if (std.meta.stringToEnum(Location, tz[1..])) |loc| - return loadTimeZone(alloc, loc, env) + return loadTimeZone(alloc, io, loc, env) else return error.UnknownLocation; } pub fn loadTimeZone( alloc: std.mem.Allocator, + io: std.Io, loc: Location, - maybe_env: ?*const std.process.EnvMap, + env: EnvConfig, ) !TimeZone { switch (builtin.os.tag) { .windows => { @@ -100,11 +102,9 @@ pub fn loadTimeZone( var dir: std.fs.Dir = blk: { // If we have an env and a TZDIR, use that - if (maybe_env) |env| { - if (env.get("TZDIR")) |tzdir| { - const dir = try std.fs.openDirAbsolute(tzdir, .{}); - break :blk dir; - } + if (env.tzdir) |tzdir| { + const d = try std.fs.openDirAbsolute(tzdir, .{}); + break :blk d; } // Otherwise check well-known locations const zone_dirs = [_][]const u8{ @@ -115,8 +115,8 @@ pub fn loadTimeZone( "/etc/zoneinfo/", }; for (zone_dirs) |zone_dir| { - const dir = std.fs.openDirAbsolute(zone_dir, .{}) catch continue; - break :blk dir; + const d = std.fs.openDirAbsolute(zone_dir, .{}) catch continue; + break :blk d; } else return error.FileNotFound; }; @@ -124,7 +124,7 @@ pub fn loadTimeZone( const f = try dir.openFile(loc.asText(), .{}); defer f.close(); var io_buffer: [2048]u8 = undefined; - var reader = f.reader(&io_buffer); + var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; } From f10624d48740d6118a9475788ca8c89bf1c6a138 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 06:57:38 -0600 Subject: [PATCH 04/10] update instant() to accept std.Io for Zig 0.16 Add io parameter to instant() function signature and replace the removed std.time.nanoTimestamp() with std.Io.Clock.now(.real, io). Update all callers in tests to pass std.testing.io. Amp-Thread-ID: https://ampcode.com/threads/T-019c1945-d480-712c-b7cf-944f207f6048 Co-authored-by: Amp --- src/zeit.zig | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index ba3ce5d..e35a927 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -273,9 +273,9 @@ pub const Instant = struct { }; /// create a new Instant -pub fn instant(cfg: Instant.Config) !Instant { +pub fn instant(io: std.Io, cfg: Instant.Config) !Instant { const ts: Nanoseconds = switch (cfg.source) { - .now => std.time.nanoTimestamp(), + .now => (try std.Io.Clock.now(.real, io)).nanoseconds, .unix_timestamp => |unix| @as(i128, unix) * ns_per_s, .unix_nano => |nano| nano, .time => |time| time.instant().timestamp, @@ -303,10 +303,7 @@ pub fn instant(cfg: Instant.Config) !Instant { } test "instant" { - const original = Instant{ - .timestamp = std.time.nanoTimestamp(), - .timezone = &utc, - }; + const original = try instant(std.testing.io, .{}); const time = original.time(); const round_trip = time.instant(); try std.testing.expectEqual(original.timestamp, round_trip.timestamp); @@ -1881,7 +1878,7 @@ test { test "fmtStrftime" { var buf: [128]u8 = undefined; - const epoch = try instant(.{ .source = .{ .unix_timestamp = 0 } }); + const epoch = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = 0 } }); const time = epoch.time(); var fbs = std.io.fixedBufferStream(&buf); @@ -2015,7 +2012,7 @@ test Instant { defer env.deinit(); // Get an instant in time. The default gets "now" in UTC - const now = try instant(.{}); + const now = try instant(std.testing.io, .{}); // Load our local timezone. This needs an allocator. Optionally pass in a // *const std.process.EnvMap to support TZ and TZDIR environment variables @@ -2058,13 +2055,13 @@ test Instant { defer vienna.deinit(); // Parse an Instant from an ISO8601 or RFC3339 string - _ = try zeit.instant(.{ + _ = try zeit.instant(std.testing.io, .{ .source = .{ .iso8601 = "2024-03-16T08:38:29.496-1200", }, }); - _ = try zeit.instant(.{ + _ = try zeit.instant(std.testing.io, .{ .source = .{ .rfc3339 = "2024-03-16T08:38:29.496706064-1200", }, @@ -2074,9 +2071,9 @@ test Instant { test "github.com/rockorager/zeit/issues/15" { // https://github.com/rockorager/zeit/issues/15 const timestamp = 1732838300; - const tz = try loadTimeZone(std.testing.allocator, .@"Europe/Berlin", null); + const tz = try loadTimeZone(std.testing.allocator, std.testing.io, .@"Europe/Berlin", .{}); defer tz.deinit(); - const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp }, .timezone = &tz }); + const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp }, .timezone = &tz }); var list = std.ArrayList(u8).empty; defer list.deinit(std.testing.allocator); const time = inst.time(); @@ -2101,7 +2098,7 @@ test "github.com/rockorager/zeit/issues/15" { test "github.com/rockorager/zeit/issues/27" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); var list: std.ArrayList(u8) = .empty; defer list.deinit(std.testing.allocator); @@ -2115,7 +2112,7 @@ test "github.com/rockorager/zeit/issues/27" { test "github.com/rockorager/zeit/issues/24" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); var list: std.ArrayList(u8) = .empty; defer list.deinit(std.testing.allocator); @@ -2133,7 +2130,7 @@ test "github.com/rockorager/zeit/issues/24" { test "github.com/rockorager/zeit/issues/26" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); var list: std.ArrayList(u8) = .empty; defer list.deinit(std.testing.allocator); @@ -2159,7 +2156,7 @@ test "bufPrintRFC3339Nano" { .{ .timestamp = "2023-01-15T00:00:00.001002003Z" }, }; for (cases) |case| { - const iso = try instant(.{ + const iso = try instant(std.testing.io, .{ .source = .{ .rfc3339 = case.timestamp, }, From e097e268624503dc1e78303dd3aa2acbc01e9b8c Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 06:59:48 -0600 Subject: [PATCH 05/10] replace std.io.fixedBufferStream with std.Io.Writer.fixed Update test code to use Zig 0.16's new Writer API: - Replace std.io.fixedBufferStream(&buf) with std.Io.Writer.fixed(&buf) - Replace fbs.writer() calls with direct &writer references - Replace fbs.reset() with writer.end = 0 - Replace fbs.getWritten() with writer.buffered() Affects fmtStrftime and gofmt tests. Amp-Thread-ID: https://ampcode.com/threads/T-019c1948-1e3c-71c0-acbc-362fd1c33a22 Co-authored-by: Amp --- src/zeit.zig | 140 +++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index e35a927..a0cc17b 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -1881,59 +1881,57 @@ test "fmtStrftime" { const epoch = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = 0 } }); const time = epoch.time(); - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer = std.Io.Writer.fixed(&buf); - try std.testing.expectError(error.InvalidFormat, time.strftime(writer, "no trailing lone percent %")); + try std.testing.expectError(error.InvalidFormat, time.strftime(&writer, "no trailing lone percent %")); - fbs.reset(); - try time.strftime(writer, "%%"); - try std.testing.expectEqualStrings("%", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%%"); + try std.testing.expectEqualStrings("%", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%a %A %b %B %c %C"); - try std.testing.expectEqualStrings("Thu Thursday Jan January Thu Jan 1 00:00:00 1970 19", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%a %A %b %B %c %C"); + try std.testing.expectEqualStrings("Thu Thursday Jan January Thu Jan 1 00:00:00 1970 19", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%d %D %e %F %h"); - try std.testing.expectEqualStrings("01 01/01/70 1 1970-01-01 Jan", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%d %D %e %F %h"); + try std.testing.expectEqualStrings("01 01/01/70 1 1970-01-01 Jan", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%H %I %j %k %l %m %M"); - try std.testing.expectEqualStrings("00 12 001 0 12 01 00", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%H %I %j %k %l %m %M"); + try std.testing.expectEqualStrings("00 12 001 0 12 01 00", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%p %P %r %R %s %S"); - try std.testing.expectEqualStrings("AM am 12:00:00 AM 00:00 0 00", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%p %P %r %R %s %S"); + try std.testing.expectEqualStrings("AM am 12:00:00 AM 00:00 0 00", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%T %u"); - try std.testing.expectEqualStrings("00:00:00 4", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%T %u"); + try std.testing.expectEqualStrings("00:00:00 4", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%U"); - try std.testing.expectEqualStrings("00", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%U"); + try std.testing.expectEqualStrings("00", writer.buffered()); - fbs.reset(); + writer.end = 0; const d2 = (try time.instant().add(.{ .days = 3 })).time(); - try d2.strftime(writer, "%U"); - try std.testing.expectEqualStrings("01", fbs.getWritten()); + try d2.strftime(&writer, "%U"); + try std.testing.expectEqualStrings("01", writer.buffered()); - fbs.reset(); - try time.strftime(writer, "%w %W %x %X %y %Y %z %Z"); - try std.testing.expectEqualStrings("4 00 01/01/70 00:00:00 70 1970 +0000 UTC", fbs.getWritten()); + writer.end = 0; + try time.strftime(&writer, "%w %W %x %X %y %Y %z %Z"); + try std.testing.expectEqualStrings("4 00 01/01/70 00:00:00 70 1970 +0000 UTC", writer.buffered()); - fbs.reset(); + writer.end = 0; var d3 = time; d3.offset = -3600; - try d3.strftime(writer, "%z"); - try std.testing.expectEqualStrings("-0100", fbs.getWritten()); + try d3.strftime(&writer, "%z"); + try std.testing.expectEqualStrings("-0100", writer.buffered()); } test "gofmt" { var buf: [128]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); + var writer = std.Io.Writer.fixed(&buf); const time: Time = .{ .year = 1970, @@ -1942,17 +1940,17 @@ test "gofmt" { .designation = "UTC", }; - fbs.reset(); - try time.gofmt(writer, "Jan January J 01 02 03 04 05 06 002 Jan"); - try std.testing.expectEqualStrings("Feb February J 02 03 12 00 00 70 034 Feb", fbs.getWritten()); + writer.end = 0; + try time.gofmt(&writer, "Jan January J 01 02 03 04 05 06 002 Jan"); + try std.testing.expectEqualStrings("Feb February J 02 03 12 00 00 70 034 Feb", writer.buffered()); - fbs.reset(); - try time.gofmt(writer, "Mon Monday MST M 1 15 2 2006 _2 __2 Mon"); - try std.testing.expectEqualStrings("Tue Tuesday UTC M 2 00 3 1970 3 34 Tue", fbs.getWritten()); + writer.end = 0; + try time.gofmt(&writer, "Mon Monday MST M 1 15 2 2006 _2 __2 Mon"); + try std.testing.expectEqualStrings("Tue Tuesday UTC M 2 00 3 1970 3 34 Tue", writer.buffered()); - fbs.reset(); - try time.gofmt(writer, "3 4 5"); - try std.testing.expectEqualStrings("12 0 0", fbs.getWritten()); + writer.end = 0; + try time.gofmt(&writer, "3 4 5"); + try std.testing.expectEqualStrings("12 0 0", writer.buffered()); const time2: Time = .{ .offset = 3661, // 1 hour, 1 minute, 1 second @@ -1961,37 +1959,37 @@ test "gofmt" { .nanosecond = 789, }; - fbs.reset(); - try time2.gofmt(writer, "-070000 -07:00:00 -0700 -07:00 -07 -00"); - try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 -00", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "-070000 -07:00:00 -0700 -07:00 -07 -00"); + try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 -00", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00"); - try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 Z00", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00"); + try std.testing.expectEqualStrings("+010101 +01:01:01 +0101 +01:01 +01 Z00", writer.buffered()); - fbs.reset(); - try time.gofmt(writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00"); - try std.testing.expectEqualStrings("Z Z Z Z Z Z00", fbs.getWritten()); + writer.end = 0; + try time.gofmt(&writer, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00"); + try std.testing.expectEqualStrings("Z Z Z Z Z Z00", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "frac ."); - try std.testing.expectEqualStrings("frac .", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "frac ."); + try std.testing.expectEqualStrings("frac .", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "frac .000000000"); - try std.testing.expectEqualStrings("frac .123456789", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "frac .000000000"); + try std.testing.expectEqualStrings("frac .123456789", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "frac .999999999"); - try std.testing.expectEqualStrings("frac .123456789", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "frac .999999999"); + try std.testing.expectEqualStrings("frac .123456789", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "frac .000000000000"); - try std.testing.expectEqualStrings("frac .123456789000", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "frac .000000000000"); + try std.testing.expectEqualStrings("frac .123456789000", writer.buffered()); - fbs.reset(); - try time2.gofmt(writer, "frac .0000000"); - try std.testing.expectEqualStrings("frac .1234567", fbs.getWritten()); + writer.end = 0; + try time2.gofmt(&writer, "frac .0000000"); + try std.testing.expectEqualStrings("frac .1234567", writer.buffered()); const time3: Time = .{ .offset = 3661, // 1 hour, 1 minute, 1 second @@ -1999,9 +1997,9 @@ test "gofmt" { .microsecond = 456, }; - fbs.reset(); - try time3.gofmt(writer, "frac .999999999"); - try std.testing.expectEqualStrings("frac .123456", fbs.getWritten()); + writer.end = 0; + try time3.gofmt(&writer, "frac .999999999"); + try std.testing.expectEqualStrings("frac .123456", writer.buffered()); } test Instant { From 788e8a971ff88ec71dee67f8914f4d3c5c6d54ea Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 07:03:11 -0600 Subject: [PATCH 06/10] update tests to use new Zig 0.16 API - Replace std.io.Reader with std.Io.Reader in TZInfo.parse - Replace writeByteNTimes with splatByteAll - Update local() call in Instant test to use new 3-arg signature - Replace std.io.null_writer with std.Io.Writer.Discarding - Replace ArrayList.writer() with std.Io.Writer.Allocating in tests - Update loadTimeZone calls to pass std.testing.io and EnvConfig Amp-Thread-ID: https://ampcode.com/threads/T-019c194a-2af5-75d1-8b83-8635b95d7d7e Co-authored-by: Amp --- src/timezone.zig | 4 +-- src/zeit.zig | 71 ++++++++++++++++++++++++------------------------ 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/timezone.zig b/src/timezone.zig index be16555..012518a 100644 --- a/src/timezone.zig +++ b/src/timezone.zig @@ -522,7 +522,7 @@ pub const TZInfo = struct { }, }; - pub fn parse(allocator: std.mem.Allocator, reader: *std.io.Reader) !TZInfo { + pub fn parse(allocator: std.mem.Allocator, reader: *std.Io.Reader) !TZInfo { var legacy_header = try reader.takeStruct(Header, .big); // handles endianness for us if (!std.mem.eql(u8, &legacy_header.magic, "TZif")) return error.BadHeader; if (legacy_header.version != 0 and legacy_header.version != '2' and legacy_header.version != '3') return error.BadVersion; @@ -545,7 +545,7 @@ pub const TZInfo = struct { } } - fn parseBlock(allocator: std.mem.Allocator, reader: *std.io.Reader, header: Header, legacy: bool) !TZInfo { + fn parseBlock(allocator: std.mem.Allocator, reader: *std.Io.Reader, header: Header, legacy: bool) !TZInfo { if (header.counts.isstdcnt != 0 and header.counts.isstdcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isstdcnt [...] MUST either be zero or equal to "typecnt" if (header.counts.isutcnt != 0 and header.counts.isutcnt != header.counts.typecnt) return error.Malformed; // rfc8536: isutcnt [...] MUST either be zero or equal to "typecnt" if (header.counts.typecnt == 0) return error.Malformed; // rfc8536: typecnt [...] MUST NOT be zero diff --git a/src/zeit.zig b/src/zeit.zig index a0cc17b..22a41c8 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -1644,7 +1644,7 @@ pub const Time = struct { ); try writer.writeAll(str[0..@min(n, str.len)]); if (n > str.len) - try writer.writeByteNTimes('0', n - str.len); + try writer.splatByteAll('0', n - str.len); }, '9' => { var n: usize = 0; @@ -2006,15 +2006,13 @@ test Instant { const zeit = @This(); const alloc = std.testing.allocator; - var env = try std.process.getEnvMap(alloc); - defer env.deinit(); // Get an instant in time. The default gets "now" in UTC const now = try instant(std.testing.io, .{}); - // Load our local timezone. This needs an allocator. Optionally pass in a - // *const std.process.EnvMap to support TZ and TZDIR environment variables - const local_tz = try zeit.local(alloc, &env); + // Load our local timezone. This needs an allocator. Optionally pass in an + // EnvConfig to support TZ and TZDIR environment variables + const local_tz = try zeit.local(alloc, std.testing.io, .{}); defer local_tz.deinit(); // Convert our instant to a new timezone @@ -2039,17 +2037,18 @@ test Instant { // .offset = -18000, // } - const anywriter = std.io.null_writer.any(); + var discard_buf: [256]u8 = undefined; + var discarding: std.Io.Writer.Discarding = .init(&discard_buf); // Format using strftime specifier. Format strings are not required to be comptime - try dt.strftime(anywriter, "%Y-%m-%d %H:%M:%S %Z"); + try dt.strftime(&discarding.writer, "%Y-%m-%d %H:%M:%S %Z"); // Or...golang magic date specifiers. Format strings are not required to be comptime - try dt.gofmt(anywriter, "2006-01-02 15:04:05 MST"); + try dt.gofmt(&discarding.writer, "2006-01-02 15:04:05 MST"); // Load an arbitrary location using IANA location syntax. The location name // comes from an enum which will automatically map IANA location names to - // Windows names, as needed. Pass an optional EnvMap to support TZDIR - const vienna = try zeit.loadTimeZone(alloc, .@"Europe/Vienna", &env); + // Windows names, as needed. Pass an optional EnvConfig to support TZDIR + const vienna = try zeit.loadTimeZone(alloc, std.testing.io, .@"Europe/Vienna", .{}); defer vienna.deinit(); // Parse an Instant from an ISO8601 or RFC3339 string @@ -2072,8 +2071,6 @@ test "github.com/rockorager/zeit/issues/15" { const tz = try loadTimeZone(std.testing.allocator, std.testing.io, .@"Europe/Berlin", .{}); defer tz.deinit(); const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp }, .timezone = &tz }); - var list = std.ArrayList(u8).empty; - defer list.deinit(std.testing.allocator); const time = inst.time(); try std.testing.expectEqual(timestamp, time.instant().unixTimestamp()); @@ -2085,12 +2082,14 @@ test "github.com/rockorager/zeit/issues/15" { try std.testing.expectEqual(58, time.minute); try std.testing.expectEqual(20, time.second); - try time.strftime(list.writer(std.testing.allocator), "%a %A %u"); - try std.testing.expectEqualStrings("Fri Friday 5", list.items); + var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer aw.deinit(); + try time.strftime(&aw.writer, "%a %A %u"); + try std.testing.expectEqualStrings("Fri Friday 5", aw.writer.buffered()); - list.clearRetainingCapacity(); - try time.gofmt(list.writer(std.testing.allocator), "Mon Monday"); - try std.testing.expectEqualStrings("Fri Friday", list.items); + aw.writer.end = 0; + try time.gofmt(&aw.writer, "Mon Monday"); + try std.testing.expectEqualStrings("Fri Friday", aw.writer.buffered()); } test "github.com/rockorager/zeit/issues/27" { @@ -2098,13 +2097,13 @@ test "github.com/rockorager/zeit/issues/27" { const timestamp = 1745414170; const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); - var list: std.ArrayList(u8) = .empty; - defer list.deinit(std.testing.allocator); + var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer aw.deinit(); const time = inst.time(); - try time.gofmt(list.writer(std.testing.allocator), "02.01.2006"); - try std.testing.expectEqualStrings("23.04.2025", list.items); + try time.gofmt(&aw.writer, "02.01.2006"); + try std.testing.expectEqualStrings("23.04.2025", aw.writer.buffered()); } test "github.com/rockorager/zeit/issues/24" { @@ -2112,17 +2111,17 @@ test "github.com/rockorager/zeit/issues/24" { const timestamp = 1745414170; const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); - var list: std.ArrayList(u8) = .empty; - defer list.deinit(std.testing.allocator); + var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer aw.deinit(); const time = inst.time(); - try time.gofmt(list.writer(std.testing.allocator), "3pm MST"); - try std.testing.expectEqualStrings("1pm UTC", list.items); - list.clearRetainingCapacity(); + try time.gofmt(&aw.writer, "3pm MST"); + try std.testing.expectEqualStrings("1pm UTC", aw.writer.buffered()); + aw.writer.end = 0; - try time.gofmt(list.writer(std.testing.allocator), "3p MST"); - try std.testing.expectEqualStrings("1p UTC", list.items); + try time.gofmt(&aw.writer, "3p MST"); + try std.testing.expectEqualStrings("1p UTC", aw.writer.buffered()); } test "github.com/rockorager/zeit/issues/26" { @@ -2130,17 +2129,17 @@ test "github.com/rockorager/zeit/issues/26" { const timestamp = 1745414170; const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); - var list: std.ArrayList(u8) = .empty; - defer list.deinit(std.testing.allocator); + var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer aw.deinit(); const time = inst.time(); - try time.gofmt(list.writer(std.testing.allocator), "02nd"); - try std.testing.expectEqualStrings("23rd", list.items); - list.clearRetainingCapacity(); + try time.gofmt(&aw.writer, "02nd"); + try std.testing.expectEqualStrings("23rd", aw.writer.buffered()); + aw.writer.end = 0; - try time.gofmt(list.writer(std.testing.allocator), "02ND"); - try std.testing.expectEqualStrings("23RD", list.items); + try time.gofmt(&aw.writer, "02ND"); + try std.testing.expectEqualStrings("23RD", aw.writer.buffered()); } test "bufPrintRFC3339Nano" { From 6acbae3ddff9a1088e7c5512e564f8a3e2d3bd76 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Sun, 1 Feb 2026 07:12:37 -0600 Subject: [PATCH 07/10] migrate file/directory operations to std.Io API Replace deprecated std.fs file and directory operations with their std.Io equivalents for Zig 0.16 compatibility: - std.fs.cwd().openFile -> std.Io.Dir.cwd().openFile(io, ...) - std.fs.openDirAbsolute -> std.Io.Dir.cwd().openDir(io, ...) - std.fs.Dir -> std.Io.Dir - f.close() -> f.close(io) - dir.close() -> dir.close(io) - dir.openFile(...) -> dir.openFile(io, ...) Amp-Thread-ID: https://ampcode.com/threads/T-019c194f-494e-7058-9236-403b366f06ad Co-authored-by: Amp --- src/zeit.zig | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index 22a41c8..51d52bd 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -46,8 +46,8 @@ pub fn local(alloc: std.mem.Allocator, io: std.Io, env: EnvConfig) !TimeZone { return localFromEnv(alloc, io, tz, env); } - const f = try std.fs.cwd().openFile("/etc/localtime", .{}); - defer f.close(); + const f = try std.Io.Dir.cwd().openFile(io, "/etc/localtime", .{}); + defer f.close(io); var io_buffer: [2048]u8 = undefined; var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; @@ -73,8 +73,8 @@ fn localFromEnv( assert(tz.len > 1); // TZ not long enough if (tz[1] == '/') { - const f = try std.fs.cwd().openFile(tz[1..], .{}); - defer f.close(); + const f = try std.Io.Dir.cwd().openFile(io, tz[1..], .{}); + defer f.close(io); var io_buffer: [1024]u8 = undefined; var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; @@ -100,10 +100,10 @@ pub fn loadTimeZone( else => {}, } - var dir: std.fs.Dir = blk: { + var dir: std.Io.Dir = blk: { // If we have an env and a TZDIR, use that if (env.tzdir) |tzdir| { - const d = try std.fs.openDirAbsolute(tzdir, .{}); + const d = try std.Io.Dir.cwd().openDir(io, tzdir, .{}); break :blk d; } // Otherwise check well-known locations @@ -115,14 +115,14 @@ pub fn loadTimeZone( "/etc/zoneinfo/", }; for (zone_dirs) |zone_dir| { - const d = std.fs.openDirAbsolute(zone_dir, .{}) catch continue; + const d = std.Io.Dir.cwd().openDir(io, zone_dir, .{}) catch continue; break :blk d; } else return error.FileNotFound; }; - defer dir.close(); - const f = try dir.openFile(loc.asText(), .{}); - defer f.close(); + defer dir.close(io); + const f = try dir.openFile(io, loc.asText(), .{}); + defer f.close(io); var io_buffer: [2048]u8 = undefined; var reader = f.reader(io, &io_buffer); return .{ .tzinfo = try timezone.TZInfo.parse(alloc, &reader.interface) }; From 3cb3ca55e329e52b677bd622dafb27bb05c02937 Mon Sep 17 00:00:00 2001 From: Krzysztof Wolicki Date: Sun, 1 Feb 2026 16:26:09 +0100 Subject: [PATCH 08/10] Fix compile errors in timezone --- src/timezone.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/timezone.zig b/src/timezone.zig index 012518a..5290e63 100644 --- a/src/timezone.zig +++ b/src/timezone.zig @@ -117,7 +117,7 @@ pub const Posix = struct { const julian = try std.fmt.parseInt(u9, str[1..], 10); return .{ .julian = .{ .day = julian } }; }, - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => |_| { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { const julian = try std.fmt.parseInt(u9, str, 10); return .{ .julian_leap = .{ .day = julian } }; }, @@ -795,7 +795,7 @@ pub const Windows = struct { /// 4. Determine if we are in DST or not /// 5. Return result pub fn adjust(self: Windows, timestamp: Seconds) AdjustedTime { - const instant = zeit.instant(.{ .source = .{ .unix_timestamp = timestamp } }) catch unreachable; + const instant = zeit.instant(undefined, .{ .source = .{ .unix_timestamp = timestamp } }) catch unreachable; const time = instant.time(); const systemtime: windows.SYSTEMTIME = .{ @@ -861,7 +861,7 @@ pub const Windows = struct { const days_from_epoch = @divFloor(timestamp, s_per_day); // first_of_month is the weekday on the first of the month - const first_of_month = zeit.weekdayFromDays(days_from_epoch - time.wDay + 1); + const first_of_month = zeit.weekdayFromDays(@intCast(days_from_epoch - time.wDay + 1)); // In the start transition month if (time.wMonth == start.wMonth) { From 61557682f1716ec5d00f826ce1ee540928df1a6e Mon Sep 17 00:00:00 2001 From: Krzysztof Wolicki Date: Sun, 1 Feb 2026 16:27:21 +0100 Subject: [PATCH 09/10] Add `Time.timeFmt` to support using `zeit.Time` in prints with `{f}` formatting `timeFmt` supports both `gofmt` and `strftime` --- src/zeit.zig | 125 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 3 deletions(-) diff --git a/src/zeit.zig b/src/zeit.zig index 51d52bd..3da212e 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -1256,8 +1256,30 @@ pub const Time = struct { } } + pub const FmtKind = enum { gofmt, strftime }; + pub fn timeFmt(self: Time, kind: FmtKind, fmt_str: []const u8) TimeFmt { + return .{ + .time = self, + .kind = kind, + .str = fmt_str, + }; + } + + const TimeFmt = struct { + time: Time, + kind: FmtKind, + str: []const u8, + + pub fn format(self: TimeFmt, writer: *std.Io.Writer) !void { + switch (self.kind) { + .gofmt => self.time.gofmt(writer, self.str) catch return error.WriteFailed, + .strftime => self.time.strftime(writer, self.str) catch return error.WriteFailed, + } + } + }; + /// Format time using strftime(3) specified, eg %Y-%m-%dT%H:%M:%S - pub fn strftime(self: Time, writer: anytype, fmt: []const u8) !void { + pub fn strftime(self: Time, writer: *std.Io.Writer, fmt: []const u8) !void { const inst = self.instant(); var i: usize = 0; while (i < fmt.len) { @@ -1425,7 +1447,7 @@ pub const Time = struct { } /// Format using golang magic date format. - pub fn gofmt(self: Time, writer: anytype, fmt: []const u8) !void { + pub fn gofmt(self: Time, writer: *std.Io.Writer, fmt: []const u8) !void { var i: usize = 0; while (i < fmt.len) : (i += 1) { const b = fmt[i]; @@ -2002,6 +2024,103 @@ test "gofmt" { try std.testing.expectEqualStrings("frac .123456", writer.buffered()); } +test "Time.timeFmt" { + const time: Time = .{ + .year = 1970, + .month = .feb, + .day = 3, + .designation = "UTC", + }; + try std.testing.expectFmt( + "Feb February J 02 03 12 00 00 70 034 Feb", + "{f}", + .{time.timeFmt(.gofmt, "Jan January J 01 02 03 04 05 06 002 Jan")}, + ); + + try std.testing.expectFmt( + "Tue Tuesday UTC M 2 00 3 1970 3 34 Tue", + "{f}", + .{time.timeFmt(.gofmt, "Mon Monday MST M 1 15 2 2006 _2 __2 Mon")}, + ); + + try std.testing.expectFmt( + "12 0 0", + "{f}", + .{time.timeFmt(.gofmt, "3 4 5")}, + ); + + const time2: Time = .{ + .offset = 3661, // 1 hour, 1 minute, 1 second + .millisecond = 123, + .microsecond = 456, + .nanosecond = 789, + }; + + try std.testing.expectFmt( + "+010101 +01:01:01 +0101 +01:01 +01 -00", + "{f}", + .{time2.timeFmt(.gofmt, "-070000 -07:00:00 -0700 -07:00 -07 -00")}, + ); + + try std.testing.expectFmt( + "+010101 +01:01:01 +0101 +01:01 +01 Z00", + "{f}", + .{time2.timeFmt(.gofmt, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00")}, + ); + + try std.testing.expectFmt( + "Z Z Z Z Z Z00", + "{f}", + .{time.timeFmt(.gofmt, "Z070000 Z07:00:00 Z0700 Z07:00 Z07 Z00")}, + ); + + try std.testing.expectFmt( + "frac .", + "{f}", + .{time2.timeFmt(.gofmt, "frac .")}, + ); + + const time3: Time = .{ + .offset = 3661, // 1 hour, 1 minute, 1 second + .millisecond = 123, + .microsecond = 456, + }; + + try std.testing.expectFmt( + "frac .123456", + "{f}", + .{time3.timeFmt(.gofmt, "frac .999999999")}, + ); + + const epoch = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = 0 } }); + const time4 = epoch.time(); + + var buf: [128]u8 = undefined; + var writer = std.Io.Writer.fixed(&buf); + try std.testing.expectError(error.WriteFailed, writer.print( + "{f}", + .{time4.timeFmt(.strftime, "no trailing lone percent %")}, + )); + + try std.testing.expectFmt( + "%", + "{f}", + .{time4.timeFmt(.strftime, "%%")}, + ); + + try std.testing.expectFmt( + "Thu Thursday Jan January Thu Jan 1 00:00:00 1970 19", + "{f}", + .{time4.timeFmt(.strftime, "%a %A %b %B %c %C")}, + ); + + try std.testing.expectFmt( + "01 01/01/70 1 1970-01-01 Jan", + "{f}", + .{time4.timeFmt(.strftime, "%d %D %e %F %h")}, + ); +} + test Instant { const zeit = @This(); @@ -2118,8 +2237,8 @@ test "github.com/rockorager/zeit/issues/24" { try time.gofmt(&aw.writer, "3pm MST"); try std.testing.expectEqualStrings("1pm UTC", aw.writer.buffered()); - aw.writer.end = 0; + aw.writer.end = 0; try time.gofmt(&aw.writer, "3p MST"); try std.testing.expectEqualStrings("1p UTC", aw.writer.buffered()); } From 474865bcc18114f6ad4ee5309af0318cef85634e Mon Sep 17 00:00:00 2001 From: Krzysztof Wolicki Date: Sun, 1 Feb 2026 20:38:50 +0100 Subject: [PATCH 10/10] Pass `io` to `zeit.instant` only when needed since no source other than `now` requires it --- src/timezone.zig | 2 +- src/zeit.zig | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/timezone.zig b/src/timezone.zig index 5290e63..a6fda6e 100644 --- a/src/timezone.zig +++ b/src/timezone.zig @@ -795,7 +795,7 @@ pub const Windows = struct { /// 4. Determine if we are in DST or not /// 5. Return result pub fn adjust(self: Windows, timestamp: Seconds) AdjustedTime { - const instant = zeit.instant(undefined, .{ .source = .{ .unix_timestamp = timestamp } }) catch unreachable; + const instant = zeit.instant(.{ .source = .{ .unix_timestamp = timestamp } }) catch unreachable; const time = instant.time(); const systemtime: windows.SYSTEMTIME = .{ diff --git a/src/zeit.zig b/src/zeit.zig index 3da212e..66eaf1c 100644 --- a/src/zeit.zig +++ b/src/zeit.zig @@ -139,6 +139,7 @@ pub const Instant = struct { pub const Config = struct { source: Source = .now, timezone: *const TimeZone = &utc, + io: ?std.Io = null, }; /// possible sources to create an Instant @@ -273,9 +274,9 @@ pub const Instant = struct { }; /// create a new Instant -pub fn instant(io: std.Io, cfg: Instant.Config) !Instant { +pub fn instant(cfg: Instant.Config) !Instant { const ts: Nanoseconds = switch (cfg.source) { - .now => (try std.Io.Clock.now(.real, io)).nanoseconds, + .now => (try std.Io.Clock.now(.real, cfg.io orelse return error.NeedIo)).toNanoseconds(), .unix_timestamp => |unix| @as(i128, unix) * ns_per_s, .unix_nano => |nano| nano, .time => |time| time.instant().timestamp, @@ -303,7 +304,7 @@ pub fn instant(io: std.Io, cfg: Instant.Config) !Instant { } test "instant" { - const original = try instant(std.testing.io, .{}); + const original = try instant(.{ .io = std.testing.io }); const time = original.time(); const round_trip = time.instant(); try std.testing.expectEqual(original.timestamp, round_trip.timestamp); @@ -1900,7 +1901,7 @@ test { test "fmtStrftime" { var buf: [128]u8 = undefined; - const epoch = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = 0 } }); + const epoch = try instant(.{ .source = .{ .unix_timestamp = 0 } }); const time = epoch.time(); var writer = std.Io.Writer.fixed(&buf); @@ -2092,7 +2093,7 @@ test "Time.timeFmt" { .{time3.timeFmt(.gofmt, "frac .999999999")}, ); - const epoch = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = 0 } }); + const epoch = try instant(.{ .source = .{ .unix_timestamp = 0 } }); const time4 = epoch.time(); var buf: [128]u8 = undefined; @@ -2127,7 +2128,7 @@ test Instant { const alloc = std.testing.allocator; // Get an instant in time. The default gets "now" in UTC - const now = try instant(std.testing.io, .{}); + const now = try instant(.{ .io = std.testing.io }); // Load our local timezone. This needs an allocator. Optionally pass in an // EnvConfig to support TZ and TZDIR environment variables @@ -2171,13 +2172,13 @@ test Instant { defer vienna.deinit(); // Parse an Instant from an ISO8601 or RFC3339 string - _ = try zeit.instant(std.testing.io, .{ + _ = try zeit.instant(.{ .source = .{ .iso8601 = "2024-03-16T08:38:29.496-1200", }, }); - _ = try zeit.instant(std.testing.io, .{ + _ = try zeit.instant(.{ .source = .{ .rfc3339 = "2024-03-16T08:38:29.496706064-1200", }, @@ -2189,7 +2190,7 @@ test "github.com/rockorager/zeit/issues/15" { const timestamp = 1732838300; const tz = try loadTimeZone(std.testing.allocator, std.testing.io, .@"Europe/Berlin", .{}); defer tz.deinit(); - const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp }, .timezone = &tz }); + const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp }, .timezone = &tz }); const time = inst.time(); try std.testing.expectEqual(timestamp, time.instant().unixTimestamp()); @@ -2214,7 +2215,7 @@ test "github.com/rockorager/zeit/issues/15" { test "github.com/rockorager/zeit/issues/27" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); defer aw.deinit(); @@ -2228,7 +2229,7 @@ test "github.com/rockorager/zeit/issues/27" { test "github.com/rockorager/zeit/issues/24" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); defer aw.deinit(); @@ -2246,7 +2247,7 @@ test "github.com/rockorager/zeit/issues/24" { test "github.com/rockorager/zeit/issues/26" { // April 23, 2025 const timestamp = 1745414170; - const inst = try instant(std.testing.io, .{ .source = .{ .unix_timestamp = timestamp } }); + const inst = try instant(.{ .source = .{ .unix_timestamp = timestamp } }); var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); defer aw.deinit(); @@ -2272,7 +2273,7 @@ test "bufPrintRFC3339Nano" { .{ .timestamp = "2023-01-15T00:00:00.001002003Z" }, }; for (cases) |case| { - const iso = try instant(std.testing.io, .{ + const iso = try instant(.{ .source = .{ .rfc3339 = case.timestamp, },