You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by am...@apache.org on 2018/03/19 22:37:32 UTC

[trafficserver] branch master updated: bwprint: Type safe printf like output to BufferWriter instances.

This is an automated email from the ASF dual-hosted git repository.

amc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new f8b7db0  bwprint: Type safe printf like output to BufferWriter instances.
f8b7db0 is described below

commit f8b7db0e7cdeda539ecbfda2204ec42da1a6c2a0
Author: Alan M. Carroll <am...@apache.org>
AuthorDate: Sun Feb 25 18:17:56 2018 -0600

    bwprint: Type safe printf like output to BufferWriter instances.
---
 .../internal-libraries/MemSpan.en.rst              |   3 +-
 .../internal-libraries/buffer-writer.en.rst        | 130 +++++-
 lib/ts/BufferWriter.h                              | 281 +++++++++--
 lib/ts/BufferWriterFormat.cc                       | 517 +++++++++++++++++++++
 lib/ts/BufferWriterForward.h                       | 102 ++++
 lib/ts/Makefile.am                                 |   5 +-
 lib/ts/ink_std_compat.h                            |  95 ++++
 lib/ts/unit-tests/test_BufferWriter.cc             |  20 +-
 lib/ts/unit-tests/test_BufferWriterFormat.cc       | 200 ++++++++
 9 files changed, 1291 insertions(+), 62 deletions(-)

diff --git a/doc/developer-guide/internal-libraries/MemSpan.en.rst b/doc/developer-guide/internal-libraries/MemSpan.en.rst
index ce5f7df..a9b6775 100644
--- a/doc/developer-guide/internal-libraries/MemSpan.en.rst
+++ b/doc/developer-guide/internal-libraries/MemSpan.en.rst
@@ -95,4 +95,5 @@ Reference
    Strong caution must be used with containers such as :code:`std::vector` or :code:`std::string`
    because the lifetime of the memory can be much less than the lifetime of the container. In
    particular, adding or removing any element from a :code:`std::vector` can cause a re-allocation,
-   invalidating any view of the original memory. In general views should be treated like iterators.
+   invalidating any view of the original memory. In general views should be treated like iterators,
+   suitable for passing to nested function calls but not for storing.
diff --git a/doc/developer-guide/internal-libraries/buffer-writer.en.rst b/doc/developer-guide/internal-libraries/buffer-writer.en.rst
index d72d7ba..7e05547 100644
--- a/doc/developer-guide/internal-libraries/buffer-writer.en.rst
+++ b/doc/developer-guide/internal-libraries/buffer-writer.en.rst
@@ -25,11 +25,6 @@
 BufferWriter
 *************
 
-:class:`BufferWriter` is designed to make writing text to a buffer fast and safe. The output buffer
-can have a size and :class:`BufferWriter` will prevent writing past the end, while tracking the
-theoretical output to enable buffer resizing after the fact. This also lets a :class:`BufferWriter`
-instance write into the middle of a larger buffer, making nested output logic easy to build.
-
 Synopsis
 ++++++++
 
@@ -40,6 +35,17 @@ Synopsis
 Description
 +++++++++++
 
+:class:`BufferWriter` is designed to make writing text to a buffer fast, convenient, and safe. It is
+easier and less error-prone than using a combination of :code:`sprintf` and :code:`memcpy` as is
+done in many places in the code.. A :class:`BufferWriter` can have a size and will prevent writing
+past the end, while tracking the theoretical output to enable buffer resizing after the fact. This
+also lets a :class:`BufferWriter` instance write into the middle of a larger buffer, making nested
+output logic easy to build.
+
+The header files are divided in to two variants. ``BufferWriter.h`` provides the basic capabilities
+of buffer output control. ``BufferWriterFormat.h`` provides formatted output mechanisms, primarily
+the implementation and ancillary classes for :func:`BufferWriter::print`.
+
 :class:`BufferWriter` is an abstract base class, in the style of :code:`std::ostream`. There are
 several subclasses for various use cases. When passing around this is the common type.
 
@@ -84,7 +90,7 @@ Several basic types are overloaded and it is easy to extend to additional types.
 
 .. code-block:: cpp
 
