Dynamic string formatting?

According to https://zig.guide/standard-library/advanced-formatting/ strings can be formatted for example like this:

bufPrint(&b, "{s:_^6}", .{"hi!"})

However, this seems to require that the _ and 6 are constants.

Is there a way to change the fill character and the length dynamically? Something like in sprintf in C?

const std = @import("std");

fn print_str(str: []const u8, fill: u21, len: usize) !void {
    try std.fmt.formatText(str, "s", std.fmt.FormatOptions{
        .alignment = .center,
        .fill = fill,
        .width = len,
    }, std.io.getStdErr().writer());
}

pub fn main() !void {
    try print_str("hi!", '_', 6);
}

Something like this? It’s a little inconvenient but I see no other solution.

3 Likes

argument (position), width, and precision can be specified at runtime as named format arguments.
The fill character is specified only at comptime as part of the format string.

Example:

const std = @import("std");

pub fn main() void {
    std.debug.print("{[value]s:_^[width]}\n", .{
        .value = "hi",
        .width = 10,
    });
}

prints: ____hi____

5 Likes

btw, in my opinion the documentation is not very clear about this feature (named format arguments not only for [argument])

2 Likes

It is not documented at all. It is discovered by digging in the code :slight_smile:

2 Likes

This is pretty cool hack!

However, how can I change the _? what if I wanted to use a fill with character that the user can choose?

It is not possible with named arguments, due to limitation in fmt.Placeholder.parse, I believe. It could be implemented, but will require parser fallback due to the optionality of [fill] and [alignment]. I see it as too complex to bother with, but maybe you should file an issue (or find one that explains why it’s like this).

I feel I’m so close, but my limited understanding of zig is still stopping me.

I loved the one line solution by dimdin. I could modify it to write into a buffer string:

_ = try std.fmt.bufPrint(&buf1, "{[left]s}{[title]s:_^[width]}{[right]s}\n", .{.left=[2]u8{s1,0}, .title=s, .width=c, .right=[2]u8{s0, 0},});

But as it turns out I cannot make the fill character dynamic.

Your solution, however, works with dynamic fill character. But I don’t understand how I can convert the print_str to print_buf so that I can store the formatted string. Because I am looking for a way to implement sprintf, not just printf.

Can you point me to the correct direction? :smiley:

convert the print_str to print_buf so that I can store the formatted string.

Adapt print_str to something like (std.fmt.bufPrint does almost the same):

const std = @import("std");
const fmt = std.fmt;
const io = std.io;

fn print_str(str: []const u8, fill: u21, len: usize, writer: anytype) !void {
    try fmt.formatText(str, "s", fmt.FormatOptions{
        .alignment = .center,
        .fill = fill,
        .width = len,
    }, writer);
}

fn print_str_buf(buf: []u8, str: []const u8, fill: u21, len: usize) ![]u8 {
    var stream = io.fixedBufferStream(buf);
    try print_str(str, fill, len, stream.writer());
    return stream.getWritten();
}

pub fn main() !void {
    var buf: [6]u8 = undefined;
    const str = try print_str_buf(&buf, "hi!", '_', buf.len);
    std.debug.print("{s}\n", .{str});
}
1 Like

Thanks a million, this works!