Sorry for the noob question, I’m sure most people have already done this and figured it out, but I’ve always let libraries do it for me until now. I’m quickly realizing why Zig doesn’t have a native string type ![]()
I’m porting a CLI project over from Go, and I need a really simple table:
┌──────────────────────────────┬──────┐
│ DIRECTORY │ PATH │
├──────────────────────────────┼──────┤
│ /home/username/Desktop/envr/ │ .env │
└──────────────────────────────┴──────┘
I was able to get this working, but the code I wrote feels…far from correct. The function is called structs, it accepts a list of structs, a writer, and optionally a subset of struct fields to print (by default it prints all of them:
test "can print a table with limited columns" {
const gpa = std.testing.allocator;
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
const F = struct { foo: []const u8, bar: []const u8 };
const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }};
// I realize @constCast is a code smell, I'll fix it later
try structs(F, @constCast(&rows), .{ .out = &out.writer, .fields = .initOne(.foo) });
const got = try out.toOwnedSlice();
defer gpa.free(got);
try std.testing.expectEqualStrings(
\\┌─────┐
\\│ foo │
\\├─────┤
\\│ bat │
\\└─────┘
\\
, got);
}
I basically just defer to writer for everything, and while it works, code like this feels inelegant, especially when called inside of an inline for loop:
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write("─");
}
I would like to try and do this without allocations, but have found that to pretty difficult (if I deviate from my working code). Even if I assign a line buffer, and break every column up into slices perfectly, the characters I’ve used to draw the lines take up more than one byte, making the math awkward.
Any advice on how the zig zen way to do this would be appreciated ![]()
- Only one obvious way to do things.
Here is the full code
tabula.zig
const std = @import("std");
// Could be an enum? or tagged union.
const hor = "─"; // Horizontal
const tl = "┌"; // Top Left
const tm = "┬"; // Top Middle
const tr = "┐"; // Top Right
const sep = "│"; // Separator
const ml = "├"; // Middle Left
const mm = "┼"; // Middle Middle (Center)
const mr = "┤"; // Middle Right
const bl = "└"; // Bottom Left
const bm = "┴"; // Bottom Middle
const br = "┘"; // Bottom Right
/// Print a list of structs as a table to opts.out.
pub fn structs(
comptime T: type,
items: []T,
opts: struct {
out: *std.Io.Writer, // TODO: Default to stdout.
fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
},
) !void {
const writer = opts.out;
const max_column_widths = determine_col_widths(T, items);
try header(T, opts.fields, &max_column_widths, opts.out);
// Print body
for (items) |item| {
_ = try writer.write(sep);
// TODO: Always loops through every field
const all_fields = @typeInfo(T).@"struct".fields;
inline for (all_fields) |field| {
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |c| : (i += 1) {
if (std.mem.eql(u8, @tagName(c), field.name)) {
_ = try writer.write(" ");
try write_aligned(writer, @field(item, field.name), max_column_widths[i], .left);
try writer.print(" {s}", .{sep});
break;
}
}
}
_ = try writer.write("\n");
}
// Print post-body
{
_ = try writer.write(bl);
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(bm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(br);
_ = try writer.write("\n");
}
}
fn determine_col_widths(
T: type,
items: []T,
) [@typeInfo(T).@"struct".fields.len]usize {
const all_fields = @typeInfo(T).@"struct".fields;
var max_column_widths: [all_fields.len]usize = @splat(0);
for (items) |item| {
inline for (all_fields, 0..) |field, i| {
// TODO: Get str len of item
const value_len = @field(item, field.name).len;
max_column_widths[i] = @max(
max_column_widths[i],
field.name.len,
value_len,
);
}
}
return max_column_widths;
}
// Print the header of a table
fn header(
T: type,
fields: std.EnumSet(std.meta.FieldEnum(T)),
max_column_widths: []const usize,
writer: *std.Io.Writer,
) !void {
// Print Pre-Header
{
_ = try writer.write(tl);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(tm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(tr ++ "\n");
}
// Main Header
{
_ = try writer.write(sep);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |field| : (i += 1) {
_ = try writer.write(" ");
try write_aligned(
writer,
@tagName(field),
max_column_widths[i],
.center,
);
try writer.print(" {s}", .{sep});
}
try writer.print("\n", .{});
}
// Print post-header
{
_ = try writer.write(ml);
var itr = fields.iterator();
var i: usize = 0;
while (itr.next()) |_| : (i += 1) {
if (i > 0) {
_ = try writer.write(mm);
}
const padding = max_column_widths[i] + 2;
for (0..padding) |_| {
_ = try writer.write(hor);
}
}
_ = try writer.write(mr ++ "\n");
}
}
fn write_aligned(
writer: *std.Io.Writer,
data: []const u8,
max_width: usize,
alignment: Alignment,
) !void {
const padding: [2]usize = switch (alignment) {
.left => .{ 0, max_width - data.len },
.right => .{ max_width - data.len, 0 },
.center => blk: {
// Faster to inline the divFloor?
const half = @divFloor(max_width - data.len, 2);
break :blk .{ half, max_width - data.len - half };
},
};
for (0..padding[0]) |_| {
_ = try writer.write(" ");
}
_ = try writer.write(data);
for (0..padding[1]) |_| {
_ = try writer.write(" ");
}
}
const Alignment = enum { left, center, right };
I did manage to rewrite write_aligned in a form that didn’t require a writer anymore
fn align_text(data: []const u8, buf: []u8, alignment: Alignment) void {
const dest = switch (alignment) {
.left => buf[0..data.len],
.right => buf[buf.len - data.len .. buf.len],
.center => blk: {
const start = (buf.len - data.len) / 2;
break :blk buf[start .. start + data.len];
},
};
@memcpy(dest, data);
}
But this necessitated adding a (fixed) buffer, which while probably fine for my small tables, could potentially run out with long path names.
pub fn structs(
comptime T: type,
items: []T,
opts: struct {
out: *std.Io.Writer, // TODO: Default stdout.
fields: std.EnumSet(std.meta.FieldEnum(T)) = .full,
},
) error{WriteFailed}!void {
const writer = opts.out;
const max_column_widths = determine_col_widths(T, items);
var buf: [255]u8 = undefined;
// Print body
for (items) |item| {
_ = try writer.write(sep);
const all_fields = @typeInfo(T).@"struct".fields;
inline for (all_fields) |field| {
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |c| : (i += 1) {
if (std.mem.eql(u8, @tagName(c), field.name)) {
const col = buf[0..max_column_widths[i]];
@memset(col, ' ');
align_text(@field(item, field.name), col, .left);
try writer.print(" {s} {s}", .{ col, sep });
break;
}
}
}
_ = try writer.write("\n");
}
// ...
}
If there’s a way to loop through just the selected fields at comptime rather than having to loop + check all of them, that would also be great.
// TODO: Always loops through every field
const all_fields = @typeInfo(T).@"struct".fields;
inline for (all_fields) |field| {
var itr = opts.fields.iterator();
var i: usize = 0;
while (itr.next()) |c| : (i += 1) {
if (std.mem.eql(u8, @tagName(c), field.name)) {
_ = try writer.write(" ");
try write_aligned(
writer,
@field(item, field.name),
max_column_widths[i],
.left,
);
try writer.print(" {s}", .{sep});
break;
}
}