-   ts::BufferWriter & operator << (ts::BufferWriter & w, ts::TextView const & sv) {
+   ts::BufferWriter & operator << (ts::BufferWriter & w, TextView const & sv) {
       w.write(sv.data(), sv.size());
       return w;
    }
@@ -263,6 +269,10 @@ Reference
       well with the standard "try before you buy" approach of attempting to write output, counting
       the characters needed, then allocating a sufficiently sized buffer and actually writing.
 
+   .. function:: BufferWriter & print(TextView fmt, ...)
+
+      Print the arguments according to the format. See `bw-formatting`_.
+
 .. class:: FixedBufferWriter : public BufferWriter
 
    This is a class that implements :class:`BufferWriter` on a fixed buffer, passed in to the constructor.
@@ -294,6 +304,114 @@ Reference
 
       Construct an instance with a capacity of :arg:`N`.
 
+.. _bw-formatting:
+
+Formatted Output
+++++++++++++++++
+
+:class:`BufferWriter` supports formatting output in a style similar to Python formatting via
+:func:`BufferWriter::print`. This takes a format string which then controls the use of subsquent
+arguments in generating out in the buffer. The basic format is divided in to three parts, separated by colons.
+
+.. productionList:: BufferWriterFormat
+   Format: "{" [name] [":" [specifier] [":" extension]] "}"
+   name: index | name
+   extension: <printable character except "{}">*
+
+:arg:`name`
+   The name of the argument to use. This can be a number in which case it is the zero based index of the argument to the method call. E.g. ``{0}`` means the first argument and ``{2}`` is the third argument after the format.
+
+      ``bw.print("{0} {1}", 'a', 'b')`` => ``a b``
+
+      ``bw.print("{1} {0}", 'a', 'b')`` => ``b a``
+
+   The name can be omitted in which case it is treated as an index in parallel to the position in
+   the format string. Only the position in the format string matters, not what names those other
+   format elements may have used.
+
+      ``bw.print("{0} {2} {}", 'a', 'b', 'c')`` => ``a c c``
+
+      ``bw.print("{0} {2} {2}", 'a', 'b', 'c')`` => ``a c c``
+
+   Note that an argument can be printed more than once if the name is used more than once.
+
+      ``bw.print("{0} {} {0}", 'a', 'b')`` => ``a b a``
+
+      ``bw.print("{0} {1} {0}", 'a', 'b')`` => ``a b a``
+
+   Alphanumeric names refer to values in a global table. These will be described in more detail someday.
+
+:arg:`specifier`
+   Basic formatting control.
+
+   .. productionList:: specifier
+      specifier: [[fill]align][sign]["#"]["0"][[min][.precision][,max][type]]
+      fill: <printable character except "{}%:"> | URI-char
+      URI-char: "%" hex-digit hex-digit
+      align: "<" | ">" | "=" | "^"
+      sign: "+" | "-" | " "
+      min: integer
+      precision: integer
+      max: integer
+      type: "x" | "o" | "b"
+
+   The output is placed in a field that is at least :token:`min` wide and no more than :token:`max` wide. If
+   the output is less than :token:`min` then
+
+      *  The :token:`fill` character is used for the extra space required. This can be an explicit
+         character or a URI encoded one (to allow otherwise reserved characters).
+      *  The output is shifted according to the :token:`align`.
+
+         <
+            Align to the left, fill to the right.
+
+         >
+            Align to the right, fill to the left.
+
+         ^
+            Align in the middle, fill to left and right.
+
+         =
+            Numerically align, putting the fill between the output and the sign character.
+
+   The output is clipped by :token:`max` width characters or the end of the buffer. :token:`precision` is used by
+   floating point values to specify the number of places of precision. The precense of the ``#`` character is used for
+   integer values and causes a radix indicator to be used (one of ``0xb``, ``0``, ``0x``).
+
+   :token:`type` is used to indicate type specific formatting. For integers it indicates the output
+   radix. If ``#`` is present the radix is prefix is generated with case matching that of the type
+   (e.g. type ``x`` causes ``0x`` and type ``X`` causes ``0X``).
+
+      = ===============
+      b binary
+      o octal
+      x hexadecimal
+      = ===============
+
+
+:arg:`extension`
+   Text (excluding braces) that is passed to the formatting function. This can be used to provide
+   extensions for specific argument types (e.g., IP addresses). The base logic ignores it but passes
+   it on to the formatting function for the corresponding argument type which can then behave
+   different based on the extension.
+
+User Defined Formatting
++++++++++++++++++++++++
+
+When an value needs to be formatted an overloaded function for type :code:`V` is called.
+
+.. code-block:: cpp
+
+   BufferWriter& ts::bwformat(BufferWriter& w, BWFSpec const& spec, V const& v)
+
+This can (and should be) overloaded for user defined types. This makes it easier and cheaper to
+build one overload on another by tweaking the :arg:`spec` as it passed through. The calling
+framework will handle basic alignment, the overload does not need to unless the alignment
+requirements are more detailed (e.g. integer alignment operations).
+
+The output stream operator :code:`operator<<` is defined to call this function with a default
+constructed :code:`BWFSpec` instance.
+
 Futures
 +++++++
 
diff --git a/lib/ts/BufferWriter.h b/lib/ts/BufferWriter.h
index ab6a6fa..71944fa 100644
--- a/lib/ts/BufferWriter.h
+++ b/lib/ts/BufferWriter.h
@@ -27,9 +27,13 @@
 #include <stdlib.h>
 #include <utility>
 #include <cstring>
+#include <vector>
+#include <map>
+#include <ts/ink_std_compat.h>
 
-#include <ts/string_view.h>
+#include <ts/TextView.h>
 #include <ts/ink_assert.h>
+#include <ts/BufferWriterForward.h>
 
 namespace ts
 {
@@ -170,6 +174,22 @@ public:
 
   // Force virtual destructor.
   virtual ~BufferWriter() {}
+
+  /** BufferWriter print.
+
+      This prints its arguments to the @c BufferWriter @a w according to the format @a fmt. The format
+      string is based on Python style formating, each argument substitution marked by braces, {}. Each
+      specification has three parts, a @a name, a @a specifier, and an @a extention. These are
+      separated by colons. The name should be either omitted or a number, the index of the argument to
+      use. If omitted the place in the format string is used as the argument index. E.g. "{} {} {}",
+      "{} {1} {}", and "{0} {1} {2}" are equivalent. Using an explicit index does not reset the
+      position of subsequent substiations, therefore "{} {0} {}" is equivalent to "{0} {0} {2}".
+  */
+  template <typename... Rest> BufferWriter &print(TextView fmt, Rest... rest);
+
+  template <typename... Rest> BufferWriter &print(BWFormat const &fmt, Rest... rest);
+
+  //    bwprint(*this, fmt, std::forward<Rest>(rest)...);
 };
 
 /** A @c BufferWrite concrete subclass to write to a fixed size buffer.
@@ -412,64 +432,253 @@ protected:
   char _arr[N]; ///< output buffer.
 };
 
-// Define stream operators for built in @c write overloads.
+// --------------- Implementation --------------------
+/** Overridable formatting for type @a V.
+
+    This is the output generator for data to a @c BufferWriter. Default stream operators call this with
+    the default format specification (although those can be overloaded specifically for performance).
+    User types should overload this function to format output for that type.
+
+    @code
+      BufferWriter &
+      bwformat(BufferWriter &w, BWFSpec  &, V const &v)
+      {
+        // generate output on @a w
+      }
+    @endcode
+  */
+
+namespace bw_fmt
+{
+  template <typename TUPLE> using ArgFormatterSignature = BufferWriter &(*)(BufferWriter &w, BWFSpec const &, TUPLE const &args);
+
+  /// Internal error / reporting message generators
+  void Err_Bad_Arg_Index(BufferWriter &w, int i, size_t n);
+
+  // MSVC will expand the parameter pack inside a lambda but not gcc, so this indirection is required.
+
+  /// This selects the @a I th argument in the @a TUPLE arg pack and calls the formatter on it. This
+  /// (or the equivalent lambda) is needed because the array of formatters must have a homogenous
+  /// signature, not vary per argument. Effectively this indirection erases the type of the specific
+  /// argument being formatter.
+  template <typename TUPLE, size_t I>
+  BufferWriter &
+  Arg_Formatter(BufferWriter &w, BWFSpec const &spec, TUPLE const &args)
+  {
+    return bwformat(w, spec, std::get<I>(args));
+  }
+
+  /// This exists only to expand the index sequence into an array of formatters for the tuple type
+  /// @a TUPLE.  Due to langauge limitations it cannot be done directly. The formatters can be
+  /// access via standard array access in constrast to templated tuple access. The actual array is
+  /// static and therefore at run time the only operation is loading the address of the array.
+  template <typename TUPLE, size_t... N>
+  ArgFormatterSignature<TUPLE> *
+  Get_Arg_Formatter_Array(std::index_sequence<N...>)
+  {
+    static ArgFormatterSignature<TUPLE> fa[sizeof...(N)] = {&bw_fmt::Arg_Formatter<TUPLE, N>...};
+    return fa;
+  }
+
+  /// Perform alignment adjustments / fill on @a w of the content in @a lw.
+  void Do_Alignment(BWFSpec const &spec, BufferWriter &w, BufferWriter &lw);
+
+  /// Global named argument table.
+  using GlobalSignature = void (*)(BufferWriter &, BWFSpec const &);
+  using GlobalTable     = std::map<string_view, GlobalSignature>;
+  extern GlobalTable BWF_GLOBAL_TABLE;
+  extern GlobalSignature Global_Table_Find(string_view name);
+
+  /// Generic integral conversion.
+  BufferWriter &Format_Integer(BufferWriter &w, BWFSpec const &spec, uintmax_t n, bool negative_p);
+
+} // bw_fmt
+
+/** Compiled BufferWriter format
+ */
+class BWFormat
+{
+public:
+  /// Construct from a format string @a fmt.
+  BWFormat(TextView fmt);
+  ~BWFormat();
+
+  /** Parse elements of a format string.
+
+      @param fmt The format string [in|out]
+      @param literal A literal if found
+      @param spec A specifier if found (less enclosing braces)
+      @return @c true if a specifier was found, @c false if not.
+
+      Pull off the next literal and/or specifier from @a fmt. The return value distinguishes
+      the case of no specifier found (@c false) or an empty specifier (@c true).
+
+   */
+  static bool parse(TextView &fmt, string_view &literal, string_view &spec);
+
+  /** Parsed items from the format string.
+
+      Literals are handled by putting the literal text in the extension field and setting the
+      global formatter @a _gf to @c LiteralFormatter, which writes out the extension as a literal.
+   */
+  struct Item {
+    BWFSpec _spec; ///< Specification.
+    /// If the spec has a global formatter name, cache it here.
+    mutable bw_fmt::GlobalSignature _gf = nullptr;
+
+    Item() {}
+    Item(BWFSpec const &spec, bw_fmt::GlobalSignature gf) : _spec(spec), _gf(gf) {}
+  };
+
+  using Items = std::vector<Item>;
+  Items _items; ///< Items from format string.
+
+protected:
+  /// Handles literals by writing the contents of the extension directly to @a w.
+  static void Format_Literal(BufferWriter &w, BWFSpec const &spec);
+};
+
+template <typename... Rest>
+BufferWriter &
+BufferWriter::print(TextView fmt, Rest... rest)
+{
+  static constexpr int N = sizeof...(Rest);
+  auto args(std::forward_as_tuple(rest...));
+  auto fa     = bw_fmt::Get_Arg_Formatter_Array<decltype(args)>(std::index_sequence_for<Rest...>{});
+  int arg_idx = 0;
+
+  while (fmt.size()) {
+    string_view lit_v;
+    string_view spec_v;
+    bool spec_p = BWFormat::parse(fmt, lit_v, spec_v);
+
+    if (lit_v.size()) {
+      this->write(lit_v);
+    }
+    if (spec_p) {
+      BWFSpec spec{spec_v};
+      size_t width = this->remaining();
+      if (spec._max > 0)
+        width = std::min(width, static_cast<size_t>(spec._max));
+      FixedBufferWriter lw{this->auxBuffer(), width};
+
+      if (spec._name.size() == 0) {
+        spec._idx = arg_idx;
+      }
+      if (0 <= spec._idx) {
+        if (spec._idx < N) {
+          fa[spec._idx](lw, spec, args);
+        } else {
+          bw_fmt::Err_Bad_Arg_Index(lw, spec._idx, N);
+        }
+      } else if (spec._name.size()) {
+        auto gf = bw_fmt::Global_Table_Find(spec._name);
+        if (gf) {
+          gf(lw, spec);
+        } else {
+          static constexpr TextView msg{"{invalid name:"};
+          lw.write(msg).write(spec._name).write('}');
+        }
+      }
+      if (lw.size()) {
+        bw_fmt::Do_Alignment(spec, *this, lw);
+      }
+      ++arg_idx;
+    }
+  }
+  return *this;
+}
+
+template <typename... Rest>
+BufferWriter &
+BufferWriter::print(BWFormat const &fmt, Rest... rest)
+{
+  static constexpr int N = sizeof...(Rest);
+  auto const args(std::forward_as_tuple(rest...));
+  static const auto fa = bw_fmt::Get_Arg_Formatter_Array<decltype(args)>(std::index_sequence_for<Rest...>{});
+
+  for (BWFormat::Item const &item : fmt._items) {
+    size_t width = this->remaining();
+    size_t max   = item._spec._max;
+    if (max && max < width) {
+      width = max;
+    }
+    FixedBufferWriter lw{this->auxBuffer(), width};
+    if (item._gf) {
+      item._gf(lw, item._spec);
+    } else {
+      auto idx = item._spec._idx;
+      if (0 <= idx && idx < N) {
+        fa[idx](lw, item._spec, args);
+      } else if (item._spec._name.size() && (nullptr != (item._gf = bw_fmt::Global_Table_Find(item._spec._name)))) {
+        item._gf(lw, item._spec);
+      }
+    }
+    bw_fmt::Do_Alignment(item._spec, *this, lw);
+  }
+  return *this;
+}
+
+// Generically a stream operator is a formatter with the default specification.
+template <typename V>
+BufferWriter &
+operator<<(BufferWriter &w, V &&v)
+{
+  return bwformat(w, BWFSpec::DEFAULT, std::forward<V>(v));
+}
+
+// -- Common formatters --
+
+BufferWriter &bwformat(BufferWriter &w, BWFSpec const &spec, string_view sv);
 
 inline BufferWriter &
-operator<<(BufferWriter &b, char c)
+bwformat(BufferWriter &w, BWFSpec const &, char c)
 {
-  return b.write(c);
+  return w.write(c);
 }
 
 inline BufferWriter &
-operator<<(BufferWriter &b, const string_view &sv)
+bwformat(BufferWriter &w, BWFSpec const &spec, const char *v)
 {
-  return b.write(sv);
+  return bwformat(w, spec, string_view(v));
 }
 
 inline BufferWriter &
-operator<<(BufferWriter &w, intmax_t i)
+bwformat(BufferWriter &w, BWFSpec const &spec, TextView const &tv)
 {
-  if (i) {
-    char txt[std::numeric_limits<intmax_t>::digits10 + 1];
-    int n = sizeof(txt);
-    while (i) {
-      txt[--n] = '0' + i % 10;
-      i /= 10;
-    }
-    return w.write(txt + n, sizeof(txt) - n);
-  } else {
-    return w.write('0');
-  }
+  return bwformat(w, spec, static_cast<string_view>(tv));
 }
 
-// Annoying but otherwise ambiguous.
+//-- Integral types
 inline BufferWriter &
-operator<<(BufferWriter &w, int i)
+bwformat(BufferWriter &w, BWFSpec const &spec, uintmax_t const &i)
 {
-  return w << static_cast<intmax_t>(i);
+  return bw_fmt::Format_Integer(w, spec, i, false);
 }
 
 inline BufferWriter &
-operator<<(BufferWriter &w, uintmax_t i)
+bwformat(BufferWriter &w, BWFSpec const &spec, intmax_t const &i)
 {
-  if (i) {
-    char txt[std::numeric_limits<uintmax_t>::digits10 + 1];
-    int n = sizeof(txt);
-    while (i) {
-      txt[--n] = '0' + i % 10;
-      i /= 10;
-    }
-    return w.write(txt + n, sizeof(txt) - n);
-  } else {
-    return w.write('0');
-  }
+  return i < 0 ? bw_fmt::Format_Integer(w, spec, -i, true) : bw_fmt::Format_Integer(w, spec, i, false);
+}
+
+inline BufferWriter &
+bwformat(BufferWriter &w, BWFSpec const &spec, unsigned int const &i)
+{
+  return bw_fmt::Format_Integer(w, spec, i, false);
+}
+
+inline BufferWriter &
+bwformat(BufferWriter &w, BWFSpec const &spec, int const &i)
+{
+  return i < 0 ? bw_fmt::Format_Integer(w, spec, -i, true) : bw_fmt::Format_Integer(w, spec, i, false);
 }
 
-// Annoying but otherwise ambiguous.
+// Annoying but otherwise ambiguous with char
 inline BufferWriter &
-operator<<(BufferWriter &w, unsigned int i)
+operator<<(BufferWriter &w, int const &i)
 {
-  return w << static_cast<uintmax_t>(i);
+  return bwformat(w, BWFSpec::DEFAULT, static_cast<intmax_t>(i));
 }
 
 } // end namespace ts
