Hexadecimal printing function

Add IntToHexString, a simple hexadecimal output function.
PointerToString uses IntToHexString instead of IntToString.

Change-Id: Ib64ab888850d5050da84b4b84a2a279f0381f48f
diff --git a/pw_string/public/pw_string/type_to_string.h b/pw_string/public/pw_string/type_to_string.h
index f2f031d..90a91cd 100644
--- a/pw_string/public/pw_string/type_to_string.h
+++ b/pw_string/public/pw_string/type_to_string.h
@@ -30,6 +30,12 @@
 // non-negative integer. Returns 1 for 0 or 1 + log base 10 for other numbers.
 uint_fast8_t DecimalDigitCount(uint64_t integer);
 
+// Returns the number of digits in the hexadecimal representation of the
+// provided non-negative integer.
+constexpr uint_fast8_t HexDigitCount(uint64_t integer) {
+  return (64u - __builtin_clzll(integer | 1u) + 3u) / 4u;
+}
+
 // Writes an integer as a null-terminated string in base 10. Returns the number
 // of characters written, excluding the null terminator, and the status.
 //
@@ -63,6 +69,10 @@
 template <>
 StatusWithSize IntToString(int64_t integer, const span<char>& buffer);
 
+// Writes an integer as a hexadecimal string. Semantics match IntToString. The
+// output is lowercase without a leading 0x.
+StatusWithSize IntToHexString(uint64_t value, const span<char>& buffer);
+
 // Rounds a floating point number to an integer and writes it as a
 // null-terminated string. Returns the number of characters written, excluding
 // the null terminator, and the status.
diff --git a/pw_string/to_string_test.cc b/pw_string/to_string_test.cc
index e34f0ec..754c96f 100644
--- a/pw_string/to_string_test.cc
+++ b/pw_string/to_string_test.cc
@@ -115,7 +115,7 @@
   CustomType custom;
   const size_t length = std::snprintf(expected,
                                       sizeof(expected),
-                                      "%" PRIdPTR,
+                                      "%" PRIxPTR,
                                       reinterpret_cast<intptr_t>(&custom));
 
   EXPECT_EQ(length, ToString(&custom, buffer).size());
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index 829dfdd..74235b5 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -47,6 +47,13 @@
     10000000000000000000ull,  // 10^19
 };
 
+StatusWithSize HandleExhaustedBuffer(const span<char>& buffer) {
+  if (!buffer.empty()) {
+    buffer[0] = '\0';
+  }
+  return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+}
+
 }  // namespace
 
 uint_fast8_t DecimalDigitCount(uint64_t integer) {
@@ -73,10 +80,7 @@
   const uint_fast8_t total_digits = DecimalDigitCount(value);
 
   if (total_digits >= buffer.size()) {
-    if (!buffer.empty()) {
-      buffer[0] = '\0';
-    }
-    return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+    return HandleExhaustedBuffer(buffer);
   }
 
   buffer[total_digits] = '\0';
@@ -106,6 +110,22 @@
   return StatusWithSize(total_digits);
 }
 
+StatusWithSize IntToHexString(uint64_t value, const span<char>& buffer) {
+  const uint_fast8_t digits = HexDigitCount(value);
+
+  if (digits >= buffer.size()) {
+    return HandleExhaustedBuffer(buffer);
+  }
+
+  for (int i = digits - 1; i >= 0; --i) {
+    buffer[i] = "0123456789abcdef"[value & 0xF];
+    value >>= 4;
+  }
+
+  buffer[digits] = '\0';
+  return StatusWithSize(digits);
+}
+
 template <>
 StatusWithSize IntToString(int64_t value, const span<char>& buffer) {
   if (value >= 0) {
@@ -121,10 +141,7 @@
     return StatusWithSize(result.size() + 1);
   }
 
-  if (!buffer.empty()) {
-    buffer[0] = '\0';
-  }
-  return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+  return HandleExhaustedBuffer(buffer);
 }
 
 // TODO(hepler): Look into using the float overload of std::to_chars when it is
@@ -147,10 +164,7 @@
     return StatusWithSize(written);
   }
 
