//! Generic padding wrapper for any type with a `format(self, *Writer)` //! method. //! //! Wraps an inner value plus a target width and an alignment side. //! Its own `format` method renders the inner value into a stack //! buffer to measure its length, then writes the padded output to //! the caller's writer. //! //! Comptime instantiation per inner type is essentially free: the //! compiler emits one body per `Padded(T)` actually used, with the //! inner `format` call inlined. //! //! Used by `Money.padRight`/`padLeft` and `Date.padRight`/`padLeft`, //! plus any future format-bearing type that needs column alignment. const std = @import("std"); pub fn Padded(comptime T: type) type { return struct { inner: T, width: usize, alignment: enum { left, right }, const Self = @This(); pub fn format(self: Self, w: *std.Io.Writer) std.Io.Writer.Error!void { // Render the inner value into a stack buffer first so // we can measure its length and pad. 64 bytes covers // every realistic format-method output in this codebase // (Money's worst case is ~14 chars; Date is exactly 10). var tmp: [64]u8 = undefined; var fixed = std.Io.Writer.fixed(&tmp); self.inner.format(&fixed) catch unreachable; const text = fixed.buffered(); const pad = if (text.len >= self.width) 0 else self.width - text.len; switch (self.alignment) { .right => { try w.splatByteAll(' ', pad); try w.writeAll(text); }, .left => { try w.writeAll(text); try w.splatByteAll(' ', pad); }, } } }; } // ── Tests ────────────────────────────────────────────────────── const testing = std.testing; /// Simple type with a format method, used to verify Padded(T) works /// for arbitrary inner types (not just Money / Date). const Tag = struct { label: []const u8, pub fn format(self: Tag, w: *std.Io.Writer) std.Io.Writer.Error!void { try w.writeAll(self.label); } }; /// Helper: render `value` via `{f}` and return an owned string. Caller frees. fn renderAlloc(value: anytype) ![]u8 { var aw: std.Io.Writer.Allocating = .init(testing.allocator); defer aw.deinit(); try aw.writer.print("{f}", .{value}); return aw.toOwnedSlice(); } test "Padded right-aligns shorter inner" { const padded: Padded(Tag) = .{ .inner = .{ .label = "x" }, .width = 5, .alignment = .right }; const s = try renderAlloc(padded); defer testing.allocator.free(s); try testing.expectEqualStrings(" x", s); } test "Padded left-aligns shorter inner" { const padded: Padded(Tag) = .{ .inner = .{ .label = "x" }, .width = 5, .alignment = .left }; const s = try renderAlloc(padded); defer testing.allocator.free(s); try testing.expectEqualStrings("x ", s); } test "Padded leaves inner unchanged when wider than width" { const padded: Padded(Tag) = .{ .inner = .{ .label = "abcdef" }, .width = 3, .alignment = .right }; const s = try renderAlloc(padded); defer testing.allocator.free(s); try testing.expectEqualStrings("abcdef", s); } test "Padded equal width emits exactly the inner with no padding" { const padded: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 3, .alignment = .right }; const s = try renderAlloc(padded); defer testing.allocator.free(s); try testing.expectEqualStrings("abc", s); } test "Padded width=0 emits inner unchanged regardless of alignment" { const right: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 0, .alignment = .right }; const left: Padded(Tag) = .{ .inner = .{ .label = "abc" }, .width = 0, .alignment = .left }; const sr = try renderAlloc(right); defer testing.allocator.free(sr); const sl = try renderAlloc(left); defer testing.allocator.free(sl); try testing.expectEqualStrings("abc", sr); try testing.expectEqualStrings("abc", sl); } test "Padded with empty inner emits pure spaces" { const right: Padded(Tag) = .{ .inner = .{ .label = "" }, .width = 4, .alignment = .right }; const left: Padded(Tag) = .{ .inner = .{ .label = "" }, .width = 4, .alignment = .left }; const sr = try renderAlloc(right); defer testing.allocator.free(sr); const sl = try renderAlloc(left); defer testing.allocator.free(sl); try testing.expectEqualStrings(" ", sr); try testing.expectEqualStrings(" ", sl); } test "Padded composes inside a larger format string" { var aw: std.Io.Writer.Allocating = .init(testing.allocator); defer aw.deinit(); const left: Padded(Tag) = .{ .inner = .{ .label = "id" }, .width = 4, .alignment = .left }; const right: Padded(Tag) = .{ .inner = .{ .label = "42" }, .width = 4, .alignment = .right }; try aw.writer.print("[{f}|{f}]", .{ left, right }); const s = try aw.toOwnedSlice(); defer testing.allocator.free(s); try testing.expectEqualStrings("[id | 42]", s); } test "Padded counts bytes, not codepoints (multi-byte content overflows visually)" { // "é" is 2 bytes in UTF-8. With width=3 and 2-byte content, only // 1 byte of padding is added — correct for fixed-width terminal // output where the user supplies a column count, but worth pinning // so a future "fix" using grapheme width doesn't silently change // existing alignment. const padded: Padded(Tag) = .{ .inner = .{ .label = "é" }, .width = 3, .alignment = .right }; const s = try renderAlloc(padded); defer testing.allocator.free(s); try testing.expectEqual(@as(usize, 3), s.len); try testing.expectEqualStrings(" é", s); } test "Padded works with Date.padLeft / Date.padRight constructors" { const Date = @import("Date.zig"); const d = Date.fromYmd(2024, 1, 5); const right_s = try renderAlloc(d.padRight(12)); defer testing.allocator.free(right_s); try testing.expectEqualStrings(" 2024-01-05", right_s); const left_s = try renderAlloc(d.padLeft(12)); defer testing.allocator.free(left_s); try testing.expectEqualStrings("2024-01-05 ", left_s); } test "Padded works with Money.padLeft / Money.padRight constructors" { const Money = @import("Money.zig"); const m = Money.from(1234.5); const right_s = try renderAlloc(m.padRight(12)); defer testing.allocator.free(right_s); try testing.expectEqualStrings(" $1,234.50", right_s); const left_s = try renderAlloc(m.padLeft(12)); defer testing.allocator.free(left_s); try testing.expectEqualStrings("$1,234.50 ", left_s); } test "Padded composes with Money variants (whole / trim / signed)" { const Money = @import("Money.zig"); const whole = try renderAlloc(Money.from(1234.0).whole().padRight(10)); defer testing.allocator.free(whole); try testing.expectEqualStrings(" $1,234", whole); const trim = try renderAlloc(Money.from(1234.0).trim().padRight(10)); defer testing.allocator.free(trim); try testing.expectEqualStrings(" $1,234", trim); const signed_pos = try renderAlloc(Money.from(50.0).signed().padRight(10)); defer testing.allocator.free(signed_pos); try testing.expectEqualStrings(" +$50.00", signed_pos); }