diff --git a/lib/ts/BufferWriterFormat.cc b/lib/ts/BufferWriterFormat.cc
new file mode 100644
index 0000000..f49482a
--- /dev/null
+++ b/lib/ts/BufferWriterFormat.cc
@@ -0,0 +1,517 @@
+/** @file
+
+    Formatted output for BufferWriter.
+
+    @section license License
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ */
+
+#include <ts/BufferWriter.h>
+#include <ctype.h>
+#include <ctime>
+
+namespace
+{
+// Customized version of string to int. Using this instead of the general @c svtoi function
+// made @c bwprint performance test run in < 30% of the time, changing it from about 2.5
+// times slower than snprintf to the same speed. This version handles only positive integers
+// in decimal.
+inline int
+tv_to_positive_decimal(ts::TextView src, ts::TextView *out)
+{
+  int zret = 0;
+
+  if (out) {
+    out->clear();
+  }
+  src.ltrim_if(&isspace);
+  if (src.size()) {
+    const char *start = src.data();
+    const char *limit = start + src.size();
+    while (start < limit && ('0' <= *start && *start <= '9')) {
+      zret = zret * 10 + *start - '0';
+      ++start;
+    }
+    if (out && (start > src.data())) {
+      out->set_view(src.data(), start);
+    }
+  }
+  return zret;
+}
+}
+
+namespace ts
+{
+const ts::BWFSpec ts::BWFSpec::DEFAULT;
+
+/// Parse a format specification.
+BWFSpec::BWFSpec(TextView fmt)
+{
+  TextView num;
+  intmax_t n;
+
+  _name = fmt.take_prefix_at(':');
+  // if it's parsable as a number, treat it as an index.
+  n = tv_to_positive_decimal(_name, &num);
+  if (num.size())
+    _idx = static_cast<decltype(_idx)>(n);
+
+  if (fmt.size()) {
+    TextView sz = fmt.take_prefix_at(':'); // the format specifier.
+    _ext        = fmt;                     // anything past the second ':' is the extension.
+    if (sz.size()) {
+      // fill and alignment
+      if ('%' == *sz) { // enable URI encoding of the fill character so metasyntactic chars can be used if needed.
+        if (sz.size() < 4) {
+          throw std::invalid_argument("Fill URI encoding without 2 hex characters and align mark");
+        }
+        if (Align::NONE == (_align = align_of(sz[3]))) {
+          throw std::invalid_argument("Fill URI without alignment mark");
+        }
+        char d1 = sz[1], d0 = sz[2];
+        if (!isxdigit(d0) || !isxdigit(d1)) {
+          throw std::invalid_argument("URI encoding with non-hex characters");
+        }
+        _fill = isdigit(d0) ? d0 - '0' : tolower(d0) - 'a' + 10;
+        _fill += (isdigit(d1) ? d1 - '0' : tolower(d1) - 'a' + 10) << 4;
+        sz += 4;
+      } else if (sz.size() > 1 && Align::NONE != (_align = align_of(sz[1]))) {
+        _fill = *sz;
+        sz += 2;
+      } else if (Align::NONE != (_align = align_of(*sz))) {
+        ++sz;
+      }
+      if (!sz.size())
+        return;
+      // sign
+      if (is_sign(*sz)) {
+        _sign = *sz;
+        if (!(++sz).size())
+          return;
+      }
+      // radix prefix
+      if ('#' == *sz) {
+        _radix_lead_p = true;
+        if (!(++sz).size())
+          return;
+      }
+      // 0 fill for integers
+      if ('0' == *sz) {
+        if (Align::NONE == _align)
+          _align = Align::SIGN;
+        _fill    = '0';
+        ++sz;
+      }
+      n = tv_to_positive_decimal(sz, &num);
+      if (num.size()) {
+        _min = static_cast<decltype(_min)>(n);
+        sz.remove_prefix(num.size());
+        if (!sz.size())
+          return;
+      }
+      // precision
+      if ('.' == *sz) {
+        n = tv_to_positive_decimal(++sz, &num);
+        if (num.size()) {
+          _prec = static_cast<decltype(_prec)>(n);
+          sz.remove_prefix(num.size());
+          if (!sz.size())
+            return;
+        } else {
+          throw std::invalid_argument("Precision mark without precision");
+        }
+      }
+      // style (type). Hex, octal, etc.
+      if (is_type(*sz)) {
+        _type = *sz;
+        if (!(++sz).size())
+          return;
+      }
+      // maximum width
+      if (',' == *sz) {
+        n = tv_to_positive_decimal(++sz, &num);
+        if (num.size()) {
+          _max = static_cast<decltype(_max)>(n);
+          sz.remove_prefix(num.size());
+          if (!sz.size())
+            return;
+        } else {
+          throw std::invalid_argument("Maximum width mark without width");
+        }
+        // Can only have a type indicator here if there was a max width.
+        if (is_type(*sz)) {
+          _type = *sz;
+          if (!(++sz).size())
+            return;
+        }
+      }
+    }
+  }
+}
+
+namespace bw_fmt
+{
+  GlobalTable BWF_GLOBAL_TABLE;
+
+  void
+  Err_Bad_Arg_Index(BufferWriter &w, int i, size_t n)
+  {
+    static const BWFormat fmt{"{{BAD_ARG_INDEX:{} of {}}}"_sv};
+    w.print(fmt, i, n);
+  }
+
+  /** This performs generic alignment operations.
+
+      If a formatter specialization performs this operation instead, that should result in output that
+      is at least @a spec._min characters wide, which will cause this function to make no further
+      adjustments.
+   */
+  void
+  Do_Alignment(BWFSpec const &spec, BufferWriter &w, BufferWriter &lw)
+  {
+    size_t size = lw.size();
+    size_t min  = spec._min;
+    if (size < min) {
+      size_t delta = min - size; // note - size <= extent -> size < min
+      switch (spec._align) {
+      case BWFSpec::Align::NONE: // same as LEFT for output.
+      case BWFSpec::Align::LEFT:
+        w.fill(size);
+        while (delta--)
+          w.write(spec._fill);
+        break;
+      case BWFSpec::Align::RIGHT:
+        std::memmove(w.auxBuffer() + delta, w.auxBuffer(), size);
+        while (delta--)
+          w.write(spec._fill);
+        w.fill(size);
+        break;
+      case BWFSpec::Align::CENTER:
+        if (delta > 1) {
+          size_t d2 = delta / 2;
+          std::memmove(w.auxBuffer() + (delta / 2), w.auxBuffer(), size);
+          while (d2--)
+            w.write(spec._fill);
+        }
+        w.fill(size);
+        delta = (delta + 1) / 2;
+        while (delta--)
+          w.write(spec._fill);
+        break;
+      case BWFSpec::Align::SIGN:
+        w.fill(size);
+        break;
+      }
+    } else {
+      w.fill(size);
+    }
+  }
+
+  // Conversions from remainder to character, in upper and lower case versions.
+  // Really only useful for hexadecimal currently.
+  namespace
+  {
+    char UPPER_DIGITS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    char LOWER_DIGITS[] = "0123456789abcdefghijklmnopqrstuvwxyz";
+  }
+
+  /// Templated radix based conversions. Only a small number of radix are supported
+  /// and providing a template minimizes cut and paste code while also enabling
+  /// compiler optimizations (e.g. for power of 2 radix the modulo / divide become
+  /// bit operations).
+  template <size_t RADIX>
+  size_t
+  To_Radix(uintmax_t n, char *buff, size_t width, char *digits)
+  {
+    static_assert(1 < RADIX && RADIX <= 36, "RADIX must be in the range 2..36");
+    char *out = buff + width;
+    if (n) {
+      while (n) {
+        *--out = digits[n % RADIX];
+        n /= RADIX;
+      }
+    } else {
+      *--out = '0';
+    }
+    return (buff + width) - out;
+  }
+
+  BufferWriter &
+  Format_Integer(BufferWriter &w, BWFSpec const &spec, uintmax_t i, bool neg_p)
+  {
+    size_t n  = 0;
+    int width = static_cast<int>(spec._min); // amount left to fill.
+    string_view prefix;
+    char neg     = 0;
+    char prefix1 = spec._radix_lead_p ? '0' : 0;
+    char prefix2 = 0;
+    char buff[std::numeric_limits<uintmax_t>::digits + 1];
+
+    if (neg_p) {
+      neg = '-';
+    } else if (spec._sign != '-') {
+      neg = spec._sign;
+    }
+
+    switch (spec._type) {
+    case 'x':
+      prefix2 = 'x';
+      n       = bw_fmt::To_Radix<16>(i, buff, sizeof(buff), bw_fmt::LOWER_DIGITS);
+      break;
+    case 'X':
+      prefix2 = 'X';
+      n       = bw_fmt::To_Radix<16>(i, buff, sizeof(buff), bw_fmt::UPPER_DIGITS);
+      break;
+    case 'b':
+      prefix2 = 'b';
+      n       = bw_fmt::To_Radix<2>(i, buff, sizeof(buff), bw_fmt::LOWER_DIGITS);
+      break;
+    case 'B':
+      prefix2 = 'B';
+      n       = bw_fmt::To_Radix<2>(i, buff, sizeof(buff), bw_fmt::UPPER_DIGITS);
+      break;
+    case 'o':
+      n = bw_fmt::To_Radix<8>(i, buff, sizeof(buff), bw_fmt::LOWER_DIGITS);
+      break;
+    default:
+      prefix1 = 0;
+      n       = bw_fmt::To_Radix<10>(i, buff, sizeof(buff), bw_fmt::LOWER_DIGITS);
+      break;
+    }
+    // Clip fill width by stuff that's already committed to be written.
+    if (neg)
+      --width;
+    if (prefix1) {
+      --width;
+      if (prefix2)
+        --width;
+    }
+    width -= static_cast<int>(n);
+    string_view digits{buff + sizeof(buff) - n, n};
+
+    // The idea here is the various pieces have all been assembled, the only difference
+    // is the order in which they are written to the output.
+    switch (spec._align) {
+    case BWFSpec::Align::LEFT:
+      if (neg)
+        w.write(neg);
+      if (prefix1) {
+        w.write(prefix1);
+        if (prefix2)
+          w.write(prefix2);
+      }
+      w.write(digits);
+      while (width-- > 0)
+        w.write(spec._fill);
+      break;
+    case BWFSpec::Align::RIGHT:
+      while (width-- > 0)
+        w.write(spec._fill);
+      if (neg)
+        w.write(neg);
+      if (prefix1) {
+        w.write(prefix1);
+        if (prefix2)
+          w.write(prefix2);
+      }
+      w.write(digits);
+      break;
+    case BWFSpec::Align::CENTER:
+      for (int i = width / 2; i > 0; --i)
+        w.write(spec._fill);
+      if (neg)
+        w.write(neg);
+      if (prefix1) {
+        w.write(prefix1);
+        if (prefix2)
+          w.write(prefix2);
+      }
+      w.write(digits);
+      for (int i = (width + 1) / 2; i > 0; --i)
+        w.write(spec._fill);
+      break;
+    case BWFSpec::Align::SIGN:
+      if (neg)
+        w.write(neg);
+      if (prefix1) {
+        w.write(prefix1);
+        if (prefix2)
+          w.write(prefix2);
+      }
+      while (width-- > 0)
+        w.write(spec._fill);
+      w.write(digits);
+      break;
+    default:
+      if (neg)
+        w.write(neg);
+      if (prefix1) {
+        w.write(prefix1);
+        if (prefix2)
+          w.write(prefix2);
+      }
+      w.write(digits);
+      break;
+    }
+    return w;
+  }
+
+} // bw_fmt
+
+BufferWriter &
+bwformat(BufferWriter &w, BWFSpec const &spec, string_view sv)
+{
+  int width = static_cast<int>(spec._min); // amount left to fill.
+  if (spec._prec > 0)
+    sv.remove_prefix(spec._prec);
+
+  width -= sv.size();
+  switch (spec._align) {
+  case BWFSpec::Align::LEFT:
+  case BWFSpec::Align::SIGN:
+    w.write(sv);
+    while (width-- > 0)
+      w.write(spec._fill);
+    break;
+  case BWFSpec::Align::RIGHT:
+    while (width-- > 0)
+      w.write(spec._fill);
+    w.write(sv);
+    break;
+  case BWFSpec::Align::CENTER:
+    for (int i = width / 2; i > 0; --i)
+      w.write(spec._fill);
+    w.write(sv);
+    for (int i = (width + 1) / 2; i > 0; --i)
+      w.write(spec._fill);
+    break;
+  default:
+    w.write(sv);
+    break;
+  }
+  return w;
+}
+
+/// Preparse format string for later use.
+BWFormat::BWFormat(ts::TextView fmt)
+{
+  BWFSpec lit_spec{BWFSpec::DEFAULT};
+  int arg_idx = 0;
+
+  while (fmt) {
+    string_view lit_str;
+    string_view spec_str;
+    bool spec_p = this->parse(fmt, lit_str, spec_str);
+
+    if (lit_str.size()) {
+      lit_spec._ext = lit_str;
+      _items.emplace_back(lit_spec, &Format_Literal);
+    }
+    if (spec_p) {
+      bw_fmt::GlobalSignature gf = nullptr;
+      BWFSpec parsed_spec{spec_str};
+      if (parsed_spec._name.size() == 0) {
+        parsed_spec._idx = arg_idx;
+      }
+      if (parsed_spec._idx < 0) {
+        gf = bw_fmt::Global_Table_Find(parsed_spec._name);
+      }
+      _items.emplace_back(parsed_spec, gf);
+      ++arg_idx;
+    }
+  }
+}
+
+BWFormat::~BWFormat()
+{
+}
+
+bool
+BWFormat::parse(ts::TextView &fmt, string_view &literal, string_view &specifier)
+{
+  TextView::size_type off;
+
+  off = fmt.find_if([](char c) { return '{' == c || '}' == c; });
+  if (off == TextView::npos) {
+    literal = fmt;
+    fmt.remove_prefix(literal.size());
+    return false;
+  }
+
+  if (fmt.size() > off + 1) {
+    char c1 = fmt[off];
+    char c2 = fmt[off + 1];
+    if (c1 == c2) {
+      literal = fmt.take_prefix_at(off + 1);
+      return false;
+    } else if ('}' == c1) {
+      throw std::invalid_argument("Unopened }");
+    } else {
+      literal = string_view{fmt.data(), off};
+      fmt.remove_prefix(off + 1);
+    }
+  } else {
+    throw std::invalid_argument("Invalid trailing character");
+  }
+
+  if (fmt.size()) {
+    // Need to be careful, because an empty format is OK and it's hard to tell if
+    // take_prefix_at failed to find the delimiter or found it as the first byte.
+    off = fmt.find('}');
+    if (off == TextView::npos) {
+      throw std::invalid_argument("Unclosed {");
+    }
+    specifier = fmt.take_prefix_at(off);
+    return true;
+  }
+  return false;
+}
+
+void
+BWFormat::Format_Literal(BufferWriter &w, BWFSpec const &spec)
+{
+  w.write(spec._ext);
+}
+
+bw_fmt::GlobalSignature
+bw_fmt::Global_Table_Find(string_view name)
+{
+  if (name.size()) {
+    auto spot = bw_fmt::BWF_GLOBAL_TABLE.find(name);
+    if (spot != bw_fmt::BWF_GLOBAL_TABLE.end())
+      return spot->second;
+  }
+  return nullptr;
+}
+
+} // ts
+
+namespace
+{
+void
+BWF_Now(ts::BufferWriter &w, ts::BWFSpec const &spec)
+{
+  std::time_t t = std::time(nullptr);
+  w.fill(std::strftime(w.auxBuffer(), w.remaining(), "%Y%b%d:%H%M%S", std::localtime(&t)));
+}
+
+static bool BW_INITIALIZED = []() -> bool {
+  ts::bw_fmt::BWF_GLOBAL_TABLE.emplace("now", &BWF_Now);
+  return true;
+}();
+}
diff --git a/lib/ts/BufferWriterForward.h b/lib/ts/BufferWriterForward.h
new file mode 100644
index 0000000..aca64e8
--- /dev/null
+++ b/lib/ts/BufferWriterForward.h
@@ -0,0 +1,102 @@
+/** @file
+
+    Forward definitions for BufferWriter formatting.
+
+    @section license License
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ */
+
+#pragma once
+
+#include <stdlib.h>
+#include <utility>
+#include <cstring>
+#include <vector>
+#include <map>
+#include <ts/ink_std_compat.h>
+
+#include <ts/TextView.h>
+#include <ts/ink_assert.h>
+
+namespace ts
+{
+/** A parsed version of a format specifier.
+ */
+struct BWFSpec {
+  using self_type = BWFSpec; ///< Self reference type.
+  /// Constructor a default instance.
+  constexpr BWFSpec() {}
+
+  /// Construct by parsing @a fmt.
+  BWFSpec(TextView fmt);
+
+  char _fill = ' '; ///< Fill character.
+  char _sign = '-'; ///< Numeric sign style, space + -
+  enum class Align : char {
+    NONE,                           ///< No alignment.
+    LEFT,                           ///< Left alignment '<'.
+    RIGHT,                          ///< Right alignment '>'.
+    CENTER,                         ///< Center alignment '='.
+    SIGN                            ///< Align plus/minus sign before numeric fill. '^'
+  } _align           = Align::NONE; ///< Output field alignment.
+  char _type         = 'g';         ///< Type / radix indicator.
+  bool _radix_lead_p = false;       ///< Print leading radix indication.
+  // @a _min is unsigned because there's no point in an invalid default, 0 works fine.
+  unsigned int _min = 0;  ///< Minimum width.
+  int _prec         = -1; ///< Precision
+  unsigned int _max = 0;  ///< Maxium width
+  int _idx          = -1; ///< Positional "name" of the specification.
+  string_view _name;      ///< Name of the specification.
+  string_view _ext;       ///< Extension if provided.
+
+  static const self_type DEFAULT;
+
+protected:
+  /// Validate character is alignment character and return the appropriate enum value.
+  Align align_of(char c);
+
+  /// Validate is sign indicator.
+  bool is_sign(char c);
+
+  /// Validate @a c is a specifier type indicator.
+  bool is_type(char c);
+};
+
+inline BWFSpec::Align
+BWFSpec::align_of(char c)
+{
+  return '<' == c ? Align::LEFT : '>' == c ? Align::RIGHT : '=' == c ? Align::CENTER : '^' == c ? Align::SIGN : Align::NONE;
+}
+
+inline bool
+BWFSpec::is_sign(char c)
+{
+  return '+' == c || '-' == c || ' ' == c;
+}
+
+inline bool
+BWFSpec::is_type(char c)
+{
+  return 'x' == c || 'X' == c || 'o' == c || 'b' == c || 'B' == c || 'd' == c;
+}
+
+class BWFormat;
+
+class BufferWriter;
+
+} // ts
diff --git a/lib/ts/Makefile.am b/lib/ts/Makefile.am
index 1c70b8b..57e9036 100644
--- a/lib/ts/Makefile.am
+++ b/lib/ts/Makefile.am
@@ -56,6 +56,9 @@ libtsutil_la_SOURCES = \
   BaseLogFile.h \
   Bitops.cc \
   Bitops.h \
