diff --git a/doc/developer-guide/internal-libraries/MemSpan.en.rst b/doc/developer-guide/internal-libraries/MemSpan.en.rst index ce5f7df93f5..a9b6775e2ac 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/TextView.en.rst b/doc/developer-guide/internal-libraries/TextView.en.rst index 58641c05985..8cb00d4c0ec 100644 --- a/doc/developer-guide/internal-libraries/TextView.en.rst +++ b/doc/developer-guide/internal-libraries/TextView.en.rst @@ -53,7 +53,7 @@ In deciding between :class:`string_view` and :class:`TextView` remember that the cheaply cross convert. In general if the string is treated as a block of data, :class:`string_view` is better. If the contents of the string are to be examined / parsed non-uniformly then :class:`TextView` is better. For example, if the string is used simply as a key or a hash source, -use :class:`string_view`. Or, if the string may contain substrings of interests such as key / value +use :class:`string_view`. Or, if the string may contain sustrings of interests such as key / value pairs, then use a :class:`TextView`. :class:`TextView` provides a variety of methods for manipulating the view as a string. These are diff --git a/doc/developer-guide/internal-libraries/buffer-writer.en.rst b/doc/developer-guide/internal-libraries/buffer-writer.en.rst index d72d7baafca..7e05547721d 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: * + +: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: | 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 ab6a6fa02b4..5f5e7bd9fb5 100644 --- a/lib/ts/BufferWriter.h +++ b/lib/ts/BufferWriter.h @@ -27,9 +27,13 @@ #include #include #include +#include +#include +#include -#include +#include #include +#include namespace ts { @@ -170,6 +174,22 @@ class BufferWriter // 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 BufferWriter &print(TextView fmt, Rest... rest); + + template BufferWriter &print(BWFormat const &fmt, Rest... rest); + + // bwprint(*this, fmt, std::forward(rest)...); }; /** A @c BufferWrite concrete subclass to write to a fixed size buffer. @@ -412,64 +432,281 @@ template class LocalBufferWriter : public FixedBufferWriter 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 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 + BufferWriter & + Arg_Formatter(BufferWriter &w, BWFSpec const &spec, TUPLE const &args) + { + return bwformat(w, spec, std::get(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 + ArgFormatterSignature * + Get_Arg_Formatter_Array(std::index_sequence) + { + static ArgFormatterSignature fa[sizeof...(N)] = {&bw_fmt::Arg_Formatter...}; + 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; + 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); + + /// Generic floating conversion. + BufferWriter &Format_Floating(BufferWriter &w, BWFSpec const &spec, double 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; + 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 +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(std::index_sequence_for{}); + 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(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 +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(std::index_sequence_for{}); + + 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 +BufferWriter & +operator<<(BufferWriter &w, V &&v) +{ + return bwformat(w, BWFSpec::DEFAULT, std::forward(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::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(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(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::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 with char +inline BufferWriter & +operator<<(BufferWriter &w, int const &i) +{ + return bwformat(w, BWFSpec::DEFAULT, static_cast(i)); +} + +//-- Floating type(s) +inline BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, double const &d) +{ + return d < 0 ? bw_fmt::Format_Floating(w, spec, -d, true) : bw_fmt::Format_Floating(w, spec, d, false); +} + +inline BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, float const &f) +{ + return f < 0 ? bw_fmt::Format_Floating(w, spec, -f, true) : bw_fmt::Format_Floating(w, spec, f, false); +} + +inline BufferWriter & +operator<<(BufferWriter &w, double const &d) +{ + return bwformat(w, BWFSpec::DEFAULT, d); } -// Annoying but otherwise ambiguous. inline BufferWriter & -operator<<(BufferWriter &w, unsigned int i) +operator<<(BufferWriter &w, float const &f) { - return w << static_cast(i); + return bwformat(w, BWFSpec::DEFAULT, f); } } // end namespace ts diff --git a/lib/ts/BufferWriterFormat.cc b/lib/ts/BufferWriterFormat.cc new file mode 100644 index 00000000000..4ef58e889a4 --- /dev/null +++ b/lib/ts/BufferWriterFormat.cc @@ -0,0 +1,584 @@ +#include +#include +#include +#include + +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(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(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(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(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 +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(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::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(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; +} + +/// Format for floating point values. Seperates floating point into a whole number and a +/// fraction. The fraction is converted into an unsigned integer based on the specified +/// precision, spec._prec. ie. 3.1415 with precision two is seperated into two unsigned +/// integers 3 and 14. The different pieces are assembled and placed into the BufferWriter. +/// The default is two decimal places. ie. X.XX. The value is always written in base 10. +/// +/// format: whole.fraction +/// or: left.right +BufferWriter & +Format_Floating(BufferWriter &w, BWFSpec const &spec, double f, bool neg_p) +{ + if(f == static_cast(f)) { //integral + return Format_Integer(w, spec, static_cast(f), neg_p); + } + + double left, right; + size_t l = 0; + size_t r = 0; + char whole[std::numeric_limits::digits + 1]; + char fraction[std::numeric_limits::digits + 1]; + char dec = '.'; + char neg = 0; + int width = static_cast(spec._min); // amount left to fill. + int precision = (spec._prec == BWFSpec::DEFAULT._prec) ? 2 : spec._prec; // default precision 2 + + right = modf(f, &left); // split the number + + if (neg_p) { + neg = '-'; + } else if (spec._sign != '-') { + neg = spec._sign; + } + + // Convert trailing fraction into an integer value. + // Final precision point is always rounded. + uint64_t shift = 1; + for(int i = 0; i < precision; ++i) { + shift *= 10; + } + uint64_t right_p = static_cast(right * shift + 0.5 /* rounding */); + + l = bw_fmt::To_Radix<10>(left, whole, sizeof(whole), bw_fmt::LOWER_DIGITS); + r = bw_fmt::To_Radix<10>(right_p, fraction, sizeof(fraction), bw_fmt::LOWER_DIGITS); + + // Clip fill width + if (neg) + --width; + width -= static_cast(l); + width -= 1; // '.' + width -= static_cast(r); + + string_view whole_digits{whole + sizeof(whole) - l, l}; + string_view frac_digits{fraction + sizeof(fraction) - r, r}; + + // 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. + #define FLUSH_NUM(w, integral, fraction) \ + w.write(integral); \ + w.write(dec); \ + w.write(fraction); + + switch (spec._align) { + case BWFSpec::Align::LEFT: + if (neg) + w.write(neg); + FLUSH_NUM(w, whole_digits, frac_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); + FLUSH_NUM(w, whole_digits, frac_digits); + break; + case BWFSpec::Align::CENTER: + for (int i = width / 2; i > 0; --i) + w.write(spec._fill); + if (neg) + w.write(neg); + FLUSH_NUM(w, whole_digits, frac_digits); + for (int i = (width + 1) / 2; i > 0; --i) + w.write(spec._fill); + break; + case BWFSpec::Align::SIGN: + if (neg) + w.write(neg); + while (width-- > 0) + w.write(spec._fill); + FLUSH_NUM(w, whole_digits, frac_digits); + break; + default: + if (neg) + w.write(neg); + FLUSH_NUM(w, whole_digits, frac_digits); + break; + } + return w; +} + +} // bw_fmt + +BufferWriter & +bwformat(BufferWriter &w, BWFSpec const &spec, string_view sv) { + int width = static_cast(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 00000000000..aca64e8f440 --- /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 +#include +#include +#include +#include +#include + +#include +#include + +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 1c70b8b96e4..f87bd10d20f 100644 --- a/lib/ts/Makefile.am +++ b/lib/ts/Makefile.am @@ -56,6 +56,10 @@ libtsutil_la_SOURCES = \ BaseLogFile.h \ Bitops.cc \ Bitops.h \ + BufferWriter.h \ + BufferWriterForward.h \ + BufferWriterFormat.cc \ + c14_utility.h \ ConsistentHash.cc \ ConsistentHash.h \ ContFlags.cc \ @@ -195,7 +199,6 @@ libtsutil_la_SOURCES = \ SourceLocation.cc \ SourceLocation.h \ string_view.h \ - BufferWriter.h \ TestBox.h \ TextBuffer.cc \ TextBuffer.h \ @@ -261,6 +264,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 ca972a6ebe3..e62d36e0e7f 100644 --- a/lib/ts/ink_std_compat.h +++ b/lib/ts/ink_std_compat.h @@ -25,6 +25,8 @@ #if __cplusplus < 201402L #include +#include + namespace std { template @@ -33,5 +35,99 @@ make_unique(Args &&... args) { return std::unique_ptr(new T(std::forward(args)...)); } -} + +// Local implementation of integer sequence templates from in C++14. +// Drop once we move to C++14. + +template struct integer_sequence { + typedef T value_type; + static_assert(std::is_integral::value, "std::integer_sequence requires an integral type"); + + static inline std::size_t + size() + { + return (sizeof...(N)); + } +}; + +template using index_sequence = integer_sequence; + +namespace sequence_expander_detail +{ + // Expand a sequence (4 ways) + template struct seq_expand; + + template struct seq_expand, _Extra...> { + typedef integer_sequence type; + }; + + template struct modulus; + template struct construct : modulus::template modular_construct { + }; + + // 4 base cases (e.g. modulo 4) + template <> struct construct<0> { + typedef integer_sequence type; + }; + template <> struct construct<1> { + typedef integer_sequence type; + }; + template <> struct construct<2> { + typedef integer_sequence type; + }; + template <> struct construct<3> { + typedef integer_sequence type; + }; + + // Modulus cases - split 4 ways and pick up the remainder explicitly. + template <> struct modulus<0> { + template struct modular_construct : seq_expand::type> { + }; + }; + template <> struct modulus<1> { + template struct modular_construct : seq_expand::type, N - 1> { + }; + }; + template <> struct modulus<2> { + template struct modular_construct : seq_expand::type, N - 2, N - 1> { + }; + }; + template <> struct modulus<3> { + template struct modular_construct : seq_expand::type, N - 3, N - 2, N - 1> { + }; + }; + + template struct convert { + template struct result; + + template struct result> { + typedef integer_sequence type; + }; + }; + + template struct convert { + template struct result { + typedef U type; + }; + }; + + template + using make_integer_sequence_unchecked = typename convert::template result::type>::type; + + template struct make_integer_sequence { + static_assert(std::is_integral::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 type; + }; + +} // namespace sequence_expander_detail + +template using make_integer_sequence = typename sequence_expander_detail::make_integer_sequence::type; + +template using make_index_sequence = make_integer_sequence; + +template using index_sequence_for = make_index_sequence; + +} // std #endif diff --git a/lib/ts/unit-tests/test_BufferWriter.cc b/lib/ts/unit-tests/test_BufferWriter.cc index 3d6b1c7caa7..cfccfd26e7a 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 +#include #include 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 00000000000..6af1eb08ae4 --- /dev/null +++ b/lib/ts/unit-tests/test_BufferWriterFormat.cc @@ -0,0 +1,353 @@ +/** @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 +#include +#include + +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; + auto 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----------_"); +} + +TEST_CASE("BWFormate integral", "[bwprint][bwformat]") +{ + ts::LocalBufferWriter<256> bw; + ts::BWFSpec spec; + uint32_t num = 30; + int num_neg = -30; + + //basic + bwformat(bw, spec, num); + REQUIRE(bw.view() == "30"); + bw.reduce(0); + bwformat(bw, spec, num_neg); + REQUIRE(bw.view() == "-30"); + bw.reduce(0); + + //radix + ts::BWFSpec spec_hex; + spec_hex._radix_lead_p = true; + spec_hex._type = 'x'; + bwformat(bw, spec_hex, num); + REQUIRE(bw.view() == "0x1e"); + bw.reduce(0); + + ts::BWFSpec spec_dec; + spec_dec._type ='0'; + bwformat(bw, spec_dec, num); + REQUIRE(bw.view() == "30"); + bw.reduce(0); + + ts::BWFSpec spec_bin; + spec_bin._radix_lead_p = true; + spec_bin._type = 'b'; + bwformat(bw, spec_bin, num); + REQUIRE(bw.view() == "0b11110"); + bw.reduce(0); + + int one = 1; + int two = 2; + int three_n = -3; + //alignment + ts::BWFSpec left; + left._align = ts::BWFSpec::Align::LEFT; + left._min = 5; + ts::BWFSpec right; + right._align = ts::BWFSpec::Align::RIGHT; + right._min = 5; + ts::BWFSpec center; + center._align = ts::BWFSpec::Align::CENTER; + center._min = 5; + + bwformat(bw, left, one); + bwformat(bw, right, two); + REQUIRE(bw.view() == "1 2"); + bwformat(bw, right, two); + REQUIRE(bw.view() == "1 2 2"); + bwformat(bw, center, three_n); + REQUIRE(bw.view() == "1 2 2 -3 "); +} + +TEST_CASE("BWFormat floating", "[bwprint][bwformat]") +{ + ts::LocalBufferWriter<256> bw; + ts::BWFSpec spec; + + double n = 180.278; + double neg = -238.47; + bwformat(bw, spec, n); + REQUIRE(bw.view() == "180.28"); + bw.reduce(0); + bwformat(bw, spec, neg); + REQUIRE(bw.view() == "-238.47"); + bw.reduce(0); + + spec._prec = 5; + bwformat(bw, spec, n); + REQUIRE(bw.view() == "180.27800"); + bw.reduce(0); + bwformat(bw, spec, neg); + REQUIRE(bw.view() == "-238.47000"); + bw.reduce(0); + + float f = 1234; + float fneg = -1; + bwformat(bw, spec, f); + REQUIRE(bw.view() == "1234"); + bw.reduce(0); + bwformat(bw, spec, fneg); + REQUIRE(bw.view() == "-1"); + bw.reduce(0); + f = 1234.5667; + spec._prec = 4; + bwformat(bw, spec, f); + REQUIRE(bw.view() == "1234.5667"); + bw.reduce(0); + + bw << 1234 << .567; + REQUIRE(bw.view() == "12340.57"); + bw.reduce(0); + bw << f; + REQUIRE(bw.view() == "1234.57"); + bw.reduce(0); + bw << n; + REQUIRE(bw.view() == "180.28"); + bw.reduce(0); + bw << f << n; + REQUIRE(bw.view() == "1234.57180.28"); + bw.reduce(0); + + + double edge = 0.345; + spec._prec = 3; + bwformat(bw, spec, edge); + REQUIRE(bw.view() == "0.345"); + bw.reduce(0); + edge = .1234; + bwformat(bw, spec, edge); + REQUIRE(bw.view() == "0.123"); + bw.reduce(0); + edge = 1.0; + bwformat(bw, spec, edge); + REQUIRE(bw.view() == "1"); + bw.reduce(0); + + //alignment + double first = 1.23; + double second = 2.35; + double third = -3.5; + ts::BWFSpec left; + left._align = ts::BWFSpec::Align::LEFT; + left._min = 5; + ts::BWFSpec right; + right._align = ts::BWFSpec::Align::RIGHT; + right._min = 5; + ts::BWFSpec center; + center._align = ts::BWFSpec::Align::CENTER; + center._min = 5; + + bwformat(bw, left, first); + bwformat(bw, right, second); + REQUIRE(bw.view() == "1.23 2.35"); + bwformat(bw, right, second); + REQUIRE(bw.view() == "1.23 2.35 2.35"); + bwformat(bw, center, third); + REQUIRE(bw.view() == "1.23 2.35 2.35-3.50"); + bw.reduce(0); + + double over = 1.4444444; + ts::BWFSpec over_min; + over_min._prec = 7; + over_min._min = 5; + bwformat(bw, over_min, over); + REQUIRE(bw.view() == "1.4444444"); + bw.reduce(0); +} + +#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(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(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(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(delta).count() << "ms" << std::endl; +} +#endif