From 51424295ae9ea77486270d7eca5c64eb9ea0bd99 Mon Sep 17 00:00:00 2001 From: Markus Mittendrein Date: Thu, 6 Jan 2022 19:19:34 +0100 Subject: Move cxxformat include-directory into a separate include directory --- CMakeLists.txt | 16 +- cxxformat/core.hpp | 572 --------------------------------------- cxxformat/cxxformat | 7 - cxxformat/file_ptr.hpp | 23 -- cxxformat/formatters.hpp | 486 --------------------------------- cxxformat/helpers.hpp | 260 ------------------ cxxformat/ostream.hpp | 27 -- cxxformat/string.hpp | 25 -- include/cxxformat/core.hpp | 572 +++++++++++++++++++++++++++++++++++++++ include/cxxformat/cxxformat | 7 + include/cxxformat/file_ptr.hpp | 23 ++ include/cxxformat/formatters.hpp | 486 +++++++++++++++++++++++++++++++++ include/cxxformat/helpers.hpp | 260 ++++++++++++++++++ include/cxxformat/ostream.hpp | 27 ++ include/cxxformat/string.hpp | 25 ++ 15 files changed, 1408 insertions(+), 1408 deletions(-) delete mode 100644 cxxformat/core.hpp delete mode 100644 cxxformat/cxxformat delete mode 100644 cxxformat/file_ptr.hpp delete mode 100644 cxxformat/formatters.hpp delete mode 100644 cxxformat/helpers.hpp delete mode 100644 cxxformat/ostream.hpp delete mode 100644 cxxformat/string.hpp create mode 100644 include/cxxformat/core.hpp create mode 100644 include/cxxformat/cxxformat create mode 100644 include/cxxformat/file_ptr.hpp create mode 100644 include/cxxformat/formatters.hpp create mode 100644 include/cxxformat/helpers.hpp create mode 100644 include/cxxformat/ostream.hpp create mode 100644 include/cxxformat/string.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bd2f310..dc51032 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,15 +5,15 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) project(cxxformat) add_library(cxxformat INTERFACE - cxxformat/cxxformat - cxxformat/core.hpp - cxxformat/file_ptr.hpp - cxxformat/formatters.hpp - cxxformat/helpers.hpp - cxxformat/ostream.hpp - cxxformat/string.hpp + include/cxxformat/cxxformat + include/cxxformat/core.hpp + include/cxxformat/file_ptr.hpp + include/cxxformat/formatters.hpp + include/cxxformat/helpers.hpp + include/cxxformat/ostream.hpp + include/cxxformat/string.hpp ) -target_include_directories(cxxformat INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(cxxformat INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") add_executable(test EXCLUDE_FROM_ALL main.cpp) target_link_libraries(test PRIVATE cxxformat) diff --git a/cxxformat/core.hpp b/cxxformat/core.hpp deleted file mode 100644 index 8669f1c..0000000 --- a/cxxformat/core.hpp +++ /dev/null @@ -1,572 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace format { - namespace { - template - struct str - { - public: - const char s[N + 1]; - - constexpr std::size_t size() const noexcept { return N; } - - constexpr str(const char (&s)[N + 1]) noexcept : str{s, std::make_index_sequence()} {} - constexpr str(std::string_view s) noexcept : str{s, std::make_index_sequence()} {} - - constexpr operator std::string_view() const noexcept - { - return {s, N}; - } - - constexpr bool operator==(std::string_view other) const noexcept - { - return operator std::string_view() == other; - } - - template - consteval auto substr(holder, holder) const noexcept - { - static_assert(pos + count <= N); - return str{s, pos, std::make_index_sequence()}; - } - - template - consteval auto substr(holder) const noexcept - { - return substr(holder{}, holder{}); - } - - template - consteval auto substr(holder p) const noexcept - { - return substr(p, holder{}); - } - - constexpr char operator[](std::size_t i) const noexcept - { - return s[i]; - } - - private: - template - constexpr str(const char (&s)[N + 1], std::index_sequence) noexcept : s{s[indices]...} {} - - template - constexpr str(const char (&s)[M], std::size_t offset, std::index_sequence) noexcept : s{s[indices + offset]..., '\0'} {} - - template - constexpr str(std::string_view s, std::index_sequence) noexcept : s{s[indices]..., '\0'} {} - - friend std::ostream& operator<<(std::ostream& out, const str& s) - { - return out << s.operator std::string_view(); - } - - template - friend struct str; - }; - - template - str(const char(&)[N]) -> str; - - template - struct FormatOutputBase { - constexpr void operator()(std::string_view what) const - { - this_().output(what); - } - - constexpr void operator()(std::string_view what, std::size_t count) const - { - for (std::size_t i = 0; i < count; ++i) - { - this_()(what); - } - } - - constexpr void operator()(char what) const - { - this_()({&what, 1}); - } - - constexpr void operator()(char what, std::size_t count) const - { - this_()({&what, 1}, count); - } - - private: - constexpr const FormatOutput& this_() const - { - static_assert(std::derived_from>, "FormatOutputBase must be used with CRTP"); - static_assert(requires(const FormatOutput o, std::string_view s) { o.output(s); }, "The derived class must implement the method output(std::string_view)"); - return static_cast(*this); - } - }; - - template - concept format_output = std::derived_from, FormatOutputBase>> && requires(const T o, std::string_view s) { o.output(s); }; - - struct NullOutput : FormatOutputBase { - constexpr void output(std::string_view) const noexcept {} - }; - - using ArgIndex = std::uint16_t; - struct format_specifier { - enum Length : std::uint8_t { - None, - Quarter, // hh - Half, // h - Long, // l - LongLong, // ll - LongDouble, // L - Maximum, // j - SizeT, // z - PtrDiff // t - }; - - optional_int minWidth; - optional_int precision; - ArgIndex argIndex; - char conversion; - char addSign; - char padding; - struct { - bool leftJustified : 1; - bool alternative : 1; - bool widthAsArg : 1; - bool precisionAsArg : 1; - Length length : 4; - }; - }; - } - - template - constexpr decltype(auto) make_output(Out&& out) { return std::forward(out); } - - template - concept format_output_compatible = format_output || requires(T&& o) { {make_output(std::forward(o))} -> format_output; }; - - template - struct formatter; - - template - concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { - { formatter::format(out, t, spec, z, z) } -> std::same_as; - { formatter::template format(out, t, z, z) } -> std::same_as; - }; - - template - concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { - { formatter::format(out, t, spec, z, z) } -> std::same_as; - { formatter::template format(out, t, z, z) } -> std::same_as; - }; - - template - concept has_some_formatter = has_formatter || has_fallback_formatter; - - namespace { - template - struct formatter_with_fallback : formatter {}; - - template requires (!has_formatter) - struct formatter_with_fallback : formatter {}; - - template - constexpr std::optional digit(char c) noexcept - { - if (c >= '0' && c <= '9') - { - return c - '0'; - } - return std::nullopt; - } - - template - constexpr std::pair, std::size_t> parseNumber(std::string_view s, std::size_t pos) noexcept - { - if (pos >= s.size()) - { - return {std::nullopt, pos}; - } - - bool isNegative = (std::signed_integral && s[pos] == '-'); - auto startPos = isNegative ? pos + 1 : pos; - if (startPos >= s.size()) - { - return {std::nullopt, pos}; - } - - auto d = digit(s[startPos]); - if (!d) - { - return {std::nullopt, pos}; - } - - T val{*d}; - auto p = startPos + 1; - for (; p < s.size(); ++p) - { - auto nextD = digit(s[p]); - if (!nextD) - { - break; - } - - val *= 10; - val += *nextD; - } - return {isNegative ? -val : val, p}; - } - - constexpr auto parseSpec(std::string_view fmt, std::size_t pos, ArgIndex nextArg) - { - struct Result { - std::size_t pos; - format_specifier spec; - ArgIndex nextArg; - ArgIndex maxArg; - }; - - const auto checkEos = [&pos, fmt](std::optional p = std::nullopt) { - if (p.value_or(pos) >= fmt.size()) - { - throw std::invalid_argument{"Incomplete format specifier at end of format string"}; - } - }; - - format_specifier spec{ - .minWidth = std::nullopt, - .precision = std::nullopt, - .addSign = '\0', - .padding = ' ', - }; - spec.leftJustified = false; - spec.alternative = false; - spec.widthAsArg = false; - spec.precisionAsArg = false; - spec.length = format_specifier::None; - - bool hasExplicitArgIndex = false; - const auto argIndex = parseNumber(fmt, pos); - if (argIndex.first) - { - checkEos(argIndex.second); - if (fmt[argIndex.second] == '$') - { - if (*argIndex.first == 0) - { - throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; - } - spec.argIndex = *argIndex.first - 1; - hasExplicitArgIndex = true; - pos = argIndex.second + 1; - checkEos(); - } - } - - for (; pos < fmt.size(); ++pos) - { - const auto c = fmt[pos]; - if (c == '-') - { - spec.leftJustified = true; - } - else if (c == '+') - { - spec.addSign = '+'; - } - else if (c == ' ') - { - spec.addSign = ' '; - } - else if (c == '#') - { - spec.alternative = true; - } - else if (c == '0') - { - spec.padding = '0'; - } - else - { - break; - } - } - if (spec.leftJustified) - { - spec.padding = ' '; - } - checkEos(); - - ArgIndex maxArg = 0; - const auto argIndexOrNextArg = [&pos, &nextArg, &maxArg, checkEos, fmt](decltype(*format_specifier::minWidth)& argIndex) { - const auto index = parseNumber(fmt, pos); - checkEos(index.second); - if (index.first && fmt[index.second] == '$') - { - if (*index.first == 0) - { - throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; - } - argIndex = *index.first - 1; - pos = index.second + 1; - checkEos(); - } - else - { - argIndex = nextArg; - ++nextArg; - } - maxArg = std::max(maxArg, argIndex); - }; - - const auto handleWidthPrecision = [&pos, argIndexOrNextArg, fmt, checkEos](decltype(format_specifier::minWidth)& indexOrValue, bool allowEmpty = false){ - bool asArg = false; - if (fmt[pos] == '*') - { - asArg = true; - ++pos; - checkEos(); - argIndexOrNextArg(indexOrValue.emplace()); - } - else - { - const auto width = parseNumber(fmt, pos); - if (width.first) - { - indexOrValue = *width.first; - pos = width.second; - } - else if (allowEmpty) - { - indexOrValue = 0; - } - } - return asArg; - }; - spec.widthAsArg = handleWidthPrecision(spec.minWidth); - - - if (fmt[pos] == '.') - { - ++pos; - checkEos(); - spec.precisionAsArg = handleWidthPrecision(spec.precision, true); - } - - { - using L = format_specifier::Length; - const auto c = fmt[pos]; - if (c == 'h' || c == 'l') - { - if (fmt[pos + 1] == c) - { - ++pos; - checkEos(); - spec.length = (c == 'h' ? L::Quarter : L::LongLong); - ++pos; - } - else - { - spec.length = (c == 'h' ? L::Half : L::Long); - ++pos; - } - } - else if (c == 'L') - { - spec.length = L::LongDouble; - ++pos; - } - else if (c == 'j') - { - spec.length = L::Maximum; - ++pos; - } - else if (c == 'z') - { - spec.length = L::SizeT; - ++pos; - } - else if (c == 't') - { - spec.length = L::PtrDiff; - ++pos; - } - } - checkEos(); - - spec.conversion = fmt[pos]; - ++pos; - - if (!hasExplicitArgIndex) - { - spec.argIndex = nextArg; - ++nextArg; - } - maxArg = std::max(maxArg, spec.argIndex); - - return Result{pos, spec, nextArg, static_cast(maxArg + 1)}; - } - - constexpr bool checkValidConversion(char c) noexcept - { - return std::string_view{"diouxXeEfFgGaAcspv"}.find(c) != std::string_view::npos; - } - - namespace compile_time { - template - struct is_str_s : std::false_type {}; - - template - struct is_str_s> : std::true_type {}; - - template - concept is_str = is_str_s::value; - - template - concept template_part = std::same_as || is_str; - - template - consteval void assertArgumentUsed() - { - static_assert(used, "Argument is not used (1-based argIndex)"); - } - - template - struct format_template { - template requires (has_some_formatter> && ...) - constexpr void operator()(Output&& output, Args&&... args) const - { - checkArgCount(); - - const auto out = make_output(std::forward(output)); - const auto format_arg = [&args..., &out](holder) -> decltype(auto) - { - optional_int minWidth = spec.minWidth; - if constexpr (spec.widthAsArg) - { - using Arg = std::decay_t>; - static_assert(std::unsigned_integral, "Width argument must be an unsigned integral"); - static_assert(sizeof(Arg) <= sizeof(std::size_t), "Width argument type must be at most as big as std::size_t"); - minWidth = nth_argument<*spec.minWidth>(std::forward(args)...); - } - optional_int precision = spec.precision; - if constexpr (spec.precisionAsArg) - { - using Arg = std::decay_t>; - static_assert(std::unsigned_integral, "Precision argument must be an unsigned integral"); - static_assert(sizeof(Arg) <= sizeof(std::size_t), "Precision argument type must be at most as big as std::size_t"); - precision = nth_argument<*spec.precision>(std::forward(args)...); - } - auto&& arg = nth_argument(std::forward(args)...); - return formatter_with_fallback>::template format(out, arg, minWidth, precision); - }; - - (([&]{ - if constexpr (is_str>) - { - out(parts); - } - else - { - format_arg(holder{}); - } - })(), ...); - } - - private: - template - consteval static void checkArgCount() - { - if constexpr (i != argCount) - { - static_assert(i > argCount, "Not enough arguments passed."); - static_assert(i < argCount, "Too many arguments passed."); - } - } - - consteval static void checkArgsUsed() - { - checkArgsUsed(std::make_index_sequence()); - } - - template - consteval static void checkArgsUsed(std::index_sequence) - { - (checkArgUsed(), ...); - } - - template - consteval static void checkArgUsed() - { - assertArgumentUsed>) - { - return false; - } - else - { - return parts.argIndex == i || (parts.widthAsArg && parts.minWidth == i) || (parts.precisionAsArg && parts.precision == i); - } - }() || ...)>(); - } - - static_assert((checkArgsUsed(), true)); - }; - - template - consteval auto parseSpec() noexcept - { - constexpr auto specResult = parseSpec(fmt, pos, nextArg); - static_assert(checkValidConversion(specResult.spec.conversion), "Invalid conversion specifier"); - return specResult; - } - - template - consteval auto parseFormat() noexcept - { - if constexpr (pos >= fmt.size()) - { - return format_template{}; - } - else - { - constexpr auto nextSpec = std::string_view{fmt}.find('%', pos); - if constexpr (nextSpec == std::string_view::npos) - { - return parseFormat{})>(); - } - else - { - static_assert(nextSpec + 1 < fmt.size(), "Incomplete format specifier at end of format string"); - - constexpr auto specStart = fmt[nextSpec + 1]; - if constexpr (specStart == '%') - { - return parseFormat{}), str{"%"}>(); - } - else - { - constexpr auto result = parseSpec(); - return parseFormat{}), result.spec>(); - } - } - } - } - - template - consteval auto operator ""_format_to() noexcept - { - return parseFormat(); - } - } - } - - using compile_time::operator ""_format_to; -} diff --git a/cxxformat/cxxformat b/cxxformat/cxxformat deleted file mode 100644 index 6da253a..0000000 --- a/cxxformat/cxxformat +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include diff --git a/cxxformat/file_ptr.hpp b/cxxformat/file_ptr.hpp deleted file mode 100644 index da516b0..0000000 --- a/cxxformat/file_ptr.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -#include - -namespace format { - namespace { - class FilePtrOutput : public FormatOutputBase { - FILE* to; - - public: - FilePtrOutput(FILE* to) noexcept : to{to} {} - - void output(std::string_view what) const - { - fwrite(what.data(), what.size(), 1, to); - } - }; - } - -} -[[maybe_unused]] format::FilePtrOutput make_output(FILE* to) noexcept { return {to}; } diff --git a/cxxformat/formatters.hpp b/cxxformat/formatters.hpp deleted file mode 100644 index a5c92ed..0000000 --- a/cxxformat/formatters.hpp +++ /dev/null @@ -1,486 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace format { - namespace { - constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int minWidth) - { - if(subject.length() < minWidth.value_or(0) && !leftJustified) - { - out(padding, *minWidth - subject.length()); - } - - out(subject); - - if(subject.length() < minWidth.value_or(0) && leftJustified) - { - out(padding, *minWidth - subject.length()); - } - } - } - - using invalid_argument = std::invalid_argument; - - template - struct formatter { - static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) - { - int base = 10; - switch(spec.conversion) - { - case 'v': - break; - case 'c': - { - // TODO: see static_assert version - char converted = static_cast(static_cast(t)); - formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); - return; - } - case 'i': case 'd': - if constexpr (std::unsigned_integral) - { - throw invalid_argument{"%i and %d are not supported for unsigned integrals"}; - } - break; - case 'o': - base = 8; - if constexpr (std::signed_integral) - { - throw invalid_argument{"%o is not supported for signed integrals"}; - } - break; - case 'x': case 'X': - base = 16; - if constexpr (std::signed_integral) - { - throw invalid_argument{"%x and %X are not supported for signed integrals"}; - } - break; - case 'u': - if constexpr (std::signed_integral) - { - throw invalid_argument{"%u is not supported for signed integrals"}; - } - break; - default: - throw invalid_argument{std::string{"The given integral type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; - } - - std::string_view converted; - std::array::digits + 2) / 3> result; - if (precision != 0 || t != 0) - { - const auto [end, ec] = std::to_chars(result.data(), result.data() + result.size(), t, base); - assert(ec == std::errc{}); - converted = {result.data(), end}; - } - const bool hasMinusSign = !std::unsigned_integral && converted.starts_with('-'); - if (hasMinusSign) - { - converted.remove_prefix(1); - } - - if (spec.conversion == 'X') - { - std::transform(result.begin(), result.end(), result.data(), [](char c) - { - return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; - }); - } - - char signPrefix{'\0'}; - std::string_view prefix; - if (hasMinusSign) - { - prefix = "-"; - } - else if (base != 10 && spec.alternative) - { - if (spec.conversion == 'o') - { - prefix = "0"; - } - else if (precision != 0 || t != 0) - { - if (spec.conversion == 'x') - { - prefix = "0x"; - } - else if (spec.conversion == 'X') - { - prefix = "0X"; - } - } - } - else if (spec.addSign != '\0') - { - signPrefix = spec.addSign; - prefix = {&signPrefix, 1}; - } - - std::size_t precisionPadding{0}; - std::size_t normalPadding{0}; - if (precision) - { - if (*precision > converted.size()) - { - precisionPadding = *precision - converted.size(); - } - } - else if (minWidth && (spec.alternative && base != 10 && spec.padding == '0')) - { - const auto totalLength = converted.size() + prefix.size(); - if (*minWidth > totalLength) - { - precisionPadding = *minWidth - totalLength; - minWidth.clear(); - } - } - - if (minWidth) - { - const auto totalLength = converted.size() + precisionPadding + prefix.size(); - if (*minWidth > totalLength) - { - normalPadding = *minWidth - totalLength; - } - } - - const auto padding = precision ? ' ' : spec.padding; - if(!spec.leftJustified) - { - out(padding, normalPadding); - } - - out(prefix); - out('0', precisionPadding); - - out(converted); - - if(spec.leftJustified) - { - out(padding, normalPadding); - } - return; - } - - template - static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) - { - constexpr auto conv = spec.conversion; - if constexpr (conv == 'c') - { - // TODO: what is the right thing to do here? - // static_assert(std::same_as, "%c only accepts int as argument"); - } - else if constexpr (conv == 'i' || conv == 'd') - { - static_assert(std::signed_integral, "%i and %d only accept signed integral types"); - } - else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u') - { - static_assert(std::unsigned_integral, "%o, %x, %X and %u only accept unsigned integral types"); - } - else if constexpr (conv != 'v') - { - static_assert(sizeof(T) == 0, "Unsupported conversion specified"); - } - - static_assert(spec.addSign == '\0' || conv == 'i' || conv == 'd', "‘+’ and ‘ ’ (sign related) flags are only allowed for %i and %d"); - static_assert(!spec.alternative || conv == 'o' || conv == 'x' || conv == 'X', "‘#’ (alternative) flag is only allowed for %x, %X and %o"); - - return format(out, t, spec, minWidth, precision); - } - }; - - // NOTE: Difference to printf: uses std::to_chars’ minimal width for exact representation when no precision is specified instead of default precision of 6 - template - struct formatter { - static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) - { - std::chars_format format; - bool uppercase = false; - switch(spec.conversion) - { - case 'A': - uppercase = true; - [[fallthrough]]; - case 'a': - format = std::chars_format::hex; - break; - case 'E': - uppercase = true; - [[fallthrough]]; - case 'e': - format = std::chars_format::scientific; - break; - case 'F': - uppercase = true; - [[fallthrough]]; - case 'f': - format = std::chars_format::fixed; - break; - case 'G': - uppercase = true; - [[fallthrough]]; - case 'g': - case 'v': - format = std::chars_format::general; - break; - default: - throw invalid_argument{std::string{"The given floating point type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; - } - - std::array::digits10 * 2 + 6)> result; - const auto [end, ec] = precision ? std::to_chars(result.data(), result.data() + result.size(), t, format ,*precision) : std::to_chars(result.data(), result.data() + result.size(), t, format); - assert(ec == std::errc{}); - std::string_view converted{result.data(), end}; - - const bool hasMinusSign = converted.starts_with('-'); - if (hasMinusSign) - { - converted.remove_prefix(1); - } - - std::string_view mantissa = converted; - std::string_view exponent; - if (const auto exponentPos = converted.find(format == std::chars_format::hex ? 'p' : 'e'); exponentPos != std::string_view::npos) - { - mantissa = converted.substr(0, exponentPos); - exponent = converted.substr(exponentPos); - } - - const bool hasDot = mantissa.find('.') != std::string_view::npos; - const bool addDot = spec.alternative && !hasDot; - - std::size_t extraZeros{0}; - if (spec.alternative && format == std::chars_format::general && precision) - { - const auto currentMantissaPrecision = mantissa.size() - (hasDot ? 1 : 0); - if (*precision > currentMantissaPrecision) - { - extraZeros = *precision - currentMantissaPrecision; - } - } - - if (uppercase) - { - std::transform(result.data(), end, result.data(), [](char c) - { - return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; - }); - } - - const char signPrefix = hasMinusSign ? '-' : spec.addSign; - std::string_view hexPrefix; - if (format == std::chars_format::hex && std::isfinite(t)) - { - if (spec.conversion == 'A') - { - hexPrefix = "0X"; - } - else - { - hexPrefix = "0x"; - } - } - - std::size_t paddingLength{0}; - if (minWidth) - { - const auto totalLength = (signPrefix != '\0' ? 1 : 0) + hexPrefix.size() + converted.size() + (addDot ? 1 : 0) + extraZeros; - if (*minWidth > totalLength) - { - paddingLength = *minWidth - totalLength; - } - } - - if(!spec.leftJustified && spec.padding != '0') - { - out(spec.padding, paddingLength); - } - - if (signPrefix != '\0') - { - out(signPrefix); - } - out(hexPrefix); - - if(!spec.leftJustified && spec.padding == '0') - { - out(spec.padding, paddingLength); - } - - out(mantissa); - if (addDot) - { - out('.'); - } - out('0', extraZeros); - out(exponent); - - if(spec.leftJustified) - { - out(spec.padding, paddingLength); - } - return; - } - - template - static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) - { - static_assert(std::string_view{"fFeEaAgGv"}.find(spec.conversion) != std::string_view::npos, "Unsupported conversion specified for floating point type"); - return format(out, t, spec, minWidth, precision); - } - }; - - template - concept stringy = requires(T t) { std::string_view{t}; }; - - template - struct formatter { - static constexpr void format(const format_output auto& out, const S& s, format_specifier spec, optional_int minWidth, optional_int precision) - { - if (spec.conversion != 's' && spec.conversion != 'v') - { - throw invalid_argument{"The given stringy type only supports the ‘s’ and ‘v’ conversions"}; - } - std::string_view val(s); - if (precision) - { - val = val.substr(0, *precision); - } - - formatPadded(out, val, ' ', spec.leftJustified, minWidth); - } - - template - static constexpr void format(const format_output auto& out, const S& s, optional_int minWidth, optional_int precision) - { - static_assert(spec.conversion == 's' || spec.conversion == 'v', "The given stringy type only supports the ‘s’ and ‘v’ conversions"); - return format(out, s, spec, minWidth, precision); - } - }; - - template - concept pointer = std::is_pointer_v; - - template - struct formatter { - static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, format_specifier spec, optional_int minWidth, optional_int precision) - { - if (t == nullptr) - { - formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); - } - else - { - constexpr format_specifier spec{.conversion = 'x', .alternative = true}; - formatter::template format(out, std::bit_cast(t), minWidth, std::nullopt); - } - } - - template - static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, optional_int minWidth, optional_int precision) - { - static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); - return format(out, t, spec, minWidth, precision); - } - }; - - template<> - struct formatter { - static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int minWidth, optional_int precision) - { - formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); - } - - template - static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int minWidth, optional_int precision) - { - static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); - return format(out, nullptr, spec, minWidth, precision); - } - }; - - template<> - struct formatter { - static constexpr void format(const format_output auto& out, const char* s, format_specifier spec, optional_int minWidth, optional_int precision) - { - if (spec.conversion == 'p') - { - formatter::format(out, static_cast(s), spec, minWidth, precision); - } - else - { - assert(spec.conversion == 's' || spec.alternative == 'v'); - - if (s == nullptr) - { - formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); - } - else - { - formatter::format(out, s, spec, minWidth, precision); - } - } - } - - template - static constexpr void format(const format_output auto& out, const char* s, optional_int minWidth, optional_int precision) - { - static_assert(spec.conversion == 'p' || spec.conversion == 's' || spec.conversion == 'v', "const char* only supports the ‘p’, ‘s’ and ‘v’ conversions"); - return format(out, s, spec, minWidth, precision); - } - }; - - template<> - struct formatter : formatter {}; - - template - concept ostreamable = requires(T t, std::ostream& out) { {out << t} -> std::same_as; }; - - template - concept has_ostream = requires(T t) { {t.stream()} -> std::same_as; }; - - template - struct formatter { - static void format(const format_output auto& out, const T& t, format_specifier spec, optional_int minWidth, optional_int) - { - if constexpr (has_ostream) - { - if (!minWidth) - { - out.stream() << t; - return; - } - } - - std::ostringstream stream; - stream << t; - formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth); - } - - template - static constexpr void format(const format_output auto& out, const T& t, optional_int minWidth, optional_int precision) - { - static_assert(spec.conversion == 'v' || spec.conversion == 's', "The operator<<(std::ostream&) fallback only supports conversions ‘s’ and ‘v’"); - static_assert(!spec.precision, "The operator<<(std::ostream&) fallback does not support specifying an precision"); - static_assert(spec.addSign == '\0', "The operator<<(std::ostream&) fallback does not support the add sign (‘+‘) flag"); - static_assert(!spec.alternative, "The operator<<(std::ostream&) fallback does not support the alternative (‘#‘) flag"); - static_assert(spec.padding == ' ', "The operator<<(std::ostream&) fallback does not support padding with 0"); - static_assert(spec.length == format_specifier::Length::None, "The operator<<(std::ostream&) fallback does not support specifying any length"); - return format(out, t, spec, minWidth, precision); - } - }; -} diff --git a/cxxformat/helpers.hpp b/cxxformat/helpers.hpp deleted file mode 100644 index 73cb0be..0000000 --- a/cxxformat/helpers.hpp +++ /dev/null @@ -1,260 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace detail { - template - class FunctorHelper : T { - using T::operator(); - }; -} - -template -concept functor = requires { - std::declval>(); -}; - -template -class overloaded_callable : public T... -{ -public: - overloaded_callable(T&&... bases) : T{std::forward(bases)}... {} - using T::operator()...; -}; - -template -struct nth_argument_s -{ - template - constexpr decltype(auto) operator()(Arg&& arg, Args&&... args) const noexcept - { - static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); - if constexpr (i == 0) - { - return std::forward(arg); - } - else - { - return nth_argument_s{}(std::forward(args)...); - } - } -}; - -template -constexpr inline auto nth_argument = nth_argument_s{}; - -template -struct nth_type_s -{ - static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); - using type = typename nth_type_s::type; -}; - -template -struct nth_type_s<0, Arg, Args...> -{ - using type = Arg; -}; - -template -using nth_type = typename nth_type_s::type; - -template -struct holder {}; - -namespace helper_concepts { -template class optional> -concept optional_ctor_ok = requires { - !std::is_constructible_v&>; - !std::is_constructible_v&>; - !std::is_constructible_v&&>; - !std::is_constructible_v&&>; - !std::is_convertible_v&, T>; - !std::is_convertible_v&, T>; - !std::is_convertible_v&&, T>; - !std::is_convertible_v&&, T>; -}; -} - -template -struct structural_optional { - - bool hasValue; - union { - char empty; // structural types can not have uninitialized memory - T value; - }; - - constexpr structural_optional() noexcept : hasValue{false}, empty{} {} - constexpr structural_optional(std::nullopt_t) noexcept : hasValue{false}, empty{} {} - constexpr structural_optional(const T& value) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{true}, value{value} {} - constexpr structural_optional(T&& value) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{true}, value{std::move(value)} {} - template - constexpr structural_optional(std::in_place_t, Args&&... value) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v) : hasValue{true}, value{std::forward(value)...} {} - template - constexpr structural_optional(U&& value) noexcept(std::is_nothrow_convertible_v) requires(std::is_convertible_v) : hasValue{true}, value{std::forward(value)} {} - template - constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} - { - if (hasValue) - { - std::construct_at(&value, *other); - } - } - template - constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} - { - if (hasValue) - { - std::construct_at(&value, std::move(*other)); - } - } - - constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{other.hasValue}, empty{} - { - if (hasValue) - { - std::construct_at(&value, other.value); - } - } - constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{other.hasValue}, empty{} - { - if (hasValue) - { - std::construct_at(&value, std::move(other.value)); - } - } - - constexpr void clear() - { - if (hasValue) - { - value.~T(); - hasValue = false; - empty = {}; - } - } - - explicit constexpr operator bool() const noexcept { return hasValue; } - constexpr T& operator*() noexcept - { - assert(hasValue); - return value; - } - - constexpr const T& operator*() const noexcept - { - assert(hasValue); - return value; - } - - constexpr structural_optional& operator=(const T& val) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) - { - emplace(val); - return *this; - } - - constexpr structural_optional& operator=(T&& val) noexcept(std::is_nothrow_move_constructible_v) requires(std::assignable_from) - { - emplace(std::move(val)); - return *this; - } - - constexpr structural_optional& operator=(const structural_optional& other) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) - { - if (other.hasValue) - { - emplace(other.value); - } - else - { - clear(); - } - return *this; - } - - constexpr structural_optional& operator=(structural_optional&& other) noexcept(std::is_nothrow_move_assignable_v) requires(std::assignable_from) - { - if (other.hasValue) - { - emplace(std::move(other.value)); - } - else - { - clear(); - } - return *this; - } - - - constexpr const T& value_or(const T& fallback) const noexcept { return hasValue ? value : fallback; } - - constexpr bool operator==(const structural_optional& other) const - { - return hasValue == other.hasValue && (!hasValue || value == other.value); - } - - constexpr bool operator==(const T& other) const - { - return hasValue && value == other; - } - - template - constexpr T& emplace(Args&&... args) noexcept - { - clear(); - hasValue = true; - std::construct_at(&value, std::forward(args)...); - return value; - } - - constexpr ~structural_optional() - { - clear(); - } -}; - -template -struct optional_int { - T val; - - constexpr optional_int() noexcept : val{emptyVal} {} - constexpr optional_int(std::nullopt_t) noexcept : val{emptyVal} {} - constexpr optional_int(T val) noexcept : val{val} {} - constexpr optional_int(const optional_int&) noexcept = default; - template U> - constexpr optional_int(const optional_int& other) noexcept : val{other ? other.val : emptyVal} {} - constexpr optional_int& operator=(const optional_int&) noexcept = default; - template U> - constexpr optional_int& operator=(optional_int other) noexcept - { - val = other ? other.val : emptyVal; - return *this; - } - - static constexpr inline T emptyVal{std::signed_integral ? std::numeric_limits::min() : std::numeric_limits::max()}; - - constexpr void clear() noexcept - { - val = emptyVal; - } - - constexpr explicit operator bool() const noexcept { return val != emptyVal; } - constexpr bool empty() const noexcept { return !*this; } - - constexpr bool operator==(const optional_int& other) const noexcept = default; - constexpr bool operator==(T other) const noexcept - { - return !empty() && val == other; - } - constexpr T& emplace() noexcept { return val = 0; } - constexpr T& operator*() noexcept { return val; } - constexpr T operator*() const noexcept { return val; } - constexpr T value_or(T fallback) const noexcept { return empty() ? fallback : val; } -}; diff --git a/cxxformat/ostream.hpp b/cxxformat/ostream.hpp deleted file mode 100644 index 308b670..0000000 --- a/cxxformat/ostream.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include - -namespace format { - namespace { - class OstreamOutput : public FormatOutputBase { - std::ostream& to; - - public: - OstreamOutput(std::ostream& to) noexcept : to{to} {} - - void output(std::string_view what) const - { - to << what; - } - - std::ostream& stream() const noexcept { return to; } - }; - } - -} -namespace std { - [[maybe_unused]] format::OstreamOutput make_output(std::ostream& to) noexcept { return {to}; } -} diff --git a/cxxformat/string.hpp b/cxxformat/string.hpp deleted file mode 100644 index 06cb620..0000000 --- a/cxxformat/string.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace format { - namespace { - namespace compile_time { - template - consteval auto operator ""_format() noexcept - { - return [formatter = parseFormat()](Args&&... args) -> std::string - { - std::ostringstream out; - formatter(out, std::forward(args)...); - return out.str(); - }; - } - } - } - - using compile_time::operator ""_format; -} diff --git a/include/cxxformat/core.hpp b/include/cxxformat/core.hpp new file mode 100644 index 0000000..8669f1c --- /dev/null +++ b/include/cxxformat/core.hpp @@ -0,0 +1,572 @@ +#pragma once + +#include +#include +#include + +#include + +namespace format { + namespace { + template + struct str + { + public: + const char s[N + 1]; + + constexpr std::size_t size() const noexcept { return N; } + + constexpr str(const char (&s)[N + 1]) noexcept : str{s, std::make_index_sequence()} {} + constexpr str(std::string_view s) noexcept : str{s, std::make_index_sequence()} {} + + constexpr operator std::string_view() const noexcept + { + return {s, N}; + } + + constexpr bool operator==(std::string_view other) const noexcept + { + return operator std::string_view() == other; + } + + template + consteval auto substr(holder, holder) const noexcept + { + static_assert(pos + count <= N); + return str{s, pos, std::make_index_sequence()}; + } + + template + consteval auto substr(holder) const noexcept + { + return substr(holder{}, holder{}); + } + + template + consteval auto substr(holder p) const noexcept + { + return substr(p, holder{}); + } + + constexpr char operator[](std::size_t i) const noexcept + { + return s[i]; + } + + private: + template + constexpr str(const char (&s)[N + 1], std::index_sequence) noexcept : s{s[indices]...} {} + + template + constexpr str(const char (&s)[M], std::size_t offset, std::index_sequence) noexcept : s{s[indices + offset]..., '\0'} {} + + template + constexpr str(std::string_view s, std::index_sequence) noexcept : s{s[indices]..., '\0'} {} + + friend std::ostream& operator<<(std::ostream& out, const str& s) + { + return out << s.operator std::string_view(); + } + + template + friend struct str; + }; + + template + str(const char(&)[N]) -> str; + + template + struct FormatOutputBase { + constexpr void operator()(std::string_view what) const + { + this_().output(what); + } + + constexpr void operator()(std::string_view what, std::size_t count) const + { + for (std::size_t i = 0; i < count; ++i) + { + this_()(what); + } + } + + constexpr void operator()(char what) const + { + this_()({&what, 1}); + } + + constexpr void operator()(char what, std::size_t count) const + { + this_()({&what, 1}, count); + } + + private: + constexpr const FormatOutput& this_() const + { + static_assert(std::derived_from>, "FormatOutputBase must be used with CRTP"); + static_assert(requires(const FormatOutput o, std::string_view s) { o.output(s); }, "The derived class must implement the method output(std::string_view)"); + return static_cast(*this); + } + }; + + template + concept format_output = std::derived_from, FormatOutputBase>> && requires(const T o, std::string_view s) { o.output(s); }; + + struct NullOutput : FormatOutputBase { + constexpr void output(std::string_view) const noexcept {} + }; + + using ArgIndex = std::uint16_t; + struct format_specifier { + enum Length : std::uint8_t { + None, + Quarter, // hh + Half, // h + Long, // l + LongLong, // ll + LongDouble, // L + Maximum, // j + SizeT, // z + PtrDiff // t + }; + + optional_int minWidth; + optional_int precision; + ArgIndex argIndex; + char conversion; + char addSign; + char padding; + struct { + bool leftJustified : 1; + bool alternative : 1; + bool widthAsArg : 1; + bool precisionAsArg : 1; + Length length : 4; + }; + }; + } + + template + constexpr decltype(auto) make_output(Out&& out) { return std::forward(out); } + + template + concept format_output_compatible = format_output || requires(T&& o) { {make_output(std::forward(o))} -> format_output; }; + + template + struct formatter; + + template + concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { + { formatter::format(out, t, spec, z, z) } -> std::same_as; + { formatter::template format(out, t, z, z) } -> std::same_as; + }; + + template + concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int z) { + { formatter::format(out, t, spec, z, z) } -> std::same_as; + { formatter::template format(out, t, z, z) } -> std::same_as; + }; + + template + concept has_some_formatter = has_formatter || has_fallback_formatter; + + namespace { + template + struct formatter_with_fallback : formatter {}; + + template requires (!has_formatter) + struct formatter_with_fallback : formatter {}; + + template + constexpr std::optional digit(char c) noexcept + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + return std::nullopt; + } + + template + constexpr std::pair, std::size_t> parseNumber(std::string_view s, std::size_t pos) noexcept + { + if (pos >= s.size()) + { + return {std::nullopt, pos}; + } + + bool isNegative = (std::signed_integral && s[pos] == '-'); + auto startPos = isNegative ? pos + 1 : pos; + if (startPos >= s.size()) + { + return {std::nullopt, pos}; + } + + auto d = digit(s[startPos]); + if (!d) + { + return {std::nullopt, pos}; + } + + T val{*d}; + auto p = startPos + 1; + for (; p < s.size(); ++p) + { + auto nextD = digit(s[p]); + if (!nextD) + { + break; + } + + val *= 10; + val += *nextD; + } + return {isNegative ? -val : val, p}; + } + + constexpr auto parseSpec(std::string_view fmt, std::size_t pos, ArgIndex nextArg) + { + struct Result { + std::size_t pos; + format_specifier spec; + ArgIndex nextArg; + ArgIndex maxArg; + }; + + const auto checkEos = [&pos, fmt](std::optional p = std::nullopt) { + if (p.value_or(pos) >= fmt.size()) + { + throw std::invalid_argument{"Incomplete format specifier at end of format string"}; + } + }; + + format_specifier spec{ + .minWidth = std::nullopt, + .precision = std::nullopt, + .addSign = '\0', + .padding = ' ', + }; + spec.leftJustified = false; + spec.alternative = false; + spec.widthAsArg = false; + spec.precisionAsArg = false; + spec.length = format_specifier::None; + + bool hasExplicitArgIndex = false; + const auto argIndex = parseNumber(fmt, pos); + if (argIndex.first) + { + checkEos(argIndex.second); + if (fmt[argIndex.second] == '$') + { + if (*argIndex.first == 0) + { + throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; + } + spec.argIndex = *argIndex.first - 1; + hasExplicitArgIndex = true; + pos = argIndex.second + 1; + checkEos(); + } + } + + for (; pos < fmt.size(); ++pos) + { + const auto c = fmt[pos]; + if (c == '-') + { + spec.leftJustified = true; + } + else if (c == '+') + { + spec.addSign = '+'; + } + else if (c == ' ') + { + spec.addSign = ' '; + } + else if (c == '#') + { + spec.alternative = true; + } + else if (c == '0') + { + spec.padding = '0'; + } + else + { + break; + } + } + if (spec.leftJustified) + { + spec.padding = ' '; + } + checkEos(); + + ArgIndex maxArg = 0; + const auto argIndexOrNextArg = [&pos, &nextArg, &maxArg, checkEos, fmt](decltype(*format_specifier::minWidth)& argIndex) { + const auto index = parseNumber(fmt, pos); + checkEos(index.second); + if (index.first && fmt[index.second] == '$') + { + if (*index.first == 0) + { + throw std::invalid_argument{"Placeholder indices are 1-based and can’t be 0"}; + } + argIndex = *index.first - 1; + pos = index.second + 1; + checkEos(); + } + else + { + argIndex = nextArg; + ++nextArg; + } + maxArg = std::max(maxArg, argIndex); + }; + + const auto handleWidthPrecision = [&pos, argIndexOrNextArg, fmt, checkEos](decltype(format_specifier::minWidth)& indexOrValue, bool allowEmpty = false){ + bool asArg = false; + if (fmt[pos] == '*') + { + asArg = true; + ++pos; + checkEos(); + argIndexOrNextArg(indexOrValue.emplace()); + } + else + { + const auto width = parseNumber(fmt, pos); + if (width.first) + { + indexOrValue = *width.first; + pos = width.second; + } + else if (allowEmpty) + { + indexOrValue = 0; + } + } + return asArg; + }; + spec.widthAsArg = handleWidthPrecision(spec.minWidth); + + + if (fmt[pos] == '.') + { + ++pos; + checkEos(); + spec.precisionAsArg = handleWidthPrecision(spec.precision, true); + } + + { + using L = format_specifier::Length; + const auto c = fmt[pos]; + if (c == 'h' || c == 'l') + { + if (fmt[pos + 1] == c) + { + ++pos; + checkEos(); + spec.length = (c == 'h' ? L::Quarter : L::LongLong); + ++pos; + } + else + { + spec.length = (c == 'h' ? L::Half : L::Long); + ++pos; + } + } + else if (c == 'L') + { + spec.length = L::LongDouble; + ++pos; + } + else if (c == 'j') + { + spec.length = L::Maximum; + ++pos; + } + else if (c == 'z') + { + spec.length = L::SizeT; + ++pos; + } + else if (c == 't') + { + spec.length = L::PtrDiff; + ++pos; + } + } + checkEos(); + + spec.conversion = fmt[pos]; + ++pos; + + if (!hasExplicitArgIndex) + { + spec.argIndex = nextArg; + ++nextArg; + } + maxArg = std::max(maxArg, spec.argIndex); + + return Result{pos, spec, nextArg, static_cast(maxArg + 1)}; + } + + constexpr bool checkValidConversion(char c) noexcept + { + return std::string_view{"diouxXeEfFgGaAcspv"}.find(c) != std::string_view::npos; + } + + namespace compile_time { + template + struct is_str_s : std::false_type {}; + + template + struct is_str_s> : std::true_type {}; + + template + concept is_str = is_str_s::value; + + template + concept template_part = std::same_as || is_str; + + template + consteval void assertArgumentUsed() + { + static_assert(used, "Argument is not used (1-based argIndex)"); + } + + template + struct format_template { + template requires (has_some_formatter> && ...) + constexpr void operator()(Output&& output, Args&&... args) const + { + checkArgCount(); + + const auto out = make_output(std::forward(output)); + const auto format_arg = [&args..., &out](holder) -> decltype(auto) + { + optional_int minWidth = spec.minWidth; + if constexpr (spec.widthAsArg) + { + using Arg = std::decay_t>; + static_assert(std::unsigned_integral, "Width argument must be an unsigned integral"); + static_assert(sizeof(Arg) <= sizeof(std::size_t), "Width argument type must be at most as big as std::size_t"); + minWidth = nth_argument<*spec.minWidth>(std::forward(args)...); + } + optional_int precision = spec.precision; + if constexpr (spec.precisionAsArg) + { + using Arg = std::decay_t>; + static_assert(std::unsigned_integral, "Precision argument must be an unsigned integral"); + static_assert(sizeof(Arg) <= sizeof(std::size_t), "Precision argument type must be at most as big as std::size_t"); + precision = nth_argument<*spec.precision>(std::forward(args)...); + } + auto&& arg = nth_argument(std::forward(args)...); + return formatter_with_fallback>::template format(out, arg, minWidth, precision); + }; + + (([&]{ + if constexpr (is_str>) + { + out(parts); + } + else + { + format_arg(holder{}); + } + })(), ...); + } + + private: + template + consteval static void checkArgCount() + { + if constexpr (i != argCount) + { + static_assert(i > argCount, "Not enough arguments passed."); + static_assert(i < argCount, "Too many arguments passed."); + } + } + + consteval static void checkArgsUsed() + { + checkArgsUsed(std::make_index_sequence()); + } + + template + consteval static void checkArgsUsed(std::index_sequence) + { + (checkArgUsed(), ...); + } + + template + consteval static void checkArgUsed() + { + assertArgumentUsed>) + { + return false; + } + else + { + return parts.argIndex == i || (parts.widthAsArg && parts.minWidth == i) || (parts.precisionAsArg && parts.precision == i); + } + }() || ...)>(); + } + + static_assert((checkArgsUsed(), true)); + }; + + template + consteval auto parseSpec() noexcept + { + constexpr auto specResult = parseSpec(fmt, pos, nextArg); + static_assert(checkValidConversion(specResult.spec.conversion), "Invalid conversion specifier"); + return specResult; + } + + template + consteval auto parseFormat() noexcept + { + if constexpr (pos >= fmt.size()) + { + return format_template{}; + } + else + { + constexpr auto nextSpec = std::string_view{fmt}.find('%', pos); + if constexpr (nextSpec == std::string_view::npos) + { + return parseFormat{})>(); + } + else + { + static_assert(nextSpec + 1 < fmt.size(), "Incomplete format specifier at end of format string"); + + constexpr auto specStart = fmt[nextSpec + 1]; + if constexpr (specStart == '%') + { + return parseFormat{}), str{"%"}>(); + } + else + { + constexpr auto result = parseSpec(); + return parseFormat{}), result.spec>(); + } + } + } + } + + template + consteval auto operator ""_format_to() noexcept + { + return parseFormat(); + } + } + } + + using compile_time::operator ""_format_to; +} diff --git a/include/cxxformat/cxxformat b/include/cxxformat/cxxformat new file mode 100644 index 0000000..6da253a --- /dev/null +++ b/include/cxxformat/cxxformat @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include +#include +#include diff --git a/include/cxxformat/file_ptr.hpp b/include/cxxformat/file_ptr.hpp new file mode 100644 index 0000000..da516b0 --- /dev/null +++ b/include/cxxformat/file_ptr.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include + +namespace format { + namespace { + class FilePtrOutput : public FormatOutputBase { + FILE* to; + + public: + FilePtrOutput(FILE* to) noexcept : to{to} {} + + void output(std::string_view what) const + { + fwrite(what.data(), what.size(), 1, to); + } + }; + } + +} +[[maybe_unused]] format::FilePtrOutput make_output(FILE* to) noexcept { return {to}; } diff --git a/include/cxxformat/formatters.hpp b/include/cxxformat/formatters.hpp new file mode 100644 index 0000000..a5c92ed --- /dev/null +++ b/include/cxxformat/formatters.hpp @@ -0,0 +1,486 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace format { + namespace { + constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int minWidth) + { + if(subject.length() < minWidth.value_or(0) && !leftJustified) + { + out(padding, *minWidth - subject.length()); + } + + out(subject); + + if(subject.length() < minWidth.value_or(0) && leftJustified) + { + out(padding, *minWidth - subject.length()); + } + } + } + + using invalid_argument = std::invalid_argument; + + template + struct formatter { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) + { + int base = 10; + switch(spec.conversion) + { + case 'v': + break; + case 'c': + { + // TODO: see static_assert version + char converted = static_cast(static_cast(t)); + formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); + return; + } + case 'i': case 'd': + if constexpr (std::unsigned_integral) + { + throw invalid_argument{"%i and %d are not supported for unsigned integrals"}; + } + break; + case 'o': + base = 8; + if constexpr (std::signed_integral) + { + throw invalid_argument{"%o is not supported for signed integrals"}; + } + break; + case 'x': case 'X': + base = 16; + if constexpr (std::signed_integral) + { + throw invalid_argument{"%x and %X are not supported for signed integrals"}; + } + break; + case 'u': + if constexpr (std::signed_integral) + { + throw invalid_argument{"%u is not supported for signed integrals"}; + } + break; + default: + throw invalid_argument{std::string{"The given integral type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + } + + std::string_view converted; + std::array::digits + 2) / 3> result; + if (precision != 0 || t != 0) + { + const auto [end, ec] = std::to_chars(result.data(), result.data() + result.size(), t, base); + assert(ec == std::errc{}); + converted = {result.data(), end}; + } + const bool hasMinusSign = !std::unsigned_integral && converted.starts_with('-'); + if (hasMinusSign) + { + converted.remove_prefix(1); + } + + if (spec.conversion == 'X') + { + std::transform(result.begin(), result.end(), result.data(), [](char c) + { + return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; + }); + } + + char signPrefix{'\0'}; + std::string_view prefix; + if (hasMinusSign) + { + prefix = "-"; + } + else if (base != 10 && spec.alternative) + { + if (spec.conversion == 'o') + { + prefix = "0"; + } + else if (precision != 0 || t != 0) + { + if (spec.conversion == 'x') + { + prefix = "0x"; + } + else if (spec.conversion == 'X') + { + prefix = "0X"; + } + } + } + else if (spec.addSign != '\0') + { + signPrefix = spec.addSign; + prefix = {&signPrefix, 1}; + } + + std::size_t precisionPadding{0}; + std::size_t normalPadding{0}; + if (precision) + { + if (*precision > converted.size()) + { + precisionPadding = *precision - converted.size(); + } + } + else if (minWidth && (spec.alternative && base != 10 && spec.padding == '0')) + { + const auto totalLength = converted.size() + prefix.size(); + if (*minWidth > totalLength) + { + precisionPadding = *minWidth - totalLength; + minWidth.clear(); + } + } + + if (minWidth) + { + const auto totalLength = converted.size() + precisionPadding + prefix.size(); + if (*minWidth > totalLength) + { + normalPadding = *minWidth - totalLength; + } + } + + const auto padding = precision ? ' ' : spec.padding; + if(!spec.leftJustified) + { + out(padding, normalPadding); + } + + out(prefix); + out('0', precisionPadding); + + out(converted); + + if(spec.leftJustified) + { + out(padding, normalPadding); + } + return; + } + + template + static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + { + constexpr auto conv = spec.conversion; + if constexpr (conv == 'c') + { + // TODO: what is the right thing to do here? + // static_assert(std::same_as, "%c only accepts int as argument"); + } + else if constexpr (conv == 'i' || conv == 'd') + { + static_assert(std::signed_integral, "%i and %d only accept signed integral types"); + } + else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u') + { + static_assert(std::unsigned_integral, "%o, %x, %X and %u only accept unsigned integral types"); + } + else if constexpr (conv != 'v') + { + static_assert(sizeof(T) == 0, "Unsupported conversion specified"); + } + + static_assert(spec.addSign == '\0' || conv == 'i' || conv == 'd', "‘+’ and ‘ ’ (sign related) flags are only allowed for %i and %d"); + static_assert(!spec.alternative || conv == 'o' || conv == 'x' || conv == 'X', "‘#’ (alternative) flag is only allowed for %x, %X and %o"); + + return format(out, t, spec, minWidth, precision); + } + }; + + // NOTE: Difference to printf: uses std::to_chars’ minimal width for exact representation when no precision is specified instead of default precision of 6 + template + struct formatter { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int minWidth, optional_int precision) + { + std::chars_format format; + bool uppercase = false; + switch(spec.conversion) + { + case 'A': + uppercase = true; + [[fallthrough]]; + case 'a': + format = std::chars_format::hex; + break; + case 'E': + uppercase = true; + [[fallthrough]]; + case 'e': + format = std::chars_format::scientific; + break; + case 'F': + uppercase = true; + [[fallthrough]]; + case 'f': + format = std::chars_format::fixed; + break; + case 'G': + uppercase = true; + [[fallthrough]]; + case 'g': + case 'v': + format = std::chars_format::general; + break; + default: + throw invalid_argument{std::string{"The given floating point type does not support conversion specifier ‘"} + spec.conversion + std::string{"’"}}; + } + + std::array::digits10 * 2 + 6)> result; + const auto [end, ec] = precision ? std::to_chars(result.data(), result.data() + result.size(), t, format ,*precision) : std::to_chars(result.data(), result.data() + result.size(), t, format); + assert(ec == std::errc{}); + std::string_view converted{result.data(), end}; + + const bool hasMinusSign = converted.starts_with('-'); + if (hasMinusSign) + { + converted.remove_prefix(1); + } + + std::string_view mantissa = converted; + std::string_view exponent; + if (const auto exponentPos = converted.find(format == std::chars_format::hex ? 'p' : 'e'); exponentPos != std::string_view::npos) + { + mantissa = converted.substr(0, exponentPos); + exponent = converted.substr(exponentPos); + } + + const bool hasDot = mantissa.find('.') != std::string_view::npos; + const bool addDot = spec.alternative && !hasDot; + + std::size_t extraZeros{0}; + if (spec.alternative && format == std::chars_format::general && precision) + { + const auto currentMantissaPrecision = mantissa.size() - (hasDot ? 1 : 0); + if (*precision > currentMantissaPrecision) + { + extraZeros = *precision - currentMantissaPrecision; + } + } + + if (uppercase) + { + std::transform(result.data(), end, result.data(), [](char c) + { + return (c >= 'a' && c <= 'z') ? c + ('A' - 'a') : c; + }); + } + + const char signPrefix = hasMinusSign ? '-' : spec.addSign; + std::string_view hexPrefix; + if (format == std::chars_format::hex && std::isfinite(t)) + { + if (spec.conversion == 'A') + { + hexPrefix = "0X"; + } + else + { + hexPrefix = "0x"; + } + } + + std::size_t paddingLength{0}; + if (minWidth) + { + const auto totalLength = (signPrefix != '\0' ? 1 : 0) + hexPrefix.size() + converted.size() + (addDot ? 1 : 0) + extraZeros; + if (*minWidth > totalLength) + { + paddingLength = *minWidth - totalLength; + } + } + + if(!spec.leftJustified && spec.padding != '0') + { + out(spec.padding, paddingLength); + } + + if (signPrefix != '\0') + { + out(signPrefix); + } + out(hexPrefix); + + if(!spec.leftJustified && spec.padding == '0') + { + out(spec.padding, paddingLength); + } + + out(mantissa); + if (addDot) + { + out('.'); + } + out('0', extraZeros); + out(exponent); + + if(spec.leftJustified) + { + out(spec.padding, paddingLength); + } + return; + } + + template + static constexpr void format(const format_output auto& out, T t, optional_int minWidth, optional_int precision) + { + static_assert(std::string_view{"fFeEaAgGv"}.find(spec.conversion) != std::string_view::npos, "Unsupported conversion specified for floating point type"); + return format(out, t, spec, minWidth, precision); + } + }; + + template + concept stringy = requires(T t) { std::string_view{t}; }; + + template + struct formatter { + static constexpr void format(const format_output auto& out, const S& s, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (spec.conversion != 's' && spec.conversion != 'v') + { + throw invalid_argument{"The given stringy type only supports the ‘s’ and ‘v’ conversions"}; + } + std::string_view val(s); + if (precision) + { + val = val.substr(0, *precision); + } + + formatPadded(out, val, ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, const S& s, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 's' || spec.conversion == 'v', "The given stringy type only supports the ‘s’ and ‘v’ conversions"); + return format(out, s, spec, minWidth, precision); + } + }; + + template + concept pointer = std::is_pointer_v; + + template + struct formatter { + static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (t == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + constexpr format_specifier spec{.conversion = 'x', .alternative = true}; + formatter::template format(out, std::bit_cast(t), minWidth, std::nullopt); + } + } + + template + static constexpr void format(const format_output auto& out, const std::remove_pointer_t* t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); + return format(out, t, spec, minWidth, precision); + } + }; + + template<> + struct formatter { + static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int minWidth, optional_int precision) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 'v', "The given pointer type only supports the ‘p’ and ‘v’ conversions"); + return format(out, nullptr, spec, minWidth, precision); + } + }; + + template<> + struct formatter { + static constexpr void format(const format_output auto& out, const char* s, format_specifier spec, optional_int minWidth, optional_int precision) + { + if (spec.conversion == 'p') + { + formatter::format(out, static_cast(s), spec, minWidth, precision); + } + else + { + assert(spec.conversion == 's' || spec.alternative == 'v'); + + if (s == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + formatter::format(out, s, spec, minWidth, precision); + } + } + } + + template + static constexpr void format(const format_output auto& out, const char* s, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'p' || spec.conversion == 's' || spec.conversion == 'v', "const char* only supports the ‘p’, ‘s’ and ‘v’ conversions"); + return format(out, s, spec, minWidth, precision); + } + }; + + template<> + struct formatter : formatter {}; + + template + concept ostreamable = requires(T t, std::ostream& out) { {out << t} -> std::same_as; }; + + template + concept has_ostream = requires(T t) { {t.stream()} -> std::same_as; }; + + template + struct formatter { + static void format(const format_output auto& out, const T& t, format_specifier spec, optional_int minWidth, optional_int) + { + if constexpr (has_ostream) + { + if (!minWidth) + { + out.stream() << t; + return; + } + } + + std::ostringstream stream; + stream << t; + formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth); + } + + template + static constexpr void format(const format_output auto& out, const T& t, optional_int minWidth, optional_int precision) + { + static_assert(spec.conversion == 'v' || spec.conversion == 's', "The operator<<(std::ostream&) fallback only supports conversions ‘s’ and ‘v’"); + static_assert(!spec.precision, "The operator<<(std::ostream&) fallback does not support specifying an precision"); + static_assert(spec.addSign == '\0', "The operator<<(std::ostream&) fallback does not support the add sign (‘+‘) flag"); + static_assert(!spec.alternative, "The operator<<(std::ostream&) fallback does not support the alternative (‘#‘) flag"); + static_assert(spec.padding == ' ', "The operator<<(std::ostream&) fallback does not support padding with 0"); + static_assert(spec.length == format_specifier::Length::None, "The operator<<(std::ostream&) fallback does not support specifying any length"); + return format(out, t, spec, minWidth, precision); + } + }; +} diff --git a/include/cxxformat/helpers.hpp b/include/cxxformat/helpers.hpp new file mode 100644 index 0000000..73cb0be --- /dev/null +++ b/include/cxxformat/helpers.hpp @@ -0,0 +1,260 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace detail { + template + class FunctorHelper : T { + using T::operator(); + }; +} + +template +concept functor = requires { + std::declval>(); +}; + +template +class overloaded_callable : public T... +{ +public: + overloaded_callable(T&&... bases) : T{std::forward(bases)}... {} + using T::operator()...; +}; + +template +struct nth_argument_s +{ + template + constexpr decltype(auto) operator()(Arg&& arg, Args&&... args) const noexcept + { + static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); + if constexpr (i == 0) + { + return std::forward(arg); + } + else + { + return nth_argument_s{}(std::forward(args)...); + } + } +}; + +template +constexpr inline auto nth_argument = nth_argument_s{}; + +template +struct nth_type_s +{ + static_assert(i < sizeof...(Args) + 1, "index must be smaller than the number of arguments"); + using type = typename nth_type_s::type; +}; + +template +struct nth_type_s<0, Arg, Args...> +{ + using type = Arg; +}; + +template +using nth_type = typename nth_type_s::type; + +template +struct holder {}; + +namespace helper_concepts { +template class optional> +concept optional_ctor_ok = requires { + !std::is_constructible_v&>; + !std::is_constructible_v&>; + !std::is_constructible_v&&>; + !std::is_constructible_v&&>; + !std::is_convertible_v&, T>; + !std::is_convertible_v&, T>; + !std::is_convertible_v&&, T>; + !std::is_convertible_v&&, T>; +}; +} + +template +struct structural_optional { + + bool hasValue; + union { + char empty; // structural types can not have uninitialized memory + T value; + }; + + constexpr structural_optional() noexcept : hasValue{false}, empty{} {} + constexpr structural_optional(std::nullopt_t) noexcept : hasValue{false}, empty{} {} + constexpr structural_optional(const T& value) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{true}, value{value} {} + constexpr structural_optional(T&& value) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{true}, value{std::move(value)} {} + template + constexpr structural_optional(std::in_place_t, Args&&... value) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v) : hasValue{true}, value{std::forward(value)...} {} + template + constexpr structural_optional(U&& value) noexcept(std::is_nothrow_convertible_v) requires(std::is_convertible_v) : hasValue{true}, value{std::forward(value)} {} + template + constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} + { + if (hasValue) + { + std::construct_at(&value, *other); + } + } + template + constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_constructible_v) requires(std::is_constructible_v && helper_concepts::optional_ctor_ok) : hasValue{other}, empty{} + { + if (hasValue) + { + std::construct_at(&value, std::move(*other)); + } + } + + constexpr structural_optional(const structural_optional& other) noexcept(std::is_nothrow_copy_constructible_v) requires(std::copy_constructible) : hasValue{other.hasValue}, empty{} + { + if (hasValue) + { + std::construct_at(&value, other.value); + } + } + constexpr structural_optional(structural_optional&& other) noexcept(std::is_nothrow_move_constructible_v) requires(std::move_constructible) : hasValue{other.hasValue}, empty{} + { + if (hasValue) + { + std::construct_at(&value, std::move(other.value)); + } + } + + constexpr void clear() + { + if (hasValue) + { + value.~T(); + hasValue = false; + empty = {}; + } + } + + explicit constexpr operator bool() const noexcept { return hasValue; } + constexpr T& operator*() noexcept + { + assert(hasValue); + return value; + } + + constexpr const T& operator*() const noexcept + { + assert(hasValue); + return value; + } + + constexpr structural_optional& operator=(const T& val) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) + { + emplace(val); + return *this; + } + + constexpr structural_optional& operator=(T&& val) noexcept(std::is_nothrow_move_constructible_v) requires(std::assignable_from) + { + emplace(std::move(val)); + return *this; + } + + constexpr structural_optional& operator=(const structural_optional& other) noexcept(std::is_nothrow_copy_assignable_v) requires(std::assignable_from) + { + if (other.hasValue) + { + emplace(other.value); + } + else + { + clear(); + } + return *this; + } + + constexpr structural_optional& operator=(structural_optional&& other) noexcept(std::is_nothrow_move_assignable_v) requires(std::assignable_from) + { + if (other.hasValue) + { + emplace(std::move(other.value)); + } + else + { + clear(); + } + return *this; + } + + + constexpr const T& value_or(const T& fallback) const noexcept { return hasValue ? value : fallback; } + + constexpr bool operator==(const structural_optional& other) const + { + return hasValue == other.hasValue && (!hasValue || value == other.value); + } + + constexpr bool operator==(const T& other) const + { + return hasValue && value == other; + } + + template + constexpr T& emplace(Args&&... args) noexcept + { + clear(); + hasValue = true; + std::construct_at(&value, std::forward(args)...); + return value; + } + + constexpr ~structural_optional() + { + clear(); + } +}; + +template +struct optional_int { + T val; + + constexpr optional_int() noexcept : val{emptyVal} {} + constexpr optional_int(std::nullopt_t) noexcept : val{emptyVal} {} + constexpr optional_int(T val) noexcept : val{val} {} + constexpr optional_int(const optional_int&) noexcept = default; + template U> + constexpr optional_int(const optional_int& other) noexcept : val{other ? other.val : emptyVal} {} + constexpr optional_int& operator=(const optional_int&) noexcept = default; + template U> + constexpr optional_int& operator=(optional_int other) noexcept + { + val = other ? other.val : emptyVal; + return *this; + } + + static constexpr inline T emptyVal{std::signed_integral ? std::numeric_limits::min() : std::numeric_limits::max()}; + + constexpr void clear() noexcept + { + val = emptyVal; + } + + constexpr explicit operator bool() const noexcept { return val != emptyVal; } + constexpr bool empty() const noexcept { return !*this; } + + constexpr bool operator==(const optional_int& other) const noexcept = default; + constexpr bool operator==(T other) const noexcept + { + return !empty() && val == other; + } + constexpr T& emplace() noexcept { return val = 0; } + constexpr T& operator*() noexcept { return val; } + constexpr T operator*() const noexcept { return val; } + constexpr T value_or(T fallback) const noexcept { return empty() ? fallback : val; } +}; diff --git a/include/cxxformat/ostream.hpp b/include/cxxformat/ostream.hpp new file mode 100644 index 0000000..308b670 --- /dev/null +++ b/include/cxxformat/ostream.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace format { + namespace { + class OstreamOutput : public FormatOutputBase { + std::ostream& to; + + public: + OstreamOutput(std::ostream& to) noexcept : to{to} {} + + void output(std::string_view what) const + { + to << what; + } + + std::ostream& stream() const noexcept { return to; } + }; + } + +} +namespace std { + [[maybe_unused]] format::OstreamOutput make_output(std::ostream& to) noexcept { return {to}; } +} diff --git a/include/cxxformat/string.hpp b/include/cxxformat/string.hpp new file mode 100644 index 0000000..06cb620 --- /dev/null +++ b/include/cxxformat/string.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +namespace format { + namespace { + namespace compile_time { + template + consteval auto operator ""_format() noexcept + { + return [formatter = parseFormat()](Args&&... args) -> std::string + { + std::ostringstream out; + formatter(out, std::forward(args)...); + return out.str(); + }; + } + } + } + + using compile_time::operator ""_format; +} -- cgit v1.2.3-54-g00ecf