+  BufferWriter.h \
+  BufferWriterForward.h \
+  BufferWriterFormat.cc \
   ConsistentHash.cc \
   ConsistentHash.h \
   ContFlags.cc \
@@ -195,7 +198,6 @@ libtsutil_la_SOURCES = \
   SourceLocation.cc \
   SourceLocation.h \
   string_view.h \
-  BufferWriter.h \
   TestBox.h \
   TextBuffer.cc \
   TextBuffer.h \
@@ -261,6 +263,7 @@ test_tslib_LDADD = libtsutil.la $(top_builddir)/iocore/eventsystem/libinkevent.a
 test_tslib_SOURCES = \
 	unit-tests/unit_test_main.cc \
 	unit-tests/test_BufferWriter.cc \
+	unit-tests/test_BufferWriterFormat.cc \
 	unit-tests/test_ink_inet.cc \
 	unit-tests/test_IpMap.cc \
 	unit-tests/test_layout.cc \
diff --git a/lib/ts/ink_std_compat.h b/lib/ts/ink_std_compat.h
index 0b76f6d..b7d37a0 100644
--- a/lib/ts/ink_std_compat.h
+++ b/lib/ts/ink_std_compat.h
@@ -30,6 +30,8 @@
 // C++ 14 compatibility
 //
 #include <memory>
