From 2111fc5f63822c05bc88b58ad85cb897fa79e3fb Mon Sep 17 00:00:00 2001 From: asynts Date: Sat, 26 Sep 2020 15:26:00 +0200 Subject: [PATCH] AK+Format: Add new integer to string backend. I put this into the header in the hope that it could be re-used by the printf implementation. That would not be super trivial though, so I am not doing that now. --- AK/PrintfImplementation.h | 167 ++++++++++++++++++++++++++++++++++++++ AK/Tests/TestPrintf.cpp | 124 ++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 AK/Tests/TestPrintf.cpp diff --git a/AK/PrintfImplementation.h b/AK/PrintfImplementation.h index c1ee76f4735..4fd8665ecee 100644 --- a/AK/PrintfImplementation.h +++ b/AK/PrintfImplementation.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,172 @@ namespace PrintfImplementation { static constexpr const char* printf_hex_digits_lower = "0123456789abcdef"; static constexpr const char* printf_hex_digits_upper = "0123456789ABCDEF"; +enum class Align { + Left, + Center, + Right, +}; + +enum class SignMode { + OnlyIfNeeded, + Always, + Reserved +}; + +// 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. +inline size_t convert_unsigned_to_string(u64 value, Array& buffer, u8 base, bool upper_case) +{ + ASSERT(base >= 2 && base <= 16); + + static constexpr const char* lowercase_lookup = "0123456789abcdef"; + static constexpr const char* uppercase_lookup = "0123456789ABCDEF"; + + if (value == 0) { + buffer[0] = '0'; + return 1; + } + + size_t used = 0; + while (value > 0) { + if (upper_case) + buffer[used++] = uppercase_lookup[value % base]; + else + buffer[used++] = lowercase_lookup[value % base]; + + value /= base; + } + + // Reverse the list; I came up with this logic in like three seconds so it's probably wrong in some edge case. + for (size_t i = 0; i < used / 2; ++i) + swap(buffer[i], buffer[used - i - 1]); + + return used; +} + +inline size_t convert_unsigned_to_string( + u64 value, + StringBuilder& builder, + u8 base = 10, + bool prefix = false, + bool upper_case = false, + bool zero_pad = false, + Align align = Align::Right, + size_t width = 0, + char fill = ' ', + SignMode sign_mode = SignMode::OnlyIfNeeded, + bool is_negative = false) +{ + Array buffer; + + const auto used_by_significant_digits = convert_unsigned_to_string(value, buffer, base, upper_case); + size_t used_by_prefix = sign_mode == SignMode::OnlyIfNeeded ? static_cast(is_negative) : 1; + + if (prefix) { + if (base == 8) + used_by_prefix += 1; + else if (base == 16) + used_by_prefix += 2; + else if (base == 2) + used_by_prefix += 2; + } + + const auto put_prefix = [&]() { + if (is_negative) + builder.append('-'); + else if (sign_mode == SignMode::Always) + builder.append('+'); + else if (sign_mode == SignMode::Reserved) + builder.append(' '); + + if (prefix) { + if (base == 2) { + if (upper_case) + builder.append("0B"); + else + builder.append("0b"); + } else if (base == 8) { + builder.append("0"); + } else if (base == 16) { + if (upper_case) + builder.append("0X"); + else + builder.append("0x"); + } + } + }; + const auto put_padding = [&](size_t amount, char fill) { + for (size_t i = 0; i < amount; ++i) + builder.append(fill); + }; + const auto put_digits = [&]() { + builder.append(StringView { buffer.span().trim(used_by_significant_digits) }); + }; + + const auto used_by_field = used_by_significant_digits + used_by_prefix; + const auto used_by_padding = static_cast(max(0, static_cast(width) - static_cast(used_by_field))); + + if (align == Align::Left) { + const auto used_by_right_padding = used_by_padding; + + put_prefix(); + put_digits(); + put_padding(used_by_right_padding, fill); + + return used_by_field + used_by_right_padding; + } + + if (align == Align::Center) { + const auto used_by_left_padding = used_by_padding / 2; + const auto used_by_right_padding = ceil_div(used_by_padding, 2); + + put_padding(used_by_left_padding, fill); + put_prefix(); + put_digits(); + put_padding(used_by_right_padding, fill); + + return used_by_left_padding + used_by_field + used_by_right_padding; + } + + if (align == Align::Right) { + const auto used_by_left_padding = used_by_padding; + + if (zero_pad) { + put_prefix(); + put_padding(used_by_left_padding, '0'); + put_digits(); + } else { + put_padding(used_by_left_padding, fill); + put_prefix(); + put_digits(); + } + + return used_by_field + used_by_left_padding; + } + + ASSERT_NOT_REACHED(); +} + +inline size_t convert_signed_to_string( + i64 value, + StringBuilder& builder, + u8 base = 10, + bool common_prefix = false, + bool upper_case = false, + bool zero_pad = false, + Align align = Align::Right, + size_t width = 0, + char fill = ' ', + SignMode sign_mode = SignMode::OnlyIfNeeded) +{ + bool is_negative = value < 0; + + if (value < 0) + value = -value; + + return convert_unsigned_to_string(static_cast(value), builder, base, common_prefix, upper_case, zero_pad, align, width, fill, sign_mode, is_negative); +} + #ifdef __serenity__ extern "C" size_t strlen(const char*); #else diff --git a/AK/Tests/TestPrintf.cpp b/AK/Tests/TestPrintf.cpp new file mode 100644 index 00000000000..84c455d7446 --- /dev/null +++ b/AK/Tests/TestPrintf.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020, the SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include +#include + +TEST_CASE(format_unsigned_with_internal_implementation) +{ + Array buffer; + size_t used = 0; + + used = PrintfImplementation::convert_unsigned_to_string(12341234, buffer, 10, false); + EXPECT_EQ(StringView { buffer.span().trim(used) }, "12341234"); + + used = PrintfImplementation::convert_unsigned_to_string(12341234, buffer, 16, false); + EXPECT_EQ(StringView { buffer.span().trim(used) }, "bc4ff2"); + + used = PrintfImplementation::convert_unsigned_to_string(12341234, buffer, 16, true); + EXPECT_EQ(StringView { buffer.span().trim(used) }, "BC4FF2"); + + used = PrintfImplementation::convert_unsigned_to_string(0, buffer, 10, true); + EXPECT_EQ(StringView { buffer.span().trim(used) }, "0"); + + used = PrintfImplementation::convert_unsigned_to_string(NumericLimits::max(), buffer, 10, true); + EXPECT_EQ(StringView { buffer.span().trim(used) }, "18446744073709551615"); +} + +TEST_CASE(format_unsigned_just_pass_through) +{ + StringBuilder builder; + size_t used = 0; + + builder.clear(); + used = PrintfImplementation::convert_unsigned_to_string(12341234, builder); + EXPECT_EQ(used, 8u); + EXPECT_EQ(builder.to_string(), "12341234"); + + builder.clear(); + used = PrintfImplementation::convert_unsigned_to_string(12341234, builder, 16); + EXPECT_EQ(used, 6u); + EXPECT_EQ(builder.to_string(), "bc4ff2"); + + builder.clear(); + used = PrintfImplementation::convert_unsigned_to_string(12341234, builder, 16, false, true); + EXPECT_EQ(used, 6u); + EXPECT_EQ(builder.to_string(), "BC4FF2"); +} + +TEST_CASE(format_unsigned) +{ + StringBuilder builder; + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Right, 4, '*', PrintfImplementation::SignMode::OnlyIfNeeded); + EXPECT_EQ(builder.to_string(), "0042"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Left, 4, '*', PrintfImplementation::SignMode::OnlyIfNeeded); + EXPECT_EQ(builder.to_string(), "42**"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Center, 4, '*', PrintfImplementation::SignMode::OnlyIfNeeded); + EXPECT_EQ(builder.to_string(), "*42*"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Center, 9, '*', PrintfImplementation::SignMode::OnlyIfNeeded); + EXPECT_EQ(builder.to_string(), "***42****"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Center, 9, '*', PrintfImplementation::SignMode::Reserved); + EXPECT_EQ(builder.to_string(), "*** 42***"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Left, 4, '*', PrintfImplementation::SignMode::Always, true); + EXPECT_EQ(builder.to_string(), "-42*"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Center, 4, '*', PrintfImplementation::SignMode::Reserved, true); + EXPECT_EQ(builder.to_string(), "-42*"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(42, builder, 10, false, false, true, PrintfImplementation::Align::Right, 4, '*', PrintfImplementation::SignMode::OnlyIfNeeded, true); + EXPECT_EQ(builder.to_string(), "-042"); + + builder.clear(); + PrintfImplementation::convert_unsigned_to_string(32, builder, 16, true, false, true, PrintfImplementation::Align::Right, 8, '*', PrintfImplementation::SignMode::OnlyIfNeeded, true); + EXPECT_EQ(builder.to_string(), "-0x00020"); +} + +TEST_CASE(format_signed) +{ + StringBuilder builder; + + builder.clear(); + PrintfImplementation::convert_signed_to_string(42, builder, 10, false, false, false, PrintfImplementation::Align::Right, 8, '/', PrintfImplementation::SignMode::OnlyIfNeeded); + EXPECT_EQ(builder.to_string(), "//////42"); +} + +TEST_MAIN(Printf)