Skip to content

Commit

Permalink
[common] Add fmt_debug_string polyfill
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri committed Nov 14, 2024
1 parent cd2ac72 commit f3b1043
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 0 deletions.
1 change: 1 addition & 0 deletions common/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ drake_cc_package_library(
drake_cc_library(
name = "fmt",
srcs = [
"fmt.cc",
"fmt_eigen.cc",
],
hdrs = [
Expand Down
49 changes: 49 additions & 0 deletions common/fmt.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "drake/common/fmt.h"

namespace drake {

// We can simplify this after we drop support for Ubuntu 22.04 Jammy.
std::string fmt_debug_string(std::string_view x) {
#if FMT_VERSION >= 90000
return fmt::format("{:?}", x);
#else
std::string result;
result.reserve(x.size() + 2);
result.push_back('"');
for (const char ch : x) {
// Check for characters with a custom escape sequence.
if (ch == '\n') {
result.push_back('\\');
result.push_back('n');
continue;
}
if (ch == '\r') {
result.push_back('\\');
result.push_back('r');
continue;
}
if (ch == '\t') {
result.push_back('\\');
result.push_back('t');
continue;
}
// Check for characters that require a leading backslash.
if (ch == '"' || ch == '\\') {
result.push_back('\\');
result.push_back(ch);
continue;
}
// Check for any other non-printable characters.
if (ch < 0x20 || ch >= 0x7F) {
result.append(fmt::format("\\x{:02x}", static_cast<int>(ch)));
continue;
}
// Normal character.
result.push_back(ch);
}
result.push_back('"');
return result;
#endif
}

} // namespace drake
8 changes: 8 additions & 0 deletions common/fmt.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ std::string fmt_floating_point(T x) {
return result;
}


/** Returns `fmt::("{:?}", x)`, i.e, using fmt's "debug string format"; see
https://fmt.dev docs for the '?' presentation type for details. We provide this
wrapper because not all of our supported platforms have a new-enough fmt
to rely on it. On platforms with older fmt, we use a Drake re-implementation
of the feature that does NOT handle unicode correctly. */
std::string fmt_debug_string(std::string_view x);

namespace internal::formatter_as {

/* The DRAKE_FORMATTER_AS macro specializes this for it's format_as types.
Expand Down
24 changes: 24 additions & 0 deletions common/test/fmt_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <ostream>

#include <fmt/args.h>
#include <fmt/ranges.h>
#include <gtest/gtest.h>

Expand Down Expand Up @@ -78,5 +79,28 @@ GTEST_TEST(FmtTest, FloatingPoint) {
EXPECT_EQ(fmt_floating_point(1.0f), "1.0");
}

GTEST_TEST(FmtTest, DebugString) {
// We'll use these named fmt args to help make our expected values clear.
fmt::dynamic_format_arg_store<fmt::format_context> args;
args.push_back(fmt::arg("bs", '\\')); // backslash
args.push_back(fmt::arg("dq", '"')); // double quote

// Plain string.
EXPECT_EQ(fmt_debug_string("Hello, world!"),
fmt::vformat("{dq}Hello, world!{dq}", args));

// Custom escape sequences.
EXPECT_EQ(fmt_debug_string("aa\nbb\rcc\tdd"),
fmt::vformat("{dq}aa{bs}nbb{bs}rcc{bs}tdd{dq}", args));

// Printable characters that require escaping.
EXPECT_EQ(fmt_debug_string("aa\"bb'cc\\dd"),
fmt::vformat("{dq}aa{bs}{dq}bb'cc{bs}{bs}dd{dq}", args));

// Non-printable characters.
EXPECT_EQ(fmt_debug_string("aa\x0e!\x7f_"),
fmt::vformat("{dq}aa{bs}x0e!{bs}x7f_{dq}", args));
}

} // namespace
} // namespace drake

0 comments on commit f3b1043

Please sign in to comment.