+#include <type_traits>
+
 namespace std
 {
 template <typename T, typename... Args>
@@ -38,6 +40,99 @@ make_unique(Args &&... args)
 {
   return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
 }
+// Local implementation of integer sequence templates from <utility> in C++14.
+// Drop once we move to C++14.
+
+template <typename T, T... N> struct integer_sequence {
+  typedef T value_type;
+  static_assert(std::is_integral<T>::value, "std::integer_sequence requires an integral type");
+
+  static inline std::size_t
+  size()
+  {
+    return (sizeof...(N));
+  }
+};
+
+template <std::size_t... N> using index_sequence = integer_sequence<std::size_t, N...>;
+
+namespace sequence_expander_detail
+{
+  // Expand a sequence (4 ways)
+  template <typename T, std::size_t... _Extra> struct seq_expand;
+
+  template <typename T, T... N, std::size_t... _Extra> struct seq_expand<integer_sequence<T, N...>, _Extra...> {
+    typedef integer_sequence<T, N..., 1 * sizeof...(N) + N..., 2 * sizeof...(N) + N..., 3 * sizeof...(N) + N..., _Extra...> type;
+  };
+
+  template <std::size_t N> struct modulus;
+  template <std::size_t N> struct construct : modulus<N % 4>::template modular_construct<N> {
+  };
+
+  // 4 base cases (e.g. modulo 4)
+  template <> struct construct<0> {
+    typedef integer_sequence<std::size_t> type;
+  };
+  template <> struct construct<1> {
+    typedef integer_sequence<std::size_t, 0> type;
+  };
+  template <> struct construct<2> {
+    typedef integer_sequence<std::size_t, 0, 1> type;
+  };
+  template <> struct construct<3> {
+    typedef integer_sequence<std::size_t, 0, 1, 2> type;
+  };
+
+  // Modulus cases - split 4 ways and pick up the remainder explicitly.
+  template <> struct modulus<0> {
+    template <std::size_t N> struct modular_construct : seq_expand<typename construct<N / 4>::type> {
+    };
+  };
+  template <> struct modulus<1> {
+    template <std::size_t N> struct modular_construct : seq_expand<typename construct<N / 4>::type, N - 1> {
+    };
+  };
+  template <> struct modulus<2> {
+    template <std::size_t N> struct modular_construct : seq_expand<typename construct<N / 4>::type, N - 2, N - 1> {
+    };
+  };
+  template <> struct modulus<3> {
+    template <std::size_t N> struct modular_construct : seq_expand<typename construct<N / 4>::type, N - 3, N - 2, N - 1> {
+    };
+  };
+
+  template <typename T, typename U> struct convert {
+    template <typename> struct result;
+
+    template <T... N> struct result<integer_sequence<T, N...>> {
+      typedef integer_sequence<U, N...> type;
+    };
+  };
+
+  template <typename T> struct convert<T, T> {
+    template <typename U> struct result {
+      typedef U type;
+    };
+  };
+
+  template <typename T, T N>
+  using make_integer_sequence_unchecked = typename convert<std::size_t, T>::template result<typename construct<N>::type>::type;
+
+  template <typename T, T N> struct make_integer_sequence {
+    static_assert(std::is_integral<T>::value, "std::make_integer_sequence can only be instantiated with an integral type");
+    static_assert(0 <= N, "std::make_integer_sequence input shall not be negative");
+
+    typedef make_integer_sequence_unchecked<T, N> type;
+  };
+
+} // namespace sequence_expander_detail
+
+template <typename T, T N> using make_integer_sequence = typename sequence_expander_detail::make_integer_sequence<T, N>::type;
+
+template <std::size_t N> using make_index_sequence = make_integer_sequence<std::size_t, N>;
+
+template <typename... T> using index_sequence_for = make_index_sequence<sizeof...(T)>;
+
 } // namespace std
 #endif // C++ 14 compatibility
 
