/* * Licensed 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. * * Copyright 2017 Danny Robson */ #ifndef CRUFT_UTIL_FORMAT_HPP #define CRUFT_UTIL_FORMAT_HPP #include "view.hpp" #include #include #include #include #include #include #include namespace util::format { /// denotes the stated data type of one specifier enum class type_t { /// an internal type that indicates a span of literal text to copy /// from the format specifier string LITERAL, /// a type that has implemented an ostream operator USER, /// a literal '%' symbol ESCAPE, /// numeric types SIGNED, UNSIGNED, REAL, /// a C style string, or equivalent C++ type (std::string, /// std::string_view, util::view, etc, ...) STRING, /// a single character CHAR, /// a raw pointer (rather than value that needs to be dereferenced) POINTER, /// number of characters written COUNT }; /// formatting information for a single specifier. /// /// TODO: investigate using a proper tagged union to compress the data size struct specifier { /// the sub-region of the format specifier that we parsed for this /// information. probably only useful for the LITERAL type as we just /// copy the view into the output buffer directly. util::view fmt = util::view {nullptr}; int parameter = -1; struct { bool plus = false; bool minus = false; bool space = false; bool zero = false; bool hash = false; } flags; int width = -1; int precision = -1; int length = -1; type_t type = type_t::USER; bool upper = false; int base = 10; enum { FIXED, SCIENTIFIC, DEFAULT, HEX, } representation = DEFAULT; }; struct parsed; template class bound; template class stored; /// a sequence of parsed specifiers that can be used to render some /// collection parameters in the future. struct parsed { std::vector m_specifiers; auto begin (void) const { return std::begin (m_specifiers); } auto end (void) const { return std::end (m_specifiers); } /// records a complete collection of parameters for rendering in the /// future. the caller must maintain the 'parsed' object for the /// lifetime of the return value. template bound operator () (const Args &...args) &; /// records a complete collection of parameters for rendering in the /// future. takes ownership of the specifiers so there is no lifetime /// requirement. template stored operator () (const Args &...args) &&; }; /// parameter collection for a non-owning sequence of specifiers template class bound { public: bound (const parsed &_parsed, const ValueT &...args): m_parsed {_parsed}, m_values {args...} { ; } auto specifiers (void) const { return util::make_view (m_parsed.m_specifiers); } template auto get (void) const& { return std::get (m_values); } private: const parsed &m_parsed; std::tuple m_values; }; /// parameter collection for an owning squence of specifiers. template class stored { public: stored (std::vector &&_specifiers, const ValueT &...args): m_specifiers {std::move (_specifiers)}, m_values {args...} { ; } auto specifiers (void) const& { return util::make_view (m_specifiers); } template const auto& get (void) const& { return std::get (m_values); } private: std::vector m_specifiers; std::tuple m_values; }; template bound parsed::operator () (const Args &...args) & { return bound { *this, args... }; } template stored parsed::operator () (const Args &...args) && { return stored { std::move (m_specifiers), args... }; } /// parses a format string in the style of std::printf /// /// if the format specifier is invalid the function will throw an error at /// runtime. specifically does not make allowances for constexpr /// validation. parsed printf (util::view); /// parses a format specifier in the style of PEP3101 (with the notable /// exception of named parameters). /// /// in the event of a parsing error the function will throw. makes no /// attempt to cater for constexpr validation. parsed python (util::view); /// parses a printf format string and binds parameters for rendering. template auto printf (util::view fmt, const Args &...args) { return printf (fmt) (args...); } /// parses a python format string and binds parameters for rendering. template auto python (util::view fmt, const Args &...args) { return python (fmt) (args...); } template struct value { static std::ostream& write (std::ostream &os, specifier spec, const ValueT &val) { os << std::resetiosflags (~std::ios_base::fmtflags{}); switch (spec.type) { case type_t::REAL: if (!std::is_floating_point_v) throw std::runtime_error ("expected real value"); break; case type_t::UNSIGNED: if (!std::is_unsigned_v) throw std::runtime_error ("expected unsigned value"); break; case type_t::SIGNED: if (!std::is_signed_v) throw std::runtime_error ("expected signed value"); break; case type_t::STRING: if (!std::is_same_v> && !std::is_same_v) throw std::runtime_error ("expected string value"); break; case type_t::POINTER: if (!std::is_pointer_v && !std::is_integral_v) throw std::runtime_error ("expected pointer value"); break; case type_t::CHAR: if (!std::is_same_v && !std::is_same_v && !std::is_same_v && !std::is_same_v && !std::is_same_v && !std::is_same_v) throw std::runtime_error ("expected character value"); break; case type_t::COUNT: if (!std::is_pointer_v && !std::is_reference_v) if (!std::is_integral_v>>) throw std::runtime_error ("expected pointer/reference to integral"); break; case type_t::USER: break; case type_t::ESCAPE: case type_t::LITERAL: break; } // easy case where we just throw it to ostream if (spec.type == type_t::USER) return os << val; if (spec.length > 0 && sizeof (val) != spec.length) throw std::runtime_error ("mismatched argument size"); const bool uses_space = std::is_arithmetic_v && spec.flags.space && !spec.flags.plus; if (uses_space) os << ' '; if (spec.flags.plus) os << std::showpos; if (spec.flags.minus) os << std::left; if (spec.flags.zero) os << std::setfill ('0'); if (spec.base >= 0) { switch (spec.base) { case 10: os << std::dec; break; case 16: os << std::hex; break; case 8: os << std::oct; break; default: throw std::runtime_error ("unhandled numeric base"); } } if (spec.precision >= 0) { os << std::setprecision (spec.precision); } if (spec.width >= 0) os << std::setw (spec.width - (uses_space ? 1 : 0)); if (spec.upper) os << std::uppercase; if (spec.type == type_t::UNSIGNED || spec.type == type_t::SIGNED) if (spec.flags.hash) os << std::showbase; if (spec.type == type_t::REAL) if (spec.flags.hash) os << std::showpoint; if (spec.type == type_t::REAL) { switch (spec.representation) { case specifier::FIXED: os << std::fixed; break; case specifier::SCIENTIFIC: os << std::scientific; break; case specifier::DEFAULT: os << std::defaultfloat; break; case specifier::HEX: os << std::hexfloat; break; } } if constexpr (std::is_integral_v) { if (spec.type == type_t::POINTER) { if (!val) return os << "(nil)"; return os << reinterpret_cast (val); } } if constexpr (std::is_floating_point_v) { if (spec.type == type_t::REAL) { if (std::isnan (val)) return os << (spec.upper ? "NAN" : "nan"); if (std::isinf (val)) return os << (spec.upper ? "INF" : "inf"); } } if constexpr (std::is_integral_v) { if (spec.type == type_t::SIGNED || spec.type == type_t::UNSIGNED) { // explicitly handle the zero width case as blank because // there's no easy way to do this using iomanip. if (spec.precision == 0 && !val) { return os; } } } if constexpr (std::is_same_v, ValueT>) { if (spec.precision >= 0) { std::copy_n ( std::begin (val), util::min (spec.precision, static_cast (val.size ())), std::ostream_iterator (os) ); return os; } } // the final output calls. we need to use unary plus so that // chars get promoted to ints for correct stream rendering when // the intention is to output a number. if constexpr (std::is_fundamental_v) { if (spec.type == type_t::CHAR) return os << val; if constexpr (!std::is_null_pointer_v) if (spec.type != type_t::USER) return os << +val; } return os << val; } }; template struct value { static std::ostream& write (std::ostream &os, specifier spec, const ValueT *val) { if (spec.type != type_t::POINTER && spec.type != type_t::USER) throw std::runtime_error ("expected pointer specification"); if (!val) return os << "(nil)"; return os << reinterpret_cast (val); } }; template struct value { static std::ostream& write (std::ostream &os, specifier spec, ValueT *val) { return value::write (os, spec, val); } }; template <> struct value { static std::ostream& write (std::ostream &os, specifier s, const std::nullptr_t &val) { if (s.type != type_t::POINTER || s.type == type_t::USER) throw std::runtime_error ("expected pointer specifier"); return value::write (os, s, val); } }; template struct value { static std::ostream& write (std::ostream &os, specifier spec, const char (&val)[N]) { if (spec.type == type_t::STRING || spec.type == type_t::USER) return value>::write (os, spec, util::view (val)); throw std::runtime_error ("invalid data type"); } }; template struct value { static std::ostream& write (std::ostream &os, specifier spec, const char (&val)[N]) { return value>::write (os, spec, util::view (val)); } }; template <> struct value { static std::ostream& write (std::ostream &os, specifier spec, char *val) { if (!val) return os << "(nil)"; if (spec.type == type_t::STRING || spec.type == type_t::USER) return value>::write (os, spec, util::view { val, val + strlen (val) }); if (spec.type == type_t::POINTER) return value::write (os, spec, val); throw std::runtime_error ("invalid data type"); } }; template <> struct value { static std::ostream& write (std::ostream &os, specifier spec, const char *val) { if (!val) return os << "(nil)"; if (spec.type == type_t::STRING || spec.type == type_t::USER) return value>::write (os, spec, util::view { val, val + strlen (val) }); if (spec.type == type_t::POINTER) return value::write (os, spec, val); throw std::runtime_error ("invalid data type"); } }; template <> struct value { static std::ostream& write (std::ostream &os, specifier spec, const std::string &val) { return value>::write ( os, spec, util::view (val.data (), val.data () + val.size ()) ); } }; template <> struct value { static std::ostream& write (std::ostream &os, specifier spec, std::string &val) { return value::write (os, spec, val); } }; /// renders an LITERAL specifiers followed by one parameter, then /// recurses for any following specifiers. /// /// \tparam Index the index of the next parameter to render /// \tparam SpecifierT a forward iterator container /// \tparam DataT a tuple-like object template class /// \tparam Args a paramater pack of all parameter types /// /// \param os the ostream that we render to /// \param specifiers the sequence of all specifiers to be rendered /// \param data a tuple-like object containing references to all parameters template class HolderT, typename ...DataT> static std::ostream& write (std::ostream &os, const SpecifiersT &specifiers, const HolderT &data) { for (auto cursor = std::cbegin (specifiers); cursor != std::cend (specifiers); ++cursor) { const auto &s = *cursor; if (s.type == type_t::LITERAL) { std::copy (std::begin (s.fmt), std::end (s.fmt), std::ostream_iterator (os)); continue; } if (s.type == type_t::ESCAPE) { os << '%'; continue; } if constexpr (Index < sizeof... (DataT)) { using value_t = std::tuple_element_t>; value::write (os, s, data.template get ()); return write (os, util::make_view (cursor+1,specifiers.end ()), data); } else { throw std::runtime_error ("insufficient data parameters"); } } return os; } /// dispatches rendering of formats with associated parameters template < typename ...Args > std::ostream& operator<< (std::ostream &os, const bound &val) { return write<0> (os, val.specifiers (), val); } /// dispatches rendering of formats with associated parameters template < typename ...Args > std::ostream& operator<< (std::ostream &os, const stored &val) { return write<0> (os, val.specifiers (), val); } template std::string to_string (const bound &fmt) { std::ostringstream os; os << fmt; return os.str (); } template std::string to_string (const stored &fmt) { std::ostringstream os; os << fmt; return os.str (); } } #endif