Support dynamic field width in folly::format()
authorBrett Simmers <bsimmers@fb.com>
Sun, 9 Aug 2015 22:41:15 +0000 (15:41 -0700)
committerfacebook-github-bot-1 <folly-bot@fb.com>
Sun, 9 Aug 2015 23:22:14 +0000 (16:22 -0700)
Summary: I added this to support logging with varying indentation levels, but
it could also be useful in other situations. Examples are in the
test/documentation.

Reviewed By: @tudor

Differential Revision: D2322206

folly/Format-inl.h
folly/Format.cpp
folly/Format.h
folly/FormatArg.h
folly/docs/Format.md
folly/test/FormatTest.cpp

index 1239188cbe987900c047602c4dccb37a1f974e0e..56adcd0132a739b76447f1cb31abde8db08df870 100644 (file)
@@ -225,6 +225,8 @@ void BaseFormatter<Derived, containerMode, Args...>::operator()(Output& out)
     int argIndex = 0;
     auto piece = arg.splitKey<true>();  // empty key component is okay
     if (containerMode) {  // static
+      arg.enforce(arg.width != FormatArg::kDynamicWidth,
+                  "dynamic field width not supported in vformat()");
       if (piece.empty()) {
         arg.setNextIntKey(nextArg++);
         hasDefaultArgIndex = true;
@@ -234,9 +236,22 @@ void BaseFormatter<Derived, containerMode, Args...>::operator()(Output& out)
       }
     } else {
       if (piece.empty()) {
+        if (arg.width == FormatArg::kDynamicWidth) {
+          arg.enforce(arg.widthIndex == FormatArg::kNoIndex,
+                      "cannot provide width arg index without value arg index");
+          int sizeArg = nextArg++;
+          arg.width = getSizeArg(sizeArg, arg);
+        }
+
         argIndex = nextArg++;
         hasDefaultArgIndex = true;
       } else {
+        if (arg.width == FormatArg::kDynamicWidth) {
+          arg.enforce(arg.widthIndex != FormatArg::kNoIndex,
+                      "cannot provide value arg index without width arg index");
+          arg.width = getSizeArg(arg.widthIndex, arg);
+        }
+
         try {
           argIndex = to<int>(piece);
         } catch (const std::out_of_range& e) {
@@ -402,6 +417,11 @@ class FormatValue<
   {
  public:
   explicit FormatValue(T val) : val_(val) { }
+
+  T getValue() const {
+    return val_;
+  }
+
   template <class FormatCallback>
   void format(FormatArg& arg, FormatCallback& cb) const {
     arg.validate(FormatArg::Type::INTEGER);
index 25a436e525a00a81a9eda0b2ff0622f8ca088d4d..0f67969c7b1df3f8ebe92f63effbd7152210ea2c 100644 (file)
@@ -209,12 +209,25 @@ void FormatArg::initSlow() {
       if (++p == end) return;
     }
 
-    if (*p >= '0' && *p <= '9') {
-      auto b = p;
+    auto readInt = [&] {
+      auto const b = p;
       do {
         ++p;
       } while (p != end && *p >= '0' && *p <= '9');
-      width = to<int>(StringPiece(b, p));
+      return to<int>(StringPiece(b, p));
+    };
+
+    if (*p == '*') {
+      width = kDynamicWidth;
+      ++p;
+
+      if (p == end) return;
+
+      if (*p >= '0' && *p <= '9') widthIndex = readInt();
+
+      if (p == end) return;
+    } else if (*p >= '0' && *p <= '9') {
+      width = readInt();
 
       if (p == end) return;
     }
index c7b729d41cb02a7f448dc7a1bade47a371117d6a..e3134a33339df78345c65b4543630b5926fd3472 100644 (file)
@@ -127,6 +127,39 @@ class BaseFormatter {
     return doFormatFrom<0>(i, arg, cb);
   }
 
+  template <size_t K>
+  typename std::enable_if<K == valueCount, int>::type
+  getSizeArgFrom(size_t i, const FormatArg& arg) const {
+    arg.error("argument index out of range, max=", i);
+  }
+
+  template <class T>
+  typename std::enable_if<std::is_integral<T>::value &&
+                          !std::is_same<T, bool>::value, int>::type
+  getValue(const FormatValue<T>& format, const FormatArg&) const {
+    return static_cast<int>(format.getValue());
+  }
+
+  template <class T>
+  typename std::enable_if<!std::is_integral<T>::value ||
+                          std::is_same<T, bool>::value, int>::type
+  getValue(const FormatValue<T>&, const FormatArg& arg) const {
+    arg.error("dynamic field width argument must be integral");
+  }
+
+  template <size_t K>
+  typename std::enable_if<K < valueCount, int>::type
+  getSizeArgFrom(size_t i, const FormatArg& arg) const {
+    if (i == K) {
+      return getValue(std::get<K>(values_), arg);
+    }
+    return getSizeArgFrom<K+1>(i, arg);
+  }
+
+  int getSizeArg(size_t i, const FormatArg& arg) const {
+    return getSizeArgFrom<0>(i, arg);
+  }
+
   StringPiece str_;
 
  protected:
index bcf4c476602e3c4e7eac549b23e9e3e0535464db..c4d94736aa6b39c25896ee17c3dc673386c9702b 100644 (file)
@@ -48,6 +48,7 @@ struct FormatArg {
       thousandsSeparator(false),
       trailingDot(false),
       width(kDefaultWidth),
+      widthIndex(kNoIndex),
       precision(kDefaultPrecision),
       presentation(kDefaultPresentation),
       nextKeyMode_(NextKeyMode::NONE) {
@@ -135,10 +136,13 @@ struct FormatArg {
   bool trailingDot;
 
   /**
-   * Field width
+   * Field width and optional argument index
    */
   static constexpr int kDefaultWidth = -1;
+  static constexpr int kDynamicWidth = -2;
+  static constexpr int kNoIndex = -1;
   int width;
+  int widthIndex;
 
   /**
    * Precision
index 5e40bf5d2f611981767792e7a0170ce425820535..b822c21a7591bf96ea27574aea0c56ffea760166 100644 (file)
@@ -82,6 +82,16 @@ std::cout << vformat("{0} {2} {1}", t);
 std::cout << format("{:X<10} {}", "hello", "world");
 // => "helloXXXXX world"
 
+// Field width may be a runtime value rather than part of the format string
+int x = 6;
+std::cout << format("{:-^*}", x, "hi");
+// => "--hi--"
+
+// Explicit arguments work with dynamic field width, as long as indexes are
+// given for both the value and the field width.
+std::cout << format("{2:+^*0}",
+9, "unused", 456); // => "+++456+++"
+
 // Format supports printf-style format specifiers
 std::cout << format("{0:05d} decimal = {0:04x} hex", 42);
 // => "00042 decimal = 002a hex"
@@ -142,7 +152,10 @@ Format specification:
   `0X` for hexadecimal; only valid for integers)
 - '`0`': 0-pad after sign, same as specifying "`0=`" as the `fill` and
   `align` parameters (only valid for numbers)
-- `width`: minimum field width
+- `width`: minimum field width. May be '`*`' to indicate that the field width
+  is given by an argument. Defaults to the next argument (preceding the value
+  to be formatted) but an explicit argument index may be given following the
+  '`*`'. Not supported in `vformat()`.
 - '`,`' (comma): output comma as thousands' separator (only valid for integers,
   and only for decimal output)
 - `precision` (not allowed for integers):
index 4385401be765425cf428aa975c1f1163d72765a8..8676180fd2f6484fb271203cea2abf04ca5d07ef 100644 (file)
@@ -105,6 +105,13 @@ TEST(Format, Simple) {
   EXPECT_EQ("hello  ", sformat("{:<7}", "hello"));
   EXPECT_EQ("  hello", sformat("{:>7}", "hello"));
 
+  EXPECT_EQ("  hi", sformat("{:>*}", 4, "hi"));
+  EXPECT_EQ("   hi!", sformat("{:*}{}", 3, "", "hi!"));
+  EXPECT_EQ("    123", sformat("{:*}", 7, 123));
+  EXPECT_EQ("123    ", sformat("{:<*}", 7, 123));
+  EXPECT_EQ("----<=>----", sformat("{:-^*}", 11, "<=>"));
+  EXPECT_EQ("+++456+++", sformat("{2:+^*0}", 9, "unused", 456));
+
   std::vector<int> v1 {10, 20, 30};
   EXPECT_EQ("0020", sformat("{0[1]:04}", v1));
   EXPECT_EQ("0020", svformat("{1:04}", v1));
@@ -420,7 +427,6 @@ TEST(Format, OutOfBounds) {
 }
 
 TEST(Format, BogusFormatString) {
-  // format() will crash the program if the format string is invalid.
   EXPECT_FORMAT_ERROR(sformat("}"), "single '}' in format string");
   EXPECT_FORMAT_ERROR(sformat("foo}bar"), "single '}' in format string");
   EXPECT_FORMAT_ERROR(sformat("foo{bar"), "missing ending '}'");
@@ -429,6 +435,22 @@ TEST(Format, BogusFormatString) {
   EXPECT_FORMAT_ERROR(sformat("{1.3}", 0, 1, 2), "index not allowed");
   EXPECT_FORMAT_ERROR(sformat("{0} {} {1}", 0, 1, 2),
                "may not have both default and explicit arg indexes");
+  EXPECT_FORMAT_ERROR(sformat("{:*}", 1.2),
+                      "dynamic field width argument must be integral");
+  EXPECT_FORMAT_ERROR(sformat("{} {:*}", "hi"),
+                      "argument index out of range, max=1");
+  EXPECT_FORMAT_ERROR(
+    sformat("{:*0}", 12, "ok"),
+    "cannot provide width arg index without value arg index"
+  );
+  EXPECT_FORMAT_ERROR(
+    sformat("{0:*}", 12, "ok"),
+    "cannot provide value arg index without width arg index"
+  );
+
+  std::vector<int> v{1, 2, 3};
+  EXPECT_FORMAT_ERROR(svformat("{:*}", v),
+                      "dynamic field width not supported in vformat()");
 
   // This one fails in detail::enforceWhitespace(), which throws
   // std::range_error