std: Respect user-specified alignment when formatting ints

This implementation tries to do the right thing (TM) by treating the
sign as part of the number itself, therefore the alignment parameter
applies to both the sign and the digits.

In other words the format string `{:>4}` with -1 as input will not
output `-  1` but `  -1`.

And let's default to right alignment for everything as that's what users
want, especially when printing numbers. Many implementations use
different defaults for numeric vs non-numeric types, let's strive for a
consistent behaviour here.
This commit is contained in:
LemonBoy 2020-09-17 00:41:26 +02:00
parent bb9a4ad6e9
commit 27adb82fda

View File

@ -22,7 +22,7 @@ pub const Alignment = enum {
pub const FormatOptions = struct { pub const FormatOptions = struct {
precision: ?usize = null, precision: ?usize = null,
width: ?usize = null, width: ?usize = null,
alignment: Alignment = .Left, alignment: Alignment = .Right,
fill: u8 = ' ', fill: u8 = ' ',
}; };
@ -631,26 +631,22 @@ pub fn formatBuf(
writer: anytype, writer: anytype,
) !void { ) !void {
const width = options.width orelse buf.len; const width = options.width orelse buf.len;
var padding = if (width > buf.len) (width - buf.len) else 0; const padding = if (width > buf.len) (width - buf.len) else 0;
const pad_byte = [1]u8{options.fill};
switch (options.alignment) { switch (options.alignment) {
.Left => { .Left => {
try writer.writeAll(buf); try writer.writeAll(buf);
while (padding > 0) : (padding -= 1) { try writer.writeByteNTimes(options.fill, padding);
try writer.writeAll(&pad_byte);
}
}, },
.Center => { .Center => {
const padl = padding / 2; const left_padding = padding / 2;
var i: usize = 0; const right_padding = (padding + 1) / 2;
while (i < padl) : (i += 1) try writer.writeAll(&pad_byte); try writer.writeByteNTimes(options.fill, left_padding);
try writer.writeAll(buf); try writer.writeAll(buf);
while (i < padding) : (i += 1) try writer.writeAll(&pad_byte); try writer.writeByteNTimes(options.fill, right_padding);
}, },
.Right => { .Right => {
while (padding > 0) : (padding -= 1) { try writer.writeByteNTimes(options.fill, padding);
try writer.writeAll(&pad_byte);
}
try writer.writeAll(buf); try writer.writeAll(buf);
}, },
} }
@ -941,61 +937,27 @@ pub fn formatInt(
options: FormatOptions, options: FormatOptions,
writer: anytype, writer: anytype,
) !void { ) !void {
assert(base >= 2);
const int_value = if (@TypeOf(value) == comptime_int) blk: { const int_value = if (@TypeOf(value) == comptime_int) blk: {
const Int = math.IntFittingRange(value, value); const Int = math.IntFittingRange(value, value);
break :blk @as(Int, value); break :blk @as(Int, value);
} else } else
value; value;
if (@typeInfo(@TypeOf(int_value)).Int.is_signed) { const value_info = @typeInfo(@TypeOf(int_value)).Int;
return formatIntSigned(int_value, base, uppercase, options, writer);
} else {
return formatIntUnsigned(int_value, base, uppercase, options, writer);
}
}
fn formatIntSigned( // The type must have the same size as `base` or be wider in order for the
value: anytype, // division to work
base: u8, const min_int_bits = comptime math.max(value_info.bits, 8);
uppercase: bool, const MinInt = std.meta.Int(false, min_int_bits);
options: FormatOptions,
writer: anytype,
) !void {
const new_options = FormatOptions{
.width = if (options.width) |w| (if (w == 0) 0 else w - 1) else null,
.precision = options.precision,
.fill = options.fill,
};
const bit_count = @typeInfo(@TypeOf(value)).Int.bits;
const Uint = std.meta.Int(false, bit_count);
if (value < 0) {
try writer.writeAll("-");
const new_value = math.absCast(value);
return formatIntUnsigned(new_value, base, uppercase, new_options, writer);
} else if (options.width == null or options.width.? == 0) {
return formatIntUnsigned(@intCast(Uint, value), base, uppercase, options, writer);
} else {
try writer.writeAll("+");
const new_value = @intCast(Uint, value);
return formatIntUnsigned(new_value, base, uppercase, new_options, writer);
}
}
fn formatIntUnsigned( const abs_value = math.absCast(int_value);
value: anytype, // The worst case in terms of space needed is base 2, plus 1 for the sign
base: u8, var buf: [1 + math.max(value_info.bits, 1)]u8 = undefined;
uppercase: bool,
options: FormatOptions, var a: MinInt = abs_value;
writer: anytype,
) !void {
assert(base >= 2);
const value_info = @typeInfo(@TypeOf(value)).Int;
var buf: [math.max(value_info.bits, 1)]u8 = undefined;
const min_int_bits = comptime math.max(value_info.bits, @typeInfo(@TypeOf(base)).Int.bits);
const MinInt = std.meta.Int(value_info.is_signed, min_int_bits);
var a: MinInt = value;
var index: usize = buf.len; var index: usize = buf.len;
while (true) { while (true) {
const digit = a % base; const digit = a % base;
index -= 1; index -= 1;
@ -1004,25 +966,21 @@ fn formatIntUnsigned(
if (a == 0) break; if (a == 0) break;
} }
const digits_buf = buf[index..]; if (value_info.is_signed) {
const width = options.width orelse 0; if (value < 0) {
const padding = if (width > digits_buf.len) (width - digits_buf.len) else 0; // Negative integer
index -= 1;
if (padding > index) { buf[index] = '-';
const zero_byte: u8 = options.fill; } else if (options.width == null or options.width.? == 0) {
var leftover_padding = padding - index; // Positive integer, omit the plus sign
while (true) {
try writer.writeAll(@as(*const [1]u8, &zero_byte)[0..]);
leftover_padding -= 1;
if (leftover_padding == 0) break;
}
mem.set(u8, buf[0..index], options.fill);
return writer.writeAll(&buf);
} else { } else {
const padded_buf = buf[index - padding ..]; // Positive integer
mem.set(u8, padded_buf[0..padding], options.fill); index -= 1;
return writer.writeAll(padded_buf); buf[index] = '+';
} }
}
return formatBuf(buf[index..], options, writer);
} }
pub fn formatIntBuf(out_buf: []u8, value: anytype, base: u8, uppercase: bool, options: FormatOptions) usize { pub fn formatIntBuf(out_buf: []u8, value: anytype, base: u8, uppercase: bool, options: FormatOptions) usize {
@ -1287,7 +1245,17 @@ test "int.specifier" {
test "int.padded" { test "int.padded" {
try testFmt("u8: ' 1'", "u8: '{:4}'", .{@as(u8, 1)}); try testFmt("u8: ' 1'", "u8: '{:4}'", .{@as(u8, 1)});
try testFmt("u8: 'xxx1'", "u8: '{:x<4}'", .{@as(u8, 1)}); try testFmt("u8: '1000'", "u8: '{:0<4}'", .{@as(u8, 1)});
try testFmt("u8: '0001'", "u8: '{:0>4}'", .{@as(u8, 1)});
try testFmt("u8: '0100'", "u8: '{:0^4}'", .{@as(u8, 1)});
try testFmt("i8: '-1 '", "i8: '{:<4}'", .{@as(i8, -1)});
try testFmt("i8: ' -1'", "i8: '{:>4}'", .{@as(i8, -1)});
try testFmt("i8: ' -1 '", "i8: '{:^4}'", .{@as(i8, -1)});
try testFmt("i16: '-1234'", "i16: '{:4}'", .{@as(i16, -1234)});
try testFmt("i16: '+1234'", "i16: '{:4}'", .{@as(i16, 1234)});
try testFmt("i16: '-12345'", "i16: '{:4}'", .{@as(i16, -12345)});
try testFmt("i16: '+12345'", "i16: '{:4}'", .{@as(i16, 12345)});
try testFmt("u16: '12345'", "u16: '{:4}'", .{@as(u16, 12345)});
} }
test "buffer" { test "buffer" {
@ -1333,7 +1301,7 @@ test "slice" {
try testFmt("slice: []const u8@deadbeef\n", "slice: {}\n", .{value}); try testFmt("slice: []const u8@deadbeef\n", "slice: {}\n", .{value});
} }
try testFmt("buf: Test \n", "buf: {s:5}\n", .{"Test"}); try testFmt("buf: Test\n", "buf: {s:5}\n", .{"Test"});
try testFmt("buf: Test\n Other text", "buf: {s}\n Other text", .{"Test"}); try testFmt("buf: Test\n Other text", "buf: {s}\n Other text", .{"Test"});
} }
@ -1366,7 +1334,7 @@ test "cstr" {
.{@ptrCast([*c]const u8, "Test C")}, .{@ptrCast([*c]const u8, "Test C")},
); );
try testFmt( try testFmt(
"cstr: Test C \n", "cstr: Test C\n",
"cstr: {s:10}\n", "cstr: {s:10}\n",
.{@ptrCast([*c]const u8, "Test C")}, .{@ptrCast([*c]const u8, "Test C")},
); );
@ -1809,7 +1777,7 @@ test "vector" {
try testFmt("{ true, false, true, false }", "{}", .{vbool}); try testFmt("{ true, false, true, false }", "{}", .{vbool});
try testFmt("{ -2, -1, 0, 1 }", "{}", .{vi64}); try testFmt("{ -2, -1, 0, 1 }", "{}", .{vi64});
try testFmt("{ - 2, - 1, + 0, + 1 }", "{d:5}", .{vi64}); try testFmt("{ -2, -1, +0, +1 }", "{d:5}", .{vi64});
try testFmt("{ 1000, 2000, 3000, 4000 }", "{}", .{vu64}); try testFmt("{ 1000, 2000, 3000, 4000 }", "{}", .{vu64});
try testFmt("{ 3e8, 7d0, bb8, fa0 }", "{x}", .{vu64}); try testFmt("{ 3e8, 7d0, bb8, fa0 }", "{x}", .{vu64});
try testFmt("{ 1kB, 2kB, 3kB, 4kB }", "{B}", .{vu64}); try testFmt("{ 1kB, 2kB, 3kB, 4kB }", "{B}", .{vu64});
@ -1822,15 +1790,16 @@ test "enum-literal" {
test "padding" { test "padding" {
try testFmt("Simple", "{}", .{"Simple"}); try testFmt("Simple", "{}", .{"Simple"});
try testFmt("true ", "{:10}", .{true}); try testFmt(" true", "{:10}", .{true});
try testFmt(" true", "{:>10}", .{true}); try testFmt(" true", "{:>10}", .{true});
try testFmt("======true", "{:=>10}", .{true}); try testFmt("======true", "{:=>10}", .{true});
try testFmt("true======", "{:=<10}", .{true}); try testFmt("true======", "{:=<10}", .{true});
try testFmt(" true ", "{:^10}", .{true}); try testFmt(" true ", "{:^10}", .{true});
try testFmt("===true===", "{:=^10}", .{true}); try testFmt("===true===", "{:=^10}", .{true});
try testFmt("Minimum width", "{:18} width", .{"Minimum"}); try testFmt(" Minimum width", "{:18} width", .{"Minimum"});
try testFmt("==================Filled", "{:=>24}", .{"Filled"}); try testFmt("==================Filled", "{:=>24}", .{"Filled"});
try testFmt(" Centered ", "{:^24}", .{"Centered"}); try testFmt(" Centered ", "{:^24}", .{"Centered"});
try testFmt("-", "{:-^1}", .{""});
} }
test "decimal float padding" { test "decimal float padding" {