From 72ea046b68822561603f91c4e8b290c4478e8d7a Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Mon, 6 Mar 2023 17:31:39 +0000 Subject: [PATCH] AK: Add option to the string formatter to use a digit separator `vformat()` can now accept format specifiers of the form {:'[numeric-type]}. This will output a number with a comma separator every 3 digits. For example: `dbgln("{:'d}", 9999999);` will output 9,999,999. Binary, octal and hexadecimal numbers can also use this feature, for example: `dbgln("{:'x}", 0xffffffff);` will output ff,fff,fff. --- AK/FixedPoint.h | 2 +- AK/Format.cpp | 49 ++++++++++++------- AK/Format.h | 6 +++ Tests/AK/TestFormat.cpp | 15 ++++++ .../Libraries/LibCrypto/Hash/HashFunction.h | 2 +- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/AK/FixedPoint.h b/AK/FixedPoint.h index 83605364d9..2453c54246 100644 --- a/AK/FixedPoint.h +++ b/AK/FixedPoint.h @@ -443,7 +443,7 @@ struct Formatter> : StandardFormatter { i64 integer = value.ltrunk(); constexpr u64 one = static_cast(1) << precision; u64 fraction_raw = value.raw() & (one - 1); - return builder.put_fixed_point(is_negative, integer, fraction_raw, one, base, upper_case, m_zero_pad, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); + return builder.put_fixed_point(is_negative, integer, fraction_raw, one, base, upper_case, m_zero_pad, m_use_separator, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); } }; diff --git a/AK/Format.cpp b/AK/Format.cpp index d820d89b38..c63583934e 100644 --- a/AK/Format.cpp +++ b/AK/Format.cpp @@ -47,8 +47,8 @@ namespace { static constexpr size_t use_next_index = NumericLimits::max(); // The worst case is that we have the largest 64-bit value formatted as binary number, this would take -// 65 bytes. Choosing a larger power of two won't hurt and is a bit of mitigation against out-of-bounds accesses. -static constexpr size_t convert_unsigned_to_string(u64 value, Array& buffer, u8 base, bool upper_case) +// 65 bytes (85 bytes with separators). Choosing a larger power of two won't hurt and is a bit of mitigation against out-of-bounds accesses. +static constexpr size_t convert_unsigned_to_string(u64 value, Array& buffer, u8 base, bool upper_case, bool use_separator) { VERIFY(base >= 2 && base <= 16); @@ -61,13 +61,18 @@ static constexpr size_t convert_unsigned_to_string(u64 value, Array& bu } size_t used = 0; + size_t digit_count = 0; while (value > 0) { if (upper_case) buffer[used++] = uppercase_lookup[value % base]; else buffer[used++] = lowercase_lookup[value % base]; + digit_count++; value /= base; + + if (use_separator && value > 0 && digit_count % 3 == 0) + buffer[used++] = ','; } for (size_t i = 0; i < used / 2; ++i) @@ -242,6 +247,7 @@ ErrorOr FormatBuilder::put_u64( bool prefix, bool upper_case, bool zero_pad, + bool use_separator, Align align, size_t min_width, char fill, @@ -253,7 +259,7 @@ ErrorOr FormatBuilder::put_u64( Array buffer; - auto const used_by_digits = convert_unsigned_to_string(value, buffer, base, upper_case); + auto const used_by_digits = convert_unsigned_to_string(value, buffer, base, upper_case, use_separator); size_t used_by_prefix = 0; if (align == Align::Right && zero_pad) { @@ -345,6 +351,7 @@ ErrorOr FormatBuilder::put_i64( bool prefix, bool upper_case, bool zero_pad, + bool use_separator, Align align, size_t min_width, char fill, @@ -353,7 +360,7 @@ ErrorOr FormatBuilder::put_i64( auto const is_negative = value < 0; value = is_negative ? -value : value; - TRY(put_u64(static_cast(value), base, prefix, upper_case, zero_pad, align, min_width, fill, sign_mode, is_negative)); + TRY(put_u64(static_cast(value), base, prefix, upper_case, zero_pad, use_separator, align, min_width, fill, sign_mode, is_negative)); return {}; } @@ -365,6 +372,7 @@ ErrorOr FormatBuilder::put_fixed_point( u8 base, bool upper_case, bool zero_pad, + bool use_separator, Align align, size_t min_width, size_t precision, @@ -378,7 +386,7 @@ ErrorOr FormatBuilder::put_fixed_point( if (is_negative) integer_value = -integer_value; - TRY(format_builder.put_u64(static_cast(integer_value), base, false, upper_case, false, Align::Right, 0, ' ', sign_mode, is_negative)); + TRY(format_builder.put_u64(static_cast(integer_value), base, false, upper_case, false, use_separator, Align::Right, 0, ' ', sign_mode, is_negative)); if (precision > 0) { // FIXME: This is a terrible approximation but doing it properly would be a lot of work. If someone is up for that, a good @@ -419,13 +427,13 @@ ErrorOr FormatBuilder::put_fixed_point( TRY(string_builder.try_append('.')); if (leading_zeroes > 0) - TRY(format_builder.put_u64(0, base, false, false, true, Align::Right, leading_zeroes)); + TRY(format_builder.put_u64(0, base, false, false, true, use_separator, Align::Right, leading_zeroes)); if (visible_precision > 0) - TRY(format_builder.put_u64(fraction, base, false, upper_case, true, Align::Right, visible_precision)); + TRY(format_builder.put_u64(fraction, base, false, upper_case, true, use_separator, Align::Right, visible_precision)); if (zero_pad && (precision - leading_zeroes - visible_precision) > 0) - TRY(format_builder.put_u64(0, base, false, false, true, Align::Right, precision - leading_zeroes - visible_precision)); + TRY(format_builder.put_u64(0, base, false, false, true, use_separator, Align::Right, precision - leading_zeroes - visible_precision)); } TRY(put_string(string_builder.string_view(), align, min_width, NumericLimits::max(), fill)); @@ -438,6 +446,7 @@ ErrorOr FormatBuilder::put_f64( u8 base, bool upper_case, bool zero_pad, + bool use_separator, Align align, size_t min_width, size_t precision, @@ -468,7 +477,7 @@ ErrorOr FormatBuilder::put_f64( if (is_negative) value = -value; - TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, false, Align::Right, 0, ' ', sign_mode, is_negative)); + TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, false, use_separator, Align::Right, 0, ' ', sign_mode, is_negative)); if (precision > 0) { // FIXME: This is a terrible approximation but doing it properly would be a lot of work. If someone is up for that, a good @@ -492,10 +501,10 @@ ErrorOr FormatBuilder::put_f64( TRY(string_builder.try_append('.')); if (visible_precision > 0) - TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, true, Align::Right, visible_precision)); + TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, true, false, Align::Right, visible_precision)); if (zero_pad && (precision - visible_precision) > 0) - TRY(format_builder.put_u64(0, base, false, false, true, Align::Right, precision - visible_precision)); + TRY(format_builder.put_u64(0, base, false, false, true, false, Align::Right, precision - visible_precision)); } TRY(put_string(string_builder.string_view(), align, min_width, NumericLimits::max(), fill)); @@ -506,6 +515,7 @@ ErrorOr FormatBuilder::put_f80( long double value, u8 base, bool upper_case, + bool use_separator, Align align, size_t min_width, size_t precision, @@ -536,7 +546,7 @@ ErrorOr FormatBuilder::put_f80( if (is_negative) value = -value; - TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, false, Align::Right, 0, ' ', sign_mode, is_negative)); + TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, false, use_separator, Align::Right, 0, ' ', sign_mode, is_negative)); if (precision > 0) { // FIXME: This is a terrible approximation but doing it properly would be a lot of work. If someone is up for that, a good @@ -558,7 +568,7 @@ ErrorOr FormatBuilder::put_f80( if (visible_precision > 0) { string_builder.append('.'); - TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, true, Align::Right, visible_precision)); + TRY(format_builder.put_u64(static_cast(value), base, false, upper_case, true, false, Align::Right, visible_precision)); } } @@ -586,7 +596,7 @@ ErrorOr FormatBuilder::put_hexdump(ReadonlyBytes bytes, size_t width, char TRY(put_literal("\n"sv)); } } - TRY(put_u64(bytes[i], 16, false, false, true, Align::Right, 2)); + TRY(put_u64(bytes[i], 16, false, false, true, false, Align::Right, 2)); } if (width > 0 && bytes.size() && bytes.size() % width == 0) @@ -628,6 +638,9 @@ void StandardFormatter::parse(TypeErasedFormatParams& params, FormatParser& pars if (parser.consume_specific('#')) m_alternative_form = true; + if (parser.consume_specific('\'')) + m_use_separator = true; + if (parser.consume_specific('0')) m_zero_pad = true; @@ -767,9 +780,9 @@ ErrorOr Formatter::format(FormatBuilder& builder, T value) m_width = m_width.value_or(0); if constexpr (IsSame, T>) - return builder.put_u64(value, base, m_alternative_form, upper_case, m_zero_pad, m_align, m_width.value(), m_fill, m_sign_mode); + return builder.put_u64(value, base, m_alternative_form, upper_case, m_zero_pad, m_use_separator, m_align, m_width.value(), m_fill, m_sign_mode); else - return builder.put_i64(value, base, m_alternative_form, upper_case, m_zero_pad, m_align, m_width.value(), m_fill, m_sign_mode); + return builder.put_i64(value, base, m_alternative_form, upper_case, m_zero_pad, m_use_separator, m_align, m_width.value(), m_fill, m_sign_mode); } ErrorOr Formatter::format(FormatBuilder& builder, char value) @@ -832,7 +845,7 @@ ErrorOr Formatter::format(FormatBuilder& builder, long double m_width = m_width.value_or(0); m_precision = m_precision.value_or(6); - return builder.put_f80(value, base, upper_case, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); + return builder.put_f80(value, base, upper_case, m_use_separator, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); } ErrorOr Formatter::format(FormatBuilder& builder, double value) @@ -858,7 +871,7 @@ ErrorOr Formatter::format(FormatBuilder& builder, double value) m_width = m_width.value_or(0); m_precision = m_precision.value_or(6); - return builder.put_f64(value, base, upper_case, m_zero_pad, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); + return builder.put_f64(value, base, upper_case, m_zero_pad, m_use_separator, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode); } ErrorOr Formatter::format(FormatBuilder& builder, float value) diff --git a/AK/Format.h b/AK/Format.h index 4aaa9feb5f..42e2a67a25 100644 --- a/AK/Format.h +++ b/AK/Format.h @@ -191,6 +191,7 @@ public: bool prefix = false, bool upper_case = false, bool zero_pad = false, + bool use_separator = false, Align align = Align::Right, size_t min_width = 0, char fill = ' ', @@ -203,6 +204,7 @@ public: bool prefix = false, bool upper_case = false, bool zero_pad = false, + bool use_separator = false, Align align = Align::Right, size_t min_width = 0, char fill = ' ', @@ -216,6 +218,7 @@ public: u8 base = 10, bool upper_case = false, bool zero_pad = false, + bool use_separator = false, Align align = Align::Right, size_t min_width = 0, size_t precision = 6, @@ -228,6 +231,7 @@ public: long double value, u8 base = 10, bool upper_case = false, + bool use_separator = false, Align align = Align::Right, size_t min_width = 0, size_t precision = 6, @@ -240,6 +244,7 @@ public: u8 base = 10, bool upper_case = false, bool zero_pad = false, + bool use_separator = false, Align align = Align::Right, size_t min_width = 0, size_t precision = 6, @@ -328,6 +333,7 @@ struct StandardFormatter { FormatBuilder::SignMode m_sign_mode = FormatBuilder::SignMode::OnlyIfNeeded; Mode m_mode = Mode::Default; bool m_alternative_form = false; + bool m_use_separator = false; char m_fill = ' '; bool m_zero_pad = false; Optional m_width; diff --git a/Tests/AK/TestFormat.cpp b/Tests/AK/TestFormat.cpp index 0d45b3b31a..73b83dd87f 100644 --- a/Tests/AK/TestFormat.cpp +++ b/Tests/AK/TestFormat.cpp @@ -35,6 +35,17 @@ TEST_CASE(format_integers) EXPECT_EQ(DeprecatedString::formatted("{:08x}", 4096), "00001000"); EXPECT_EQ(DeprecatedString::formatted("{:x}", 0x1111222233334444ull), "1111222233334444"); EXPECT_EQ(DeprecatedString::formatted("{:4}", 12345678), "12345678"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", 0), "0"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", 4096), "4,096"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", 16777216), "16,777,216"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", AK::NumericLimits::max()), "18,446,744,073,709,551,615"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", AK::NumericLimits::max()), "18,446,744,073,709,551,615"); + EXPECT_EQ(DeprecatedString::formatted("{:'}", AK::NumericLimits::min() + 1), "-9,223,372,036,854,775,807"); + EXPECT_EQ(DeprecatedString::formatted("{:'x}", 0), "0"); + EXPECT_EQ(DeprecatedString::formatted("{:'x}", 16777216), "1,000,000"); + EXPECT_EQ(DeprecatedString::formatted("{:'x}", AK::NumericLimits::max()), "f,fff,fff,fff,fff,fff"); + EXPECT_EQ(DeprecatedString::formatted("{:'x}", AK::NumericLimits::min() + 1), "-7,fff,fff,fff,fff,fff"); + EXPECT_EQ(DeprecatedString::formatted("{:'b}", AK::NumericLimits::max()), "1,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111"); } TEST_CASE(reorder_format_arguments) @@ -95,6 +106,8 @@ TEST_CASE(format_octal) { EXPECT_EQ(DeprecatedString::formatted("{:o}", 0744), "744"); EXPECT_EQ(DeprecatedString::formatted("{:#o}", 0744), "0744"); + EXPECT_EQ(DeprecatedString::formatted("{:'o}", 054321), "54,321"); + EXPECT_EQ(DeprecatedString::formatted("{:'o}", 0567012340), "567,012,340"); } TEST_CASE(zero_pad) @@ -244,6 +257,8 @@ TEST_CASE(floating_point_numbers) EXPECT_EQ(DeprecatedString::formatted("{:.3}", 1.12), "1.12"); EXPECT_EQ(DeprecatedString::formatted("{:.1}", 1.12), "1.1"); EXPECT_EQ(DeprecatedString::formatted("{}", -1.12), "-1.12"); + EXPECT_EQ(DeprecatedString::formatted("{:'.4}", 1234.5678), "1,234.5678"); + EXPECT_EQ(DeprecatedString::formatted("{:'.4}", -1234.5678), "-1,234.5678"); EXPECT_EQ(DeprecatedString::formatted("{}", NAN), "nan"); EXPECT_EQ(DeprecatedString::formatted("{}", INFINITY), "inf"); diff --git a/Userland/Libraries/LibCrypto/Hash/HashFunction.h b/Userland/Libraries/LibCrypto/Hash/HashFunction.h index a4026bb151..0ea0df7f35 100644 --- a/Userland/Libraries/LibCrypto/Hash/HashFunction.h +++ b/Userland/Libraries/LibCrypto/Hash/HashFunction.h @@ -70,7 +70,7 @@ struct AK::Formatter> : StandardFormatter { for (size_t i = 0; i < digest.Size; ++i) { if (i > 0 && i % 4 == 0) TRY(builder.put_padding('-', 1)); - TRY(builder.put_u64(digest.data[i], 16, false, false, true, FormatBuilder::Align::Right, 2)); + TRY(builder.put_u64(digest.data[i], 16, false, false, true, false, FormatBuilder::Align::Right, 2)); } return {}; }