authorgravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-09-17 00:41:26+02:00
committergravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-09-17 00:53:08+02:00
log27adb82fda516e95c3c03df80a95b895969fdd56
treed906389a448b260b4c64bf101fab822944ed4105
parentbb9a4ad6e903ad2f9c137bca56bdf2dd6fc65d03

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.

1 files changed, 51 insertions(+), 82 deletions(-)

lib/std/fmt.zig+51-82
...@@ -22,7 +22,7 @@ pub const Alignment = enum {...@@ -22,7 +22,7 @@ pub const Alignment = enum {
22pub const FormatOptions = struct {22pub const FormatOptions = struct {
23 precision: ?usize = null,23 precision: ?usize = null,
24 width: ?usize = null,24 width: ?usize = null,
25 alignment: Alignment = .Left,25 alignment: Alignment = .Right,
26 fill: u8 = ' ',26 fill: u8 = ' ',
27};27};
2828
...@@ -631,26 +631,22 @@ pub fn formatBuf(...@@ -631,26 +631,22 @@ pub fn formatBuf(
631 writer: anytype,631 writer: anytype,
632) !void {632) !void {
633 const width = options.width orelse buf.len;633 const width = options.width orelse buf.len;
634 var padding = if (width > buf.len) (width - buf.len) else 0;634 const padding = if (width > buf.len) (width - buf.len) else 0;
635 const pad_byte = [1]u8{options.fill};635
636 switch (options.alignment) {636 switch (options.alignment) {
637 .Left => {637 .Left => {
638 try writer.writeAll(buf);638 try writer.writeAll(buf);
639 while (padding > 0) : (padding -= 1) {639 try writer.writeByteNTimes(options.fill, padding);
640 try writer.writeAll(&pad_byte);
641 }
642 },640 },
643 .Center => {641 .Center => {
644 const padl = padding / 2;642 const left_padding = padding / 2;
645 var i: usize = 0;643 const right_padding = (padding + 1) / 2;
646 while (i < padl) : (i += 1) try writer.writeAll(&pad_byte);644 try writer.writeByteNTimes(options.fill, left_padding);
647 try writer.writeAll(buf);645 try writer.writeAll(buf);
648 while (i < padding) : (i += 1) try writer.writeAll(&pad_byte);646 try writer.writeByteNTimes(options.fill, right_padding);
649 },647 },
650 .Right => {648 .Right => {
651 while (padding > 0) : (padding -= 1) {649 try writer.writeByteNTimes(options.fill, padding);
652 try writer.writeAll(&pad_byte);
653 }
654 try writer.writeAll(buf);650 try writer.writeAll(buf);
655 },651 },
656 }652 }
...@@ -941,61 +937,27 @@ pub fn formatInt(...@@ -941,61 +937,27 @@ pub fn formatInt(
941 options: FormatOptions,937 options: FormatOptions,
942 writer: anytype,938 writer: anytype,
943) !void {939) !void {
940 assert(base >= 2);
941
944 const int_value = if (@TypeOf(value) == comptime_int) blk: {942 const int_value = if (@TypeOf(value) == comptime_int) blk: {
945 const Int = math.IntFittingRange(value, value);943 const Int = math.IntFittingRange(value, value);
946 break :blk @as(Int, value);944 break :blk @as(Int, value);
947 } else945 } else
948 value;946 value;
949947
950 if (@typeInfo(@TypeOf(int_value)).Int.is_signed) {948 const value_info = @typeInfo(@TypeOf(int_value)).Int;
951 return formatIntSigned(int_value, base, uppercase, options, writer);
952 } else {
953 return formatIntUnsigned(int_value, base, uppercase, options, writer);
954 }
955}
956949
957fn formatIntSigned(950 // The type must have the same size as `base` or be wider in order for the
958 value: anytype,951 // division to work
959 base: u8,952 const min_int_bits = comptime math.max(value_info.bits, 8);
960 uppercase: bool,953 const MinInt = std.meta.Int(false, min_int_bits);
961 options: FormatOptions,
962 writer: anytype,
963) !void {
964 const new_options = FormatOptions{
965 .width = if (options.width) |w| (if (w == 0) 0 else w - 1) else null,
966 .precision = options.precision,
967 .fill = options.fill,
968 };
969 const bit_count = @typeInfo(@TypeOf(value)).Int.bits;
970 const Uint = std.meta.Int(false, bit_count);
971 if (value < 0) {
972 try writer.writeAll("-");
973 const new_value = math.absCast(value);
974 return formatIntUnsigned(new_value, base, uppercase, new_options, writer);
975 } else if (options.width == null or options.width.? == 0) {
976 return formatIntUnsigned(@intCast(Uint, value), base, uppercase, options, writer);
977 } else {
978 try writer.writeAll("+");
979 const new_value = @intCast(Uint, value);
980 return formatIntUnsigned(new_value, base, uppercase, new_options, writer);
981 }
982}
983954
984fn formatIntUnsigned(955 const abs_value = math.absCast(int_value);
985 value: anytype,956 // The worst case in terms of space needed is base 2, plus 1 for the sign
986 base: u8,957 var buf: [1 + math.max(value_info.bits, 1)]u8 = undefined;
987 uppercase: bool,
988 options: FormatOptions,
989 writer: anytype,
990) !void {
991 assert(base >= 2);
992 const value_info = @typeInfo(@TypeOf(value)).Int;
993 var buf: [math.max(value_info.bits, 1)]u8 = undefined;
994 const min_int_bits = comptime math.max(value_info.bits, @typeInfo(@TypeOf(base)).Int.bits);
995 const MinInt = std.meta.Int(value_info.is_signed, min_int_bits);
996 var a: MinInt = value;
997 var index: usize = buf.len;
998958
959 var a: MinInt = abs_value;
960 var index: usize = buf.len;
999 while (true) {961 while (true) {
1000 const digit = a % base;962 const digit = a % base;
1001 index -= 1;963 index -= 1;
...@@ -1004,25 +966,21 @@ fn formatIntUnsigned(...@@ -1004,25 +966,21 @@ fn formatIntUnsigned(
1004 if (a == 0) break;966 if (a == 0) break;
1005 }967 }
1006968
1007 const digits_buf = buf[index..];969 if (value_info.is_signed) {
1008 const width = options.width orelse 0;970 if (value < 0) {
1009 const padding = if (width > digits_buf.len) (width - digits_buf.len) else 0;971 // Negative integer
1010972 index -= 1;
1011 if (padding > index) {973 buf[index] = '-';
1012 const zero_byte: u8 = options.fill;974 } else if (options.width == null or options.width.? == 0) {
1013 var leftover_padding = padding - index;975 // Positive integer, omit the plus sign
1014 while (true) {976 } else {
1015 try writer.writeAll(@as(*const [1]u8, &zero_byte)[0..]);977 // Positive integer
1016 leftover_padding -= 1;978 index -= 1;
1017 if (leftover_padding == 0) break;979 buf[index] = '+';
1018 }980 }
1019 mem.set(u8, buf[0..index], options.fill);
1020 return writer.writeAll(&buf);
1021 } else {
1022 const padded_buf = buf[index - padding ..];
1023 mem.set(u8, padded_buf[0..padding], options.fill);
1024 return writer.writeAll(padded_buf);
1025 }981 }
982
983 return formatBuf(buf[index..], options, writer);
1026}984}
1027985
1028pub fn formatIntBuf(out_buf: []u8, value: anytype, base: u8, uppercase: bool, options: FormatOptions) usize {986pub fn formatIntBuf(out_buf: []u8, value: anytype, base: u8, uppercase: bool, options: FormatOptions) usize {
...@@ -1287,7 +1245,17 @@ test "int.specifier" {...@@ -1287,7 +1245,17 @@ test "int.specifier" {
12871245
1288test "int.padded" {1246test "int.padded" {
1289 try testFmt("u8: ' 1'", "u8: '{:4}'", .{@as(u8, 1)});1247 try testFmt("u8: ' 1'", "u8: '{:4}'", .{@as(u8, 1)});
1290 try testFmt("u8: 'xxx1'", "u8: '{:x<4}'", .{@as(u8, 1)});1248 try testFmt("u8: '1000'", "u8: '{:0<4}'", .{@as(u8, 1)});
1249 try testFmt("u8: '0001'", "u8: '{:0>4}'", .{@as(u8, 1)});
1250 try testFmt("u8: '0100'", "u8: '{:0^4}'", .{@as(u8, 1)});
1251 try testFmt("i8: '-1 '", "i8: '{:<4}'", .{@as(i8, -1)});
1252 try testFmt("i8: ' -1'", "i8: '{:>4}'", .{@as(i8, -1)});
1253 try testFmt("i8: ' -1 '", "i8: '{:^4}'", .{@as(i8, -1)});
1254 try testFmt("i16: '-1234'", "i16: '{:4}'", .{@as(i16, -1234)});
1255 try testFmt("i16: '+1234'", "i16: '{:4}'", .{@as(i16, 1234)});
1256 try testFmt("i16: '-12345'", "i16: '{:4}'", .{@as(i16, -12345)});
1257 try testFmt("i16: '+12345'", "i16: '{:4}'", .{@as(i16, 12345)});
1258 try testFmt("u16: '12345'", "u16: '{:4}'", .{@as(u16, 12345)});
1291}1259}
12921260
1293test "buffer" {1261test "buffer" {
...@@ -1333,7 +1301,7 @@ test "slice" {...@@ -1333,7 +1301,7 @@ test "slice" {
1333 try testFmt("slice: []const u8@deadbeef\n", "slice: {}\n", .{value});1301 try testFmt("slice: []const u8@deadbeef\n", "slice: {}\n", .{value});
1334 }1302 }
13351303
1336 try testFmt("buf: Test \n", "buf: {s:5}\n", .{"Test"});1304 try testFmt("buf: Test\n", "buf: {s:5}\n", .{"Test"});
1337 try testFmt("buf: Test\n Other text", "buf: {s}\n Other text", .{"Test"});1305 try testFmt("buf: Test\n Other text", "buf: {s}\n Other text", .{"Test"});
1338}1306}
13391307
...@@ -1366,7 +1334,7 @@ test "cstr" {...@@ -1366,7 +1334,7 @@ test "cstr" {
1366 .{@ptrCast([*c]const u8, "Test C")},1334 .{@ptrCast([*c]const u8, "Test C")},
1367 );1335 );
1368 try testFmt(1336 try testFmt(
1369 "cstr: Test C \n",1337 "cstr: Test C\n",
1370 "cstr: {s:10}\n",1338 "cstr: {s:10}\n",
1371 .{@ptrCast([*c]const u8, "Test C")},1339 .{@ptrCast([*c]const u8, "Test C")},
1372 );1340 );
...@@ -1809,7 +1777,7 @@ test "vector" {...@@ -1809,7 +1777,7 @@ test "vector" {
18091777
1810 try testFmt("{ true, false, true, false }", "{}", .{vbool});1778 try testFmt("{ true, false, true, false }", "{}", .{vbool});
1811 try testFmt("{ -2, -1, 0, 1 }", "{}", .{vi64});1779 try testFmt("{ -2, -1, 0, 1 }", "{}", .{vi64});
1812 try testFmt("{ - 2, - 1, + 0, + 1 }", "{d:5}", .{vi64});1780 try testFmt("{ -2, -1, +0, +1 }", "{d:5}", .{vi64});
1813 try testFmt("{ 1000, 2000, 3000, 4000 }", "{}", .{vu64});1781 try testFmt("{ 1000, 2000, 3000, 4000 }", "{}", .{vu64});
1814 try testFmt("{ 3e8, 7d0, bb8, fa0 }", "{x}", .{vu64});1782 try testFmt("{ 3e8, 7d0, bb8, fa0 }", "{x}", .{vu64});
1815 try testFmt("{ 1kB, 2kB, 3kB, 4kB }", "{B}", .{vu64});1783 try testFmt("{ 1kB, 2kB, 3kB, 4kB }", "{B}", .{vu64});
...@@ -1822,15 +1790,16 @@ test "enum-literal" {...@@ -1822,15 +1790,16 @@ test "enum-literal" {
18221790
1823test "padding" {1791test "padding" {
1824 try testFmt("Simple", "{}", .{"Simple"});1792 try testFmt("Simple", "{}", .{"Simple"});
1825 try testFmt("true ", "{:10}", .{true});1793 try testFmt(" true", "{:10}", .{true});
1826 try testFmt(" true", "{:>10}", .{true});1794 try testFmt(" true", "{:>10}", .{true});
1827 try testFmt("======true", "{:=>10}", .{true});1795 try testFmt("======true", "{:=>10}", .{true});
1828 try testFmt("true======", "{:=<10}", .{true});1796 try testFmt("true======", "{:=<10}", .{true});
1829 try testFmt(" true ", "{:^10}", .{true});1797 try testFmt(" true ", "{:^10}", .{true});
1830 try testFmt("===true===", "{:=^10}", .{true});1798 try testFmt("===true===", "{:=^10}", .{true});
1831 try testFmt("Minimum width", "{:18} width", .{"Minimum"});1799 try testFmt(" Minimum width", "{:18} width", .{"Minimum"});
1832 try testFmt("==================Filled", "{:=>24}", .{"Filled"});1800 try testFmt("==================Filled", "{:=>24}", .{"Filled"});
1833 try testFmt(" Centered ", "{:^24}", .{"Centered"});1801 try testFmt(" Centered ", "{:^24}", .{"Centered"});
1802 try testFmt("-", "{:-^1}", .{""});
1834}1803}
18351804
1836test "decimal float padding" {1805test "decimal float padding" {