format: do not force user to provide an alignment field when it's not necessary (#19049)

* format: fix default character when no alignment

When no alignment is specified, the character that should be used is the
fill character that is otherwise provided, not space.

This is closer to the default that C programmers (and other languages)
use: "04x" fills with zeroes (in zig as of today x:04 fills with spaces)

Test:

    const std = @import("std");
    const expectFmt = std.testing.expectFmt;

    test "fmt.defaultchar.no-alignment" {

        // as of today the following test passes:
        try expectFmt("0x00ff", "0x{x:0>4}", .{255});

        // as of today the following test fails (returns "0x  ff" instead)
        try expectFmt("0x00ff", "0x{x:04}", .{255});
    }

* non breaking improvement of string formatting

* improved comment

* simplify the code a little

* small improvement around how  characters identified as valid are consumed
This commit is contained in:
eric-saintetienne 2024-07-17 20:02:10 +01:00 committed by GitHub
parent 9d9b5a11e8
commit c3faae6bf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -20,11 +20,14 @@ pub const Alignment = enum {
right, right,
}; };
const default_alignment = .right;
const default_fill_char = ' ';
pub const FormatOptions = struct { pub const FormatOptions = struct {
precision: ?usize = null, precision: ?usize = null,
width: ?usize = null, width: ?usize = null,
alignment: Alignment = .right, alignment: Alignment = default_alignment,
fill: u21 = ' ', fill: u21 = default_fill_char,
}; };
/// Renders fmt string with args, calling `writer` with slices of bytes. /// Renders fmt string with args, calling `writer` with slices of bytes.
@ -48,8 +51,8 @@ pub const FormatOptions = struct {
/// ///
/// Note that most of the parameters are optional and may be omitted. Also you can leave out separators like `:` and `.` when /// Note that most of the parameters are optional and may be omitted. Also you can leave out separators like `:` and `.` when
/// all parameters after the separator are omitted. /// all parameters after the separator are omitted.
/// Only exception is the *fill* parameter. If *fill* is required, one has to specify *alignment* as well, as otherwise /// Only exception is the *fill* parameter. If a non-zero *fill* character is required at the same time as *width* is specified,
/// the digits after `:` is interpreted as *width*, not *fill*. /// one has to specify *alignment* as well, as otherwise the digit following `:` is interpreted as *width*, not *fill*.
/// ///
/// The *specifier* has several options for types: /// The *specifier* has several options for types:
/// - `x` and `X`: output numeric value in hexadecimal notation /// - `x` and `X`: output numeric value in hexadecimal notation
@ -239,29 +242,38 @@ pub const Placeholder = struct {
} }
} }
// Parse the fill character // Parse the fill character, if present.
// The fill parameter requires the alignment parameter to be specified // When the width field is also specified, the fill character must
// too // be followed by an alignment specifier, unless it's '0' (zero)
const fill = comptime if (parser.peek(1)) |ch| // (in which case it's handled as part of the width specifier)
var fill: ?u21 = comptime if (parser.peek(1)) |ch|
switch (ch) { switch (ch) {
'<', '^', '>' => parser.char().?, '<', '^', '>' => parser.char(),
else => ' ', else => null,
} }
else else
' '; null;
// Parse the alignment parameter // Parse the alignment parameter
const alignment: Alignment = comptime if (parser.peek(0)) |ch| init: { const alignment: ?Alignment = comptime if (parser.peek(0)) |ch| init: {
switch (ch) { switch (ch) {
'<', '^', '>' => _ = parser.char(), '<', '^', '>' => {
else => {}, // consume the character
break :init switch (parser.char().?) {
'<' => .left,
'^' => .center,
else => .right,
};
},
else => break :init null,
} }
break :init switch (ch) { } else null;
'<' => .left,
'^' => .center, // When none of the fill character and the alignment specifier have
else => .right, // been provided, check whether the width starts with a zero.
}; if (fill == null and alignment == null) {
} else .right; fill = comptime if (parser.peek(0) == '0') '0' else null;
}
// Parse the width parameter // Parse the width parameter
const width = comptime parser.specifier() catch |err| const width = comptime parser.specifier() catch |err|
@ -284,8 +296,8 @@ pub const Placeholder = struct {
return Placeholder{ return Placeholder{
.specifier_arg = cacheString(specifier_arg[0..specifier_arg.len].*), .specifier_arg = cacheString(specifier_arg[0..specifier_arg.len].*),
.fill = fill, .fill = fill orelse default_fill_char,
.alignment = alignment, .alignment = alignment orelse default_alignment,
.arg = arg, .arg = arg,
.width = width, .width = width,
.precision = precision, .precision = precision,
@ -2648,6 +2660,15 @@ test "sci float padding" {
try expectFmt("right-pad: 3.142e0****\n", "right-pad: {e:*<11.3}\n", .{number}); try expectFmt("right-pad: 3.142e0****\n", "right-pad: {e:*<11.3}\n", .{number});
} }
test "padding.zero" {
try expectFmt("zero-pad: '0042'", "zero-pad: '{:04}'", .{42});
try expectFmt("std-pad: ' 42'", "std-pad: '{:10}'", .{42});
try expectFmt("std-pad-1: '001'", "std-pad-1: '{:0>3}'", .{1});
try expectFmt("std-pad-2: '911'", "std-pad-2: '{:1<03}'", .{9});
try expectFmt("std-pad-3: ' 1'", "std-pad-3: '{:>03}'", .{1});
try expectFmt("center-pad: '515'", "center-pad: '{:5^03}'", .{1});
}
test "null" { test "null" {
const inst = null; const inst = null;
try expectFmt("null", "{}", .{inst}); try expectFmt("null", "{}", .{inst});