-  if (!buffer.empty()) {
-    buffer[0] = '\0';
-  }
-  return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+  return HandleExhaustedBuffer(buffer);
 }
 
 StatusWithSize BoolToString(bool value, const span<char>& buffer) {
@@ -161,8 +175,7 @@
   if (pointer == nullptr) {
     return CopyEntireString("null", buffer);
   }
-  // TODO(hepler): Add support for hexadecimal output.
-  return IntToString(reinterpret_cast<uintptr_t>(pointer), buffer);
+  return IntToHexString(reinterpret_cast<uintptr_t>(pointer), buffer);
 }
 
 StatusWithSize CopyString(const std::string_view& value,
@@ -181,10 +194,7 @@
 StatusWithSize CopyEntireString(const std::string_view& value,
                                 const span<char>& buffer) {
   if (value.size() >= buffer.size()) {
-    if (!buffer.empty()) {
-      buffer[0] = '\0';
-    }
-    return StatusWithSize(Status::RESOURCE_EXHAUSTED, 0);
+    return HandleExhaustedBuffer(buffer);
   }
 
   std::memcpy(buffer.data(), value.data(), value.size());
diff --git a/pw_string/type_to_string_test.cc b/pw_string/type_to_string_test.cc
index 389f9f6..af1a607 100644
--- a/pw_string/type_to_string_test.cc
+++ b/pw_string/type_to_string_test.cc
@@ -57,6 +57,39 @@
   }
 }
 
+TEST(Digits, HexDigits_AllOneDigit) {
+  for (uint64_t i = 0; i < 0x10; ++i) {
+    ASSERT_EQ(1u, HexDigitCount(i));
+  }
+}
+
+TEST(Digits, HexDigits_AllTwoDigit) {
+  for (uint64_t i = 0x10; i < 0x100u; ++i) {
+    ASSERT_EQ(2u, HexDigitCount(i));
+  }
+}
+
+TEST(Digits, HexDigits_1To15Digits) {
+  uint64_t value = 1;
+  for (unsigned digits = 1; digits <= 15u; ++digits) {
+    ASSERT_EQ(digits, HexDigitCount(value));
+    ASSERT_EQ(digits, HexDigitCount(value + 1));
+
+    value *= 0x10;
+    ASSERT_EQ(digits, HexDigitCount(value - 1));
+  }
+}
+
+TEST(Digits, HexDigits_16) {
+  for (uint64_t i : {
+           0x1000000000000000llu,
+           0x1000000000000001llu,
+           std::numeric_limits<unsigned long long>::max(),
+       }) {
+    ASSERT_EQ(16u, HexDigitCount(i));
+  }
+}
+
 class TestWithBuffer : public ::testing::Test {
  protected:
   static constexpr char kStartingString[] = "!@#$%^&*()!@#$%^&*()";
@@ -233,6 +266,57 @@
   }
 }
 
+class IntToHexStringTest : public TestWithBuffer {};
+
+TEST_F(IntToHexStringTest, Sweep) {
+  for (unsigned i = 0; i < 1030; ++i) {
+    char hex[16];
+    int bytes = std::snprintf(hex, sizeof(hex), "%x", static_cast<unsigned>(i));
+
+    auto result = IntToHexString(i, buffer_);
+    EXPECT_EQ(static_cast<size_t>(bytes), result.size());
+    EXPECT_TRUE(result.ok());
+    EXPECT_STREQ(hex, buffer_);
+  }
+}
+
+TEST_F(IntToHexStringTest, Uint32Max) {
+  EXPECT_EQ(
+      8u,
+      IntToHexString(std::numeric_limits<uint32_t>::max() - 1, buffer_).size());
+  EXPECT_STREQ("fffffffe", buffer_);
+
+  EXPECT_EQ(
+      8u, IntToHexString(std::numeric_limits<uint32_t>::max(), buffer_).size());
+  EXPECT_STREQ("ffffffff", buffer_);
+}
+
+TEST_F(IntToHexStringTest, Uint64Max) {
+  EXPECT_EQ(
+      16u,
+      IntToHexString(std::numeric_limits<uint64_t>::max() - 1, buffer_).size());
+  EXPECT_STREQ("fffffffffffffffe", buffer_);
+
+  EXPECT_EQ(
+      16u,
+      IntToHexString(std::numeric_limits<uint64_t>::max(), buffer_).size());
+  EXPECT_STREQ("ffffffffffffffff", buffer_);
+}
+
+TEST_F(IntToHexStringTest, EmptyBuffer_WritesNothing) {
+  auto result = IntToHexString(0xbeef, span(buffer_, 0));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(IntToHexStringTest, TooSmall_Truncates) {
+  auto result = IntToHexString(0xbeef, span(buffer_, 3));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("", buffer_);
+}
+
 class FloatAsIntToStringTest : public TestWithBuffer {};
 
 TEST_F(FloatAsIntToStringTest, PositiveInfinity) {
@@ -393,9 +477,9 @@
 }
 
 TEST_F(PointerToStringTest, WritesAddress) {
-  const void* pointer = reinterpret_cast<void*>(321);
-  EXPECT_EQ(3u, PointerToString(pointer, buffer_).size());
-  EXPECT_STREQ("321", buffer_);
+  const void* pointer = reinterpret_cast<void*>(0xbeef);
+  EXPECT_EQ(4u, PointerToString(pointer, buffer_).size());
+  EXPECT_STREQ("beef", buffer_);
 }
 
 class BoolToStringTest : public TestWithBuffer {};