Printing floats with runtime known precision

Hi.

It is quite common situation when a user of a program/system
may want to specify how many digits after decimal point should be seen.

In C this can be achieved by at least 3 ways:

#include <stdio.h>
#include <math.h>

void print_float_with_given_precision_v1(float x, int dp)
{
    char fmt[16];
    sprintf(fmt, "x = %%.%df\n", dp);
    printf(fmt, x);
}

void print_float_with_given_precision_v2(float x, int dp)
{
    printf("x = %.*f\n", dp, x);
}

void print_float_with_given_precision_v3(float x, int dp)
{
    printf("x = %1$.*2$f\n", x, dp);
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("usage: %s <number>\n", argv[0]);
        return 0;
    }

    int dp = 3;
    sscanf(argv[1], "%d", &dp); // ignore errors
    float x = M_PI;
    print_float_with_given_precision_v1(x, dp);
    print_float_with_given_precision_v2(x, dp);
    print_float_with_given_precision_v3(x, dp);
}
$ ./a.out 5
x = 3.14159
x = 3.14159
x = 3.14159

However, in Zig format string for std.debug.print() and alike must be compile time known.
Are there any ways to deal with runtime known precision (number of digits after decimal point) in ZIg?..

4 Likes

See std.fmt, there’s formatValue, formatFloat, etc… that take FormatOptions struct

pub const FormatOptions = struct {
    precision: ?usize = null,
    width: ?usize = null,
    alignment: Alignment = .right,
    fill: u8 = ' ',
};
3 Likes

Thanks, I will take a look at this.

Yes, that’s all right.

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    if (std.os.argv.len != 2) {
        try stdout.print("usage: {s} <precision>\n", .{std.os.argv[0]});
        return;
    }
    const p = std.fmt.parseInt(usize, std.mem.sliceTo(std.os.argv[1], 0), 10) catch 3;
    try std.fmt.formatFloatDecimal(std.math.pi, .{.precision = p}, stdout);
    try stdout.print("\n", .{});
}
$ ./dp 
usage: ./dp <precision>
$ ./dp 4
3.1416

In a more similar fashion to the C examples you provided, you can use a runtime value in the args tuple or struct to fill in parts of the format specifier. You put the index or field name in square brackets []. In this example, I use the second arg of the tuple (index 1) as the precision:

std.debug.print("{d:.[1]}\n", .{ 3.1415, 2 });
12 Likes

Oh, great, thanks! I did not know about this possibility.
Less words and no writers as arguments, good.

Both variants in one place, just to compare:

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    if (std.os.argv.len != 2) {
        try stdout.print("usage: {s} <precision>\n", .{std.os.argv[0]});
        return;
    }

    const arg1 = std.mem.sliceTo(std.os.argv[1], 0);
    const p = std.fmt.parseInt(usize, arg1, 10) catch 2;

    // variant I
    try stdout.print("π = ", .{});
    try std.fmt.formatFloatDecimal(std.math.pi, .{.precision = p}, stdout);
    try stdout.print("\n", .{});

    // variant II
    try stdout.print("π = {d:.[1]}\n", .{std.math.pi, p});
}
2 Likes

A third, a bit more explicit, variant:

// variant III
    try stdout.print("π = {[x]d:.[precision]}\n", .{.x = std.math.pi, .precision = p});
5 Likes

Also good. The score was even, 3:3 :slight_smile:

I’ve marked @dude_the_builder’s variant as solution,
because I like this variant more than the other two.
Anyway, thanks to @Cloudef and @slonik-az too.

1 Like