diff --git a/lib/ts/unit-tests/test_BufferWriter.cc b/lib/ts/unit-tests/test_BufferWriter.cc
index 3d6b1c7..cfccfd2 100644
--- a/lib/ts/unit-tests/test_BufferWriter.cc
+++ b/lib/ts/unit-tests/test_BufferWriter.cc
@@ -21,12 +21,9 @@
     limitations under the License.
  */
 
-#include "BufferWriter.h"
-
 #include "catch.hpp"
-
-#include "string_view.h"
-
+#include <ts/BufferWriter.h>
+#include <ts/string_view.h>
 #include <cstring>
 
 namespace
@@ -333,19 +330,6 @@ TEST_CASE("Discard Buffer Writer", "[BWD]")
   REQUIRE(scratch[0] == '!');
 }
 
-TEST_CASE("Buffer Writer << operator", "[BW<<]")
-{
-  ts::LocalBufferWriter<50> bw;
-
-  bw << "The" << ' ' << "quick" << ' ' << "brown fox";
-
-  REQUIRE(bw.view() == "The quick brown fox");
-
-  bw.reduce(0);
-  bw << "x=" << bw.capacity();
-  REQUIRE(bw.view() == "x=50");
-}
-
 TEST_CASE("LocalBufferWriter clip and extend")
 {
   ts::LocalBufferWriter<10> bw;
diff --git a/lib/ts/unit-tests/test_BufferWriterFormat.cc b/lib/ts/unit-tests/test_BufferWriterFormat.cc
new file mode 100644
index 0000000..2ea8cba
--- /dev/null
+++ b/lib/ts/unit-tests/test_BufferWriterFormat.cc
@@ -0,0 +1,200 @@
+/** @file
+
+    Unit tests for BufferFormat and bwprint.
+
+    @section license License
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+ */
+
+#include "catch.hpp"
+#include <ts/BufferWriter.h>
+#include <chrono>
+#include <iostream>
+
+TEST_CASE("Buffer Writer << operator", "[bufferwriter][stream]")
+{
+  ts::LocalBufferWriter<50> bw;
+
+  bw << "The" << ' ' << "quick" << ' ' << "brown fox";
+
+  REQUIRE(bw.view() == "The quick brown fox");
+
+  bw.reduce(0);
+  bw << "x=" << bw.capacity();
+  REQUIRE(bw.view() == "x=50");
+}
+
+TEST_CASE("bwprint basics", "[bwprint]")
+{
+  ts::LocalBufferWriter<256> bw;
+  ts::string_view fmt1{"Some text"_sv};
+
+  bw.print(fmt1);
+  REQUIRE(bw.view() == fmt1);
+  bw.reduce(0);
+  bw.print("Arg {}", 1);
+  REQUIRE(bw.view() == "Arg 1");
+  bw.reduce(0);
+  bw.print("arg 1 {1} and 2 {2} and 0 {0}", "zero", "one", "two");
+  REQUIRE(bw.view() == "arg 1 one and 2 two and 0 zero");
+  bw.reduce(0);
+  bw.print("args {2}{0}{1}", "zero", "one", "two");
+  REQUIRE(bw.view() == "args twozeroone");
+  bw.reduce(0);
+  bw.print("left |{:<10}|", "text");
+  REQUIRE(bw.view() == "left |text      |");
+  bw.reduce(0);
+  bw.print("right |{:>10}|", "text");
+  REQUIRE(bw.view() == "right |      text|");
+  bw.reduce(0);
+  bw.print("right |{:.>10}|", "text");
+  REQUIRE(bw.view() == "right |......text|");
+  bw.reduce(0);
+  bw.print("center |{:.=10}|", "text");
+  REQUIRE(bw.view() == "center |...text...|");
+  bw.reduce(0);
+  bw.print("center |{:.=11}|", "text");
+  REQUIRE(bw.view() == "center |...text....|");
+  bw.reduce(0);
+  bw.print("center |{:==10}|", "text");
+  REQUIRE(bw.view() == "center |===text===|");
+  bw.reduce(0);
+  bw.print("center |{:%3A=10}|", "text");
+  REQUIRE(bw.view() == "center |:::text:::|");
+  bw.reduce(0);
+  bw.print("left >{0:<9}< right >{0:>9}< center >{0:=9}<", 956);
+  REQUIRE(bw.view() == "left >956      < right >      956< center >   956   <");
+
+  bw.reduce(0);
+  bw.print("Format |{:>#010x}|", -956);
+  REQUIRE(bw.view() == "Format |0000-0x3bc|");
+  bw.reduce(0);
+  bw.print("Format |{:<#010x}|", -956);
+  REQUIRE(bw.view() == "Format |-0x3bc0000|");
+  bw.reduce(0);
+  bw.print("Format |{:#010x}|", -956);
+  REQUIRE(bw.view() == "Format |-0x00003bc|");
+
+  bw.reduce(0);
+  bw.print("{{BAD_ARG_INDEX:{} of {}}}", 17, 23);
+  REQUIRE(bw.view() == "{BAD_ARG_INDEX:17 of 23}");
+
+  bw.reduce(0);
+  bw.print("Arg {0} Arg {3}", 1, 2);
+  REQUIRE(bw.view() == "Arg 1 Arg {BAD_ARG_INDEX:3 of 2}");
+
+  bw.reduce(0);
+  bw.print("{{stuff}} Arg {0} Arg {}", 1, 2);
+  REQUIRE(bw.view() == "{stuff} Arg 1 Arg 2");
+  bw.reduce(0);
+  bw.print("Arg {0} Arg {} and {{stuff}}", 3, 4);
+  REQUIRE(bw.view() == "Arg 3 Arg 4 and {stuff}");
+  bw.reduce(0);
+  bw.print("Arg {{{0}}} Arg {} and {{stuff}}", 5, 6);
+  REQUIRE(bw.view() == "Arg {5} Arg 6 and {stuff}");
+  bw.reduce(0);
+  bw.print("Arg {0} Arg {{}}{{}} {} and {{stuff}}", 7, 8);
+  REQUIRE(bw.view() == "Arg 7 Arg {}{} 8 and {stuff}");
+  bw.reduce(0);
+  bw.print("Arg {0} Arg {{{{}}}} {}", 9, 10);
+  REQUIRE(bw.view() == "Arg 9 Arg {{}} 10");
+
+  bw.reduce(0);
+  bw.print("Arg {0} Arg {{{{}}}} {}", 9, 10);
+  REQUIRE(bw.view() == "Arg 9 Arg {{}} 10");
+  bw.reduce(0);
+  bw.print("Time is {now}");
+  //  REQUIRE(bw.view() == "Time is");
+}
+
+TEST_CASE("BWFormat", "[bwprint][bwformat]")
+{
+  ts::LocalBufferWriter<256> bw;
+  ts::BWFormat fmt("left >{0:<9}< right >{0:>9}< center >{0:=9}<");
+  ts::string_view text{"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
+
+  bw.reduce(0);
+  static const ts::BWFormat bad_arg_fmt{"{{BAD_ARG_INDEX:{} of {}}}"};
+  bw.print(bad_arg_fmt, 17, 23);
+  REQUIRE(bw.view() == "{BAD_ARG_INDEX:17 of 23}");
+
+  bw.reduce(0);
+  bw.print(fmt, 956);
+  REQUIRE(bw.view() == "left >956      < right >      956< center >   956   <");
+
+  bw.reduce(0);
+  bw.print("Text: _{0:.10,20}_", text);
+  REQUIRE(bw.view() == "Text: _abcdefghijklmnopqrst_");
+  bw.reduce(0);
+  bw.print("Text: _{0:-<20.52,20}_", text);
+  REQUIRE(bw.view() == "Text: _QRSTUVWXYZ----------_");
+}
+
+// Normally there's no point in running the performance tests, but it's worth keeping the code
+// for when additional testing needs to be done.
+#if 0
+TEST_CASE("bwperf", "[bwprint][performance]")
+{
+  // Force these so I can easily change the set of tests.
+  auto start            = std::chrono::high_resolution_clock::now();
+  auto delta = std::chrono::high_resolution_clock::now() - start;
+  constexpr int N_LOOPS = 1000000;
+
+  ts::string_view text{"Format |"};
+  ts::LocalBufferWriter<256> bw;
+
+  ts::BWFSpec spec;
+
+  start = std::chrono::high_resolution_clock::now();
+  for (int i = 0; i < N_LOOPS; ++i) {
+    bw.reduce(0);
+    bw.print( "Format |{:#010x}|", -956);
+  }
+  delta = std::chrono::high_resolution_clock::now() - start;
+  std::cout << "BW Timing is " << delta.count() << "ns or " << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count()
+            << "ms" << std::endl;
+
+  start = std::chrono::high_resolution_clock::now();
+  for (int i = 0; i < N_LOOPS; ++i) {
+    bw.reduce(0);
+    bw.print("Format |{:#010x}|", -956);
+  }
+  delta = std::chrono::high_resolution_clock::now() - start;
+  std::cout << "bw.print() " << delta.count() << "ns or " << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count()
+            << "ms" << std::endl;
+
+  ts::BWFormat fmt("Format |{:#010x}|");
+  start = std::chrono::high_resolution_clock::now();
+  for (int i = 0; i < N_LOOPS; ++i) {
+    bw.reduce(0);
+    bw.print( fmt, -956);
+  }
+  delta = std::chrono::high_resolution_clock::now() - start;
+  std::cout << "Preformatted: " << delta.count() << "ns or "
+            << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count() << "ms" << std::endl;
+
+  char buff[256];
+  start = std::chrono::high_resolution_clock::now();
+  for (int i = 0; i < N_LOOPS; ++i) {
+    snprintf(buff, sizeof(buff), "Format |%#0x10|", -956);
+  }
+  delta = std::chrono::high_resolution_clock::now() - start;
+  std::cout << "snprint Timing is " << delta.count() << "ns or "
+            << std::chrono::duration_cast<std::chrono::milliseconds>(delta).count() << "ms" << std::endl;
+}
+#endif

-- 
To stop receiving notification emails like this one, please contact
amc@apache.org.