diff options
| -rw-r--r-- | cxxformat.hpp | 656 | ||||
| -rw-r--r-- | cxxformat/core.hpp | 582 | ||||
| -rw-r--r-- | cxxformat/cxxformat.hpp | 7 | ||||
| -rw-r--r-- | cxxformat/file_ptr.hpp | 23 | ||||
| -rw-r--r-- | cxxformat/formatters.hpp | 480 | ||||
| -rw-r--r-- | cxxformat/helpers.hpp | 259 | ||||
| -rw-r--r-- | cxxformat/ostream.hpp | 27 | ||||
| -rw-r--r-- | cxxformat/string.hpp | 25 | ||||
| -rw-r--r-- | main.cpp | 144 |
9 files changed, 1501 insertions, 702 deletions
diff --git a/cxxformat.hpp b/cxxformat.hpp deleted file mode 100644 index 47c0cd7..0000000 --- a/cxxformat.hpp +++ /dev/null @@ -1,656 +0,0 @@ -/* -Copyright 2019 Mittendrein Markus - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ - -#pragma once -#include <string> -#include <string_view> -#include <optional> -#include <cstring> -#include <stdexcept> -#include <cstdint> -#include <concepts> - -namespace format { - -using FormatException = std::invalid_argument; - -template<typename To, typename From, typename Enable = void> -struct FormatConvert {}; - -template<typename To, typename From> -struct ImplicitConversion -{ - static constexpr To convert(From from) - { - return from; - } -}; - -template<std::signed_integral From> -struct FormatConvert<std::intmax_t, From> : ImplicitConversion<std::intmax_t, From> {}; - -template<std::unsigned_integral From> -struct FormatConvert<std::intmax_t, From, std::enable_if_t<(sizeof(From) < sizeof(std::intmax_t))>> : ImplicitConversion<std::intmax_t, From> {}; - -template<std::unsigned_integral From> -struct FormatConvert<std::uintmax_t, From> : ImplicitConversion<std::uintmax_t, From> {}; - -template<std::convertible_to<std::string_view> String> -struct FormatConvert<std::string_view, String> : ImplicitConversion<std::string_view, const String &> {}; - -template<std::floating_point From> -struct FormatConvert<long double, From> : ImplicitConversion<long double, const From &> {}; - -template<typename Pointer> -struct FormatConvert<const void *, Pointer, std::enable_if_t<std::is_pointer_v<Pointer> && !std::is_array_v<std::remove_pointer_t<Pointer>>>> : ImplicitConversion<const void *, const Pointer> {}; - -template<typename Pointer> -struct FormatConvert<const void *, Pointer, std::enable_if_t<std::is_pointer_v<Pointer> && std::is_array_v<std::remove_pointer_t<Pointer>>>> : ImplicitConversion<const void*, const Pointer> {}; - -template<typename From, typename Enable = void> -struct AutoConversion -{ - static bool conversion() - { - throw FormatException{""}; - } -}; - -template<char Conversion, typename From> -struct SimpleAutoConversion -{ - static constexpr char conversion() - { - return Conversion; - } -}; - -template<std::unsigned_integral From> -struct AutoConversion<From> : SimpleAutoConversion<'u', From> {}; - -template<std::signed_integral From> -struct AutoConversion<From> : SimpleAutoConversion<'d', From> {}; - -template<std::floating_point From> -struct AutoConversion<From> : SimpleAutoConversion<'G', From> {}; - -template<std::convertible_to<std::string_view> From> -struct AutoConversion<From> : SimpleAutoConversion<'s', From> {}; - -template<> -struct AutoConversion<char> : SimpleAutoConversion<'c', const char> {}; - -namespace detail -{ - template<typename T, bool Ok = false> - struct StointHelper - { - static T convert(const std::string &, std::size_t *, int) - { - static_assert(Ok, "stoint: Unsupported type"); - return T{}; - } - }; - - template<typename T, T(converter)(const std::string &, std::size_t *, int)> - struct StointHelperConverter - { - static T convert(const std::string &str, std::size_t *pos, int base) - { - return converter(str, pos, base); - } - }; - - template<> - struct StointHelper<int> : StointHelperConverter<int, std::stoi> {}; - template<> - struct StointHelper<long> : StointHelperConverter<long, std::stol> {}; - template<> - struct StointHelper<long long> : StointHelperConverter<long long, std::stoll> {}; - template<> - struct StointHelper<unsigned long> : StointHelperConverter<unsigned long, std::stoul> {}; - template<> - struct StointHelper<unsigned long long> : StointHelperConverter<unsigned long long, std::stoull> {}; - - template<typename T> - T stoint(const std::string &str, std::size_t *pos = nullptr, int base = 10) - { - return StointHelper<T>::convert(str, pos, base); - } - - template<typename... Args> - std::string strprintf(const char *fmt, Args... args) - { - int size = std::snprintf(nullptr, 0, fmt, args...) + 1; - std::string result; - result.resize(size); - std::snprintf(result.data(), size, fmt, args...); - result.resize(size - 1); - return result; - } - - template<typename To, typename From, bool allowFallback, typename Enable = void> - struct FormatConvertHelper - { - static constexpr To convert(const From &) - { - static_assert(allowFallback, "Impossible conversion needed"); - throw FormatException{""}; - } - }; - - template<typename T, bool allowFallback> - struct FormatConvertHelper<T, T, allowFallback> - { - static constexpr const T &convert(const T &from) - { - return from; - } - }; - - template<typename To, typename From, bool allowFallback> - struct FormatConvertHelper<To, From, allowFallback, std::enable_if_t<!std::is_same_v<To, From> && std::is_same_v<std::remove_cvref_t<decltype(FormatConvert<To, std::remove_cvref_t<From>>::convert(std::declval<From>()))>, std::remove_cvref_t<To>>>> - { - static constexpr To convert(const From &from) - { - return FormatConvert<To, std::remove_cvref_t<From>>::convert(from); - } - }; - - template<typename To, bool allowFallback = true, typename From> - auto convert(const From &from, const char *toName, std::size_t index = 0) - { - try - { - return FormatConvertHelper<To, From, allowFallback>::convert(from); - } - catch (const FormatException &) - { - throw FormatException{"Argument#" + std::to_string(index) + " is not convertible to " + std::string{toName}}; - } - } - - template<typename From, typename Enable = void> - struct StringOrStringViewHelper - { - using type = std::string_view; - }; - - template<typename From> - struct StringOrStringViewHelper<From, std::enable_if_t<std::is_same_v<std::string, std::decay_t<decltype(FormatConvert<std::string, From>::convert(std::declval<From>()))>>>> - { - using type = std::string; - }; - - template<typename From> - using string_or_string_view = typename StringOrStringViewHelper<From>::type; - - template<bool allowPointer = false, bool allowFallback = true, typename From> - constexpr char autoConversionSpecifier(std::size_t index = 0) - { - try - { - if constexpr (!allowFallback) - { - if constexpr(!std::is_same_v<char, decltype(AutoConversion<std::remove_cvref_t<From>>::conversion())>) - { - if constexpr (std::is_pointer_v<From>) - { - if constexpr (allowPointer) - { - return 'p'; - } - else - { - if constexpr (!allowFallback) static_assert(allowPointer, "%v doesn't allow automatic conversions for pointers; use %V to alsow allow arbitrary pointers"); - else throw FormatException{"Argument#" + std::to_string(index) + " is not applicable for auto conversion with %v, use %V to also allow arbitrary pointers"}; - } - } - } - } - return AutoConversion<std::remove_cvref_t<From>>::conversion(); - } - catch (const FormatException &) - { - if constexpr (std::is_pointer_v<From>) - { - if constexpr (allowPointer) - { - return 'p'; - } - else - { - if constexpr (!allowFallback) static_assert(allowPointer, "%v doesn't allow automatic conversions for pointers; use %V to alsow allow arbitrary pointers"); - throw FormatException{"Argument#" + std::to_string(index) + " is not applicable for auto conversion with %v, use %V to also allow arbitrary pointers"}; - } - } - throw FormatException{"Argument#" + std::to_string(index) + " is not applicable for auto conversion (%v / %V)"}; - } - } - - std::string format(std::string_view fmt, const std::size_t argumentIndex) - { - const auto fmtSize = fmt.size(); - for (std::size_t i = 0; i < fmtSize; ++i) - { - if (fmt[i] == '%') - { - if (i >= fmtSize - 1) - { - throw FormatException{"Incomplete directive"}; - } - - if (fmt[++i] == '%') - { - return std::string{fmt.substr(0, i)} + format(fmt.substr(i + 1), argumentIndex); - } - - throw FormatException{"Not enough arguments passed"}; - } - } - return std::string{fmt}; - } - - template<typename Arg, typename... Args> - std::string format(std::string_view fmt, const std::size_t argumentIndex, Arg &&arg, Args &&... args) - { - const auto fmtSize = fmt.size(); - for (std::size_t i = 0; i < fmtSize; ++i) - { - auto checkFmtLength = [i, fmtSize] - { - if (i >= fmtSize - 1) - { - throw FormatException{"Incomplete directive"}; - } - }; - const auto current = fmt[i]; - if (current == '%') - { - const auto directiveStart = i; - checkFmtLength(); - std::string specification{"%"}; - - if (fmt[i + 1] == '%') // handle %% here - { - return std::string{fmt.substr(0, directiveStart + 1)} + format(fmt.substr(i + 1), argumentIndex, std::forward<Arg>(arg), std::forward<Args>(args)...); - } - while(i < fmtSize - 1) - { - switch (const auto flag = fmt[++i]) - { - case '#': case '0': case '-': case ' ': case '+': - specification.push_back(flag); - checkFmtLength(); - break; - default: - --i; - goto afterFlagLoop; - } - } - afterFlagLoop: - - std::string tempNumber; - // field width - while (std::isdigit(fmt[++i])) - { - tempNumber.push_back(fmt[i]); - checkFmtLength(); - } - --i; - const auto fieldWidth = tempNumber.empty() ? 0 : stoint<std::size_t>(tempNumber); - tempNumber.clear(); - - std::optional<std::size_t> precision; - // precision - if (fmt[++i] == '.') - { - checkFmtLength(); - while (std::isdigit(fmt[++i])) - { - tempNumber.push_back(fmt[i]); - checkFmtLength(); - } - precision = tempNumber.empty() ? 0 : stoint<std::size_t>(tempNumber); - } - --i; - - std::string conversion{fmt[++i]}; - if(conversion.front() == 'v') - { - conversion = autoConversionSpecifier<false, true, Arg>(argumentIndex); - } - else if(conversion.front() == 'V') - { - conversion = autoConversionSpecifier<true, true, Arg>(argumentIndex); - } - - auto continueFormatting = [directiveStart, &args..., fmt, &specification, &fieldWidth, &precision, i, conversion, argumentIndex](auto length, auto converted) - { - if (fieldWidth) specification += std::to_string(fieldWidth); - if (precision) specification += '.' + std::to_string(*precision); - if (length) specification += length; - specification += conversion; - return std::string{fmt.substr(0, directiveStart)} + strprintf(specification.c_str(), converted) + format(fmt.substr(i + 1), argumentIndex + 1, std::forward<Args>(args)...); - }; - - switch (conversion.back()) - { - case 'c': - return continueFormatting(false, convert<char>(std::forward<Arg>(arg), "character", argumentIndex)); - - case 'd': case 'i': - return continueFormatting('j', convert<std::intmax_t>(std::forward<Arg>(arg), "signed integer", argumentIndex)); - - case 'u': case 'o': case 'x': case 'X': - return continueFormatting('j', convert<std::uintmax_t>(std::forward<Arg>(arg), "unsigned integer", argumentIndex)); - - case 's': - { - const auto& string = convert<string_or_string_view<std::remove_cvref_t<Arg>>>(std::forward<Arg>(arg), "string", argumentIndex); - precision = precision ? std::min(*precision, string.size()) : string.size(); - return continueFormatting(false, string.data()); - } - - case 'e': case 'E': case 'f': case 'F': case 'g': case 'G': case 'a': case 'A': - return continueFormatting(false, convert<double>(std::forward<Arg>(arg), "double", argumentIndex)); - - case 'p': - return continueFormatting(false, convert<const void *>(std::forward<Arg>(arg), "pointer", argumentIndex)); - - default: - throw FormatException{std::string{"Unknown conversion specifier: \""} + conversion + "\""}; - } - } - } - - throw FormatException{std::to_string(argumentIndex - 1) + " placeholders found, but " + std::to_string(argumentIndex + sizeof...(args)) + " arguments passed"}; - } -} - -template<bool NoThrow = false, typename String, typename... Args> -std::string format(const String &fmt, Args &&... args) -{ - const auto fmtStr = detail::convert<std::string_view, false>(fmt, "string"); - try - { - return detail::format(fmtStr, 1, std::forward<Args>(args)...); - } - catch (const FormatException &e) - { - const auto &message = std::string{"format(\""} + std::string{fmtStr} + "\", " + std::to_string(sizeof...(args)) + " more arguments...): " + e.what(); - - if constexpr (NoThrow) return message; - else throw FormatException(message); - } -} - -template<size_t N> -struct str -{ -public: - const char s[N + 1]; - static constexpr size_t size = N; - -private: - template<size_t... indices> - constexpr str<N + 1> appendCharHelper(const char c, std::index_sequence<indices...>) const - { - return str<N + 1>{s[indices]..., c}; - } - - template<size_t... indices> - static constexpr str<N + 1> prependCharHelper(const char c, const str<N> &s, std::index_sequence<indices...>) - { - return str<N + 1>{c, s.s[indices]...}; - } - - template<auto other, size_t... ownIndices, size_t... othersIndices> - constexpr str<N + sizeof...(othersIndices)> appendStrHelper(std::index_sequence<ownIndices...>, std::index_sequence<othersIndices...>) const - { - return str<N + sizeof...(othersIndices)>{s[ownIndices]..., other.s[othersIndices]...}; - } - -public: - template<size_t... indices> - constexpr str(const char (&s)[N + 1], std::index_sequence<indices...>) : s{s[indices]...} - { - - } - - constexpr str(const char (&s)[N + 1]) : str{s, std::make_index_sequence<N + 1>()} - { - - } - - template<typename... Chars> - constexpr str(Chars... chars) : s{chars..., '\0'} - { - static_assert(N == sizeof...(chars), "O.o"); - } - - template<size_t index> - constexpr char get() const - { - static_assert(index < N, "str::get: index out of range"); - return s[index]; - } - - template<size_t offset, size_t... indices> - constexpr str<sizeof...(indices)> extract(std::index_sequence<indices...>) const - { - static_assert(((indices + offset < N) && ...), "str::extract: index out of range"); - return str<sizeof...(indices)>{s[indices + offset]...}; - } - - constexpr str<N + 1> operator+(const char c) const - { - return appendCharHelper(c, std::make_index_sequence<N>()); - } - - template<size_t otherN> - constexpr str<N + otherN> operator+(const str<otherN> &other) const - { - return appendStrHelper<other>(std::make_index_sequence<N>(), std::make_index_sequence<otherN>()); - } - - template<size_t _N> - friend constexpr str<_N + 1> operator+(const char c, const str<_N> &other); -}; - -template<size_t N> -constexpr str<N + 1> operator+(const char c, const str<N> &other) -{ - return str<N>::prependCharHelper(c, other, std::make_index_sequence<N>()); -} - -template<size_t N> -str(const char(&)[N]) -> str<N - 1>; - -template<typename... Chars> -str(Chars... chars) -> str<sizeof...(Chars)>; - -namespace detail -{ - template<size_t i, size_t N> - constexpr char get(const str<N>& s) - { - return s.template get<i>(); - } - - template<size_t N, size_t offset, size_t... indices> - constexpr str<sizeof...(indices)> extractStr(const str<N> &source, std::index_sequence<indices...>) - { - return str<sizeof...(indices)>{get<indices + offset>(source)...}; - } - - template<size_t start, size_t end = std::string::npos, bool endIsIndex = false, size_t N> - constexpr str<end == std::string::npos ? N - start : (endIsIndex ? end + 1 - start : end)> substr(const str<N> &s) - { - static_assert(!endIsIndex || end < N, "substr: end index / substr-length is out of range"); - if constexpr (!endIsIndex && end == 0) return str<0>{}; - else if constexpr (end == std::string::npos) return substr<start, N - 1, true>(s); - else if constexpr (!endIsIndex) return substr<start, start + end - 1, true>(s); - else return s.template extract<start>(std::make_index_sequence<end - start + 1>()); - } - - template<typename T, auto s, size_t i = 0, bool allowSign = true, T parsedPart = 0> - constexpr T strToInt() - { - if constexpr (i == s.size) return parsedPart; - else - { - constexpr char c = get<i>(s); - if constexpr (c == '-') - { - static_assert(allowSign, "strToInt: invalid character found"); - static_assert(std::is_signed_v<T>, "strToInt: negative sign found when converting to an unsigned type"); - return -strToInt<T, s, i + 1, false>(); - } - else if constexpr (c == '+') - { - static_assert(allowSign, "strToInt: invalid character found"); - return strToInt<T, s, i + 1, false>(); - } - else - { - static_assert(c >= '0' && c <= '9', "strToInt: invalid character found"); - return strToInt<T, s, i + 1, false, parsedPart * T{10} + T{c - '0'}>(); - } - } - } - - template<typename T, T N> - constexpr size_t decimalLength() - { - if constexpr (N < 0) return 1 + decimalLength<T, -N>(); - else if constexpr (N < 10) return 1; - else return 1 + decimalLength<T, N / 10>(); - } - - template<typename T, T N> - constexpr str<decimalLength<T, N>()> toStr() - { - if constexpr (N < 0) return '-' + toStr<T, -N>(); - else if constexpr (N < 10) - { - return str<1>{'0' + N}; - } - else return toStr<T, N / 10>() + char(N % 10 + '0'); - } - - template<auto fmt, size_t i, size_t argumentIndex> - std::string format_s() - { - if constexpr (i == fmt.size) return fmt.s; - else if constexpr (get<i>(fmt) == '%') - { - if constexpr (get<i + 1>(fmt) == '%') return substr<0, i, true>(fmt).s + format_s<substr<i + 2>(fmt), 0, argumentIndex>(); - else - { - static_assert(get<i + 1>(fmt) == '%', "Not enough arguments passed"); - throw FormatException{"Not enough arguments passed"}; - } - } - else return format_s<fmt, i + 1, argumentIndex>(); - } - - template<auto fmt, size_t i, size_t offset = 0> - constexpr size_t getFlagEnd() - { - constexpr char next = get<i + offset>(fmt); - if constexpr (next == '#' || next == '0' || next == '-' || next == ' ' || next == '+') return getFlagEnd<fmt, i, offset + 1>(); - else return i + offset; - } - - template<auto fmt, size_t i> - constexpr size_t getNumberEnd() - { - if constexpr (std::isdigit(get<i>(fmt))) return getNumberEnd<fmt, i + 1>(); - else return i; - } - - template<char conversion, size_t argumentIndex, typename Arg> - constexpr char checkReplaceAutoConversion() - { - if constexpr (conversion == 'v' || conversion == 'V') return autoConversionSpecifier<conversion == 'V', false, Arg>(argumentIndex); - else return conversion; - } - - template<bool havePrecision, auto fmt, size_t iAfterFieldWidth, size_t iAfterPrecision, typename String> - size_t determineStringPrecision(const String& string) - { - if constexpr (havePrecision) return std::min(strToInt<size_t, substr<iAfterFieldWidth + 1, iAfterPrecision - 1, true>(fmt)>(), string.size()); - else return string.size(); - } - - template<auto fmt, char conversion, size_t i, size_t iAfterPrecision, char length> - constexpr auto makeDirective() - { - if constexpr (length) return substr<i, iAfterPrecision - 1, true>(fmt) + length + conversion; - else return substr<i, iAfterPrecision - 1, true>(fmt) + conversion; - } - - template<char c, std::size_t size> - consteval void showUnknownConversionSpecifier() - { - static_assert(size == 0, "Unknown conversion specifier!"); - } - - template<auto fmt, size_t i, size_t argumentIndex, typename Arg, typename... Args> - std::string format_s(Arg &&arg, Args &&...args) - { - static_assert(i < fmt.size, "Too many arguments passed"); - if constexpr (i == fmt.size) - { - throw FormatException{std::to_string(argumentIndex - 1) + " placeholders found, but " + std::to_string(argumentIndex + sizeof...(args)) + " arguments passed"}; - } - else if constexpr (get<i>(fmt) == '%') - { - if constexpr (get<i + 1>(fmt) == '%') return substr<0, i, true>(fmt).s + format_s<substr<i + 2>(fmt), 0, argumentIndex>(std::forward<Arg>(arg), std::forward<Args>(args)...); - - constexpr char iAfterFlag = getFlagEnd<fmt, i + 1>(); - constexpr size_t iAfterFieldWidth = getNumberEnd<fmt, iAfterFlag>(); - - constexpr auto havePrecision = get<iAfterFieldWidth>(fmt) == '.'; - constexpr size_t iAfterPrecision = [havePrecision, iAfterFieldWidth]() -> size_t { if constexpr (havePrecision) return getNumberEnd<fmt, iAfterFieldWidth + 1>(); else return iAfterFieldWidth; }(); - - constexpr char conversion = checkReplaceAutoConversion<get<iAfterPrecision>(fmt), argumentIndex, Arg>(); - - if constexpr (conversion == 'c') return substr<0, i>(fmt).s + strprintf(makeDirective<fmt, conversion, i, iAfterPrecision, 0>().s, convert<char, false>(std::forward<Arg>(arg), "character", argumentIndex)) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - else if constexpr (conversion == 'd' || conversion == 'i') return substr<0, i>(fmt).s + strprintf(makeDirective<fmt, conversion, i, iAfterPrecision, 'j'>().s, convert<std::intmax_t, false>(std::forward<Arg>(arg), "signed integer", argumentIndex)) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - else if constexpr (conversion == 'u' || conversion == 'o' || conversion == 'x' || conversion == 'X') return substr<0, i>(fmt).s + strprintf(makeDirective<fmt, conversion, i, iAfterPrecision, 'j'>().s, convert<std::uintmax_t, false>(std::forward<Arg>(arg), "unsigned integer", argumentIndex)) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - else if constexpr (conversion == 'e' || conversion == 'E' || conversion == 'f' || conversion == 'F' || conversion == 'g' || conversion == 'G' || conversion == 'a' || conversion == 'A') return substr<0, i>(fmt).s + strprintf(makeDirective<fmt, conversion, i, iAfterPrecision, 0>().s, convert<double, false>(std::forward<Arg>(arg), "double", argumentIndex)) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - else if constexpr (conversion == 'p') return substr<0, i>(fmt).s + strprintf(makeDirective<fmt, conversion, i, iAfterPrecision, 0>().s, convert<const void *, false>(std::forward<Arg>(arg), "pointer", argumentIndex)) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - else if constexpr (conversion == 's') - { - const auto& string = convert<string_or_string_view<std::remove_cvref_t<Arg>>, false>(std::forward<Arg>(arg), "string", argumentIndex); - return substr<0, i>(fmt).s + strprintf("%.*s", determineStringPrecision<havePrecision, fmt, iAfterFieldWidth, iAfterPrecision>(string), string.data()) + format_s<substr<iAfterPrecision + 1>(fmt), 0, argumentIndex + 1>(std::forward<Args>(args)...); - } - else - { - showUnknownConversionSpecifier<conversion, fmt.size>(); - throw FormatException{std::string{"Unknown conversion specifier: \""} + conversion + "\""}; - } - } - else return format_s<fmt, i + 1, argumentIndex>(std::forward<Arg>(arg), std::forward<Args>(args)...); - } -} - -template<str fmt, typename... Args> -std::string format(Args &&...args) -{ - return detail::format_s<fmt, 0, 1>(std::forward<Args>(args)...); -} -} diff --git a/cxxformat/core.hpp b/cxxformat/core.hpp new file mode 100644 index 0000000..3b665a5 --- /dev/null +++ b/cxxformat/core.hpp @@ -0,0 +1,582 @@ +#pragma once + +#include <algorithm> +#include <array> +#include <bit> +#include <cassert> +#include <charconv> +#include <cmath> +#include <cstddef> +#include <limits> +#include <optional> +#include <stdexcept> +#include <sstream> +#include <string> +#include <string_view> + +#include "helpers.hpp" + +namespace format { + namespace { + template<std::size_t N> + 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<N + 1>()} {} + constexpr str(std::string_view s) noexcept : str{s, std::make_index_sequence<N>()} {} + + 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<std::size_t pos, std::size_t count> + consteval auto substr(holder<pos>, holder<count>) const noexcept + { + static_assert(pos + count <= N); + return str<count>{s, pos, std::make_index_sequence<count>()}; + } + + template<std::size_t begin, std::size_t end> + consteval auto substr(holder<begin, end>) const noexcept + { + return substr(holder<begin>{}, holder<end - begin>{}); + } + + template<std::size_t pos> + consteval auto substr(holder<pos> p) const noexcept + { + return substr(p, holder<N - pos>{}); + } + + constexpr char operator[](std::size_t i) const noexcept + { + return s[i]; + } + + private: + template<std::size_t... indices> + constexpr str(const char (&s)[N + 1], std::index_sequence<indices...>) noexcept : s{s[indices]...} {} + + template<std::size_t M, std::size_t... indices> + constexpr str(const char (&s)[M], std::size_t offset, std::index_sequence<indices...>) noexcept : s{s[indices + offset]..., '\0'} {} + + template<std::size_t... indices> + constexpr str(std::string_view s, std::index_sequence<indices...>) noexcept : s{s[indices]..., '\0'} {} + + friend std::ostream& operator<<(std::ostream& out, const str& s) + { + return out << s.operator std::string_view(); + } + + template<std::size_t M> + friend struct str; + }; + + template<std::size_t N> + str(const char(&)[N]) -> str<N - 1>; + + template<typename FormatOutput> + 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<FormatOutput, FormatOutputBase<FormatOutput>>, "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<const FormatOutput&>(*this); + } + }; + + template<typename T> + concept format_output = std::derived_from<std::remove_cvref_t<T>, FormatOutputBase<std::remove_cvref_t<T>>> && requires(const T o, std::string_view s) { o.output(s); }; + + struct NullOutput : FormatOutputBase<NullOutput> { + 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<ArgIndex> minWidth; + optional_int<ArgIndex> 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<format_output Out> + constexpr decltype(auto) make_output(Out&& out) { return std::forward<Out>(out); } + + template<typename T> + concept format_output_compatible = format_output<T> || requires(T&& o) { {make_output(std::forward<T>(o))} -> format_output; }; + + template<typename T, bool fallback = false> + struct formatter; + + template<typename T> + concept has_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int<std::size_t> z) { + { formatter<T>::format(out, t, spec, z, z) } -> std::same_as<void>; + { formatter<T>::template format<format_specifier{}>(out, t, z, z) } -> std::same_as<void>; + }; + + template<typename T> + concept has_fallback_formatter = requires(T t, format_specifier spec, const NullOutput& out, optional_int<std::size_t> z) { + { formatter<T, true>::format(out, t, spec, z, z) } -> std::same_as<void>; + { formatter<T, true>::template format<format_specifier{}>(out, t, z, z) } -> std::same_as<void>; + }; + + template<typename T> + concept has_some_formatter = has_formatter<T> || has_fallback_formatter<T>; + + namespace { + template<typename T> + struct formatter_with_fallback : formatter<T> {}; + + template<has_fallback_formatter T> requires (!has_formatter<T>) + struct formatter_with_fallback<T> : formatter<T, true> {}; + + template<std::integral T> + constexpr std::optional<T> digit(char c) noexcept + { + if (c >= '0' && c <= '9') + { + return c - '0'; + } + return std::nullopt; + } + + template<std::integral T = int> + constexpr std::pair<std::optional<T>, 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<T> && s[pos] == '-'); + auto startPos = isNegative ? pos + 1 : pos; + if (startPos >= s.size()) + { + return {std::nullopt, pos}; + } + + auto d = digit<T>(s[startPos]); + if (!d) + { + return {std::nullopt, pos}; + } + + T val{*d}; + auto p = startPos + 1; + for (; p < s.size(); ++p) + { + auto nextD = digit<T>(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<std::size_t> 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<ArgIndex>(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<ArgIndex>(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<ArgIndex>(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<ArgIndex>(maxArg + 1)}; + } + + constexpr bool checkValidConversion(char c) noexcept + { + return std::string_view{"diouxXeEfFgGaAcspv"}.find(c) != std::string_view::npos; + } + + namespace compile_time { + template<typename T> + struct is_str_s : std::false_type {}; + + template<std::size_t N> + struct is_str_s<str<N>> : std::true_type {}; + + template<typename T> + concept is_str = is_str_s<T>::value; + + template<typename T> + concept template_part = std::same_as<T, format_specifier> || is_str<T>; + + template<ArgIndex argIndex, bool used> + consteval void assertArgumentUsed() + { + static_assert(used, "Argument is not used (1-based argIndex)"); + } + + template<ArgIndex argCount, template_part auto... parts> + struct format_template { + template<format_output_compatible Output, typename... Args> requires (has_some_formatter<std::decay_t<Args>> && ...) + constexpr void operator()(Output&& output, Args&&... args) const + { + checkArgCount<sizeof...(Args)>(); + + const auto out = make_output(std::forward<Output>(output)); + const auto format_arg = [&args..., &out]<format_specifier spec>(holder<spec>) -> decltype(auto) + { + optional_int<std::size_t> minWidth = spec.minWidth; + if constexpr (spec.widthAsArg) + { + using Arg = std::decay_t<nth_type<*spec.minWidth, Args...>>; + static_assert(std::unsigned_integral<Arg>, "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>(args)...); + } + optional_int<std::size_t> precision = spec.precision; + if constexpr (spec.precisionAsArg) + { + using Arg = std::decay_t<nth_type<*spec.precision, Args...>>; + static_assert(std::unsigned_integral<Arg>, "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>(args)...); + } + auto&& arg = nth_argument<spec.argIndex>(std::forward<Args>(args)...); + return formatter_with_fallback<std::decay_t<decltype(arg)>>::template format<spec>(out, arg, minWidth, precision); + }; + + (([&]{ + if constexpr (is_str<std::decay_t<decltype(parts)>>) + { + out(parts); + } + else + { + format_arg(holder<parts>{}); + } + })(), ...); + } + + private: + template<std::size_t i> + 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<argCount>()); + } + + template<std::size_t... indices> + consteval static void checkArgsUsed(std::index_sequence<indices...>) + { + (checkArgUsed<indices>(), ...); + } + + template<std::size_t i> + consteval static void checkArgUsed() + { + assertArgumentUsed<i + 1, ([]{ + if constexpr (is_str<std::decay_t<decltype(parts)>>) + { + return false; + } + else + { + return parts.argIndex == i || (parts.widthAsArg && parts.minWidth == i) || (parts.precisionAsArg && parts.precision == i); + } + }() || ...)>(); + } + + static_assert((checkArgsUsed(), true)); + }; + + template<str fmt, std::size_t pos, ArgIndex nextArg> + consteval auto parseSpec() noexcept + { + constexpr auto specResult = parseSpec(fmt, pos, nextArg); + static_assert(checkValidConversion(specResult.spec.conversion), "Invalid conversion specifier"); + return specResult; + } + + template<str fmt, std::size_t pos, ArgIndex nextArg, ArgIndex maxArg, template_part auto... parts> + consteval auto parseFormat() noexcept + { + if constexpr (pos >= fmt.size()) + { + return format_template<maxArg, parts...>{}; + } + else + { + constexpr auto nextSpec = std::string_view{fmt}.find('%', pos); + if constexpr (nextSpec == std::string_view::npos) + { + return parseFormat<fmt, nextSpec, nextArg, maxArg, parts..., fmt.substr(holder<pos>{})>(); + } + 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<fmt, nextSpec + 2, nextArg, maxArg, parts..., fmt.substr(holder<pos, nextSpec>{}), str{"%"}>(); + } + else + { + constexpr auto result = parseSpec<fmt, nextSpec + 1, nextArg>(); + return parseFormat<fmt, result.pos, result.nextArg, std::max(maxArg, result.maxArg), parts..., fmt.substr(holder<pos, nextSpec>{}), result.spec>(); + } + } + } + } + + template<str fmt> + consteval auto operator ""_format_to() noexcept + { + return parseFormat<fmt, 0, 0, 0>(); + } + } + } + + using compile_time::operator ""_format_to; +} diff --git a/cxxformat/cxxformat.hpp b/cxxformat/cxxformat.hpp new file mode 100644 index 0000000..3e6fe21 --- /dev/null +++ b/cxxformat/cxxformat.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "core.hpp" +#include "formatters.hpp" +#include "file_ptr.hpp" +#include "ostream.hpp" +#include "string.hpp" diff --git a/cxxformat/file_ptr.hpp b/cxxformat/file_ptr.hpp new file mode 100644 index 0000000..53c8a9a --- /dev/null +++ b/cxxformat/file_ptr.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "core.hpp" + +#include <cstdio> + +namespace format { + namespace { + class FilePtrOutput : public FormatOutputBase<FilePtrOutput> { + 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 new file mode 100644 index 0000000..ad253f6 --- /dev/null +++ b/cxxformat/formatters.hpp @@ -0,0 +1,480 @@ +#pragma once + +#include "core.hpp" + +#include <concepts> + +namespace format { + namespace { + constexpr void formatPadded(const format_output auto& out, std::string_view subject, char padding, bool leftJustified, optional_int<std::size_t> 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<std::integral T> + struct formatter<T> { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision) + { + int base = 10; + switch(spec.conversion) + { + case 'v': + break; + case 'c': + { + // TODO: see static_assert version + char converted = static_cast<char>(static_cast<unsigned char>(t)); + formatPadded(out, {&converted, 1}, ' ', spec.leftJustified, minWidth); + return; + } + case 'i': case 'd': + if constexpr (std::unsigned_integral<T>) + { + throw invalid_argument{"%i and %d are not supported for unsigned integrals"}; + } + break; + case 'o': + base = 8; + if constexpr (std::signed_integral<T>) + { + throw invalid_argument{"%o is not supported for signed integrals"}; + } + break; + case 'x': case 'X': + base = 16; + if constexpr (std::signed_integral<T>) + { + throw invalid_argument{"%x and %X are not supported for signed integrals"}; + } + break; + case 'u': + if constexpr (std::signed_integral<T>) + { + 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<char, (std::numeric_limits<T>::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<T> && 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<format_specifier spec> + static constexpr void format(const format_output auto& out, T t, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision) + { + constexpr auto conv = spec.conversion; + if constexpr (conv == 'c') + { + // TODO: what is the right thing to do here? + // static_assert(std::same_as<T, int>, "%c only accepts int as argument"); + } + else if constexpr (conv == 'i' || conv == 'd') + { + static_assert(std::signed_integral<T>, "%i and %d only accept signed integral types"); + } + else if constexpr (conv == 'o' || conv == 'x' || conv == 'X' || conv == 'u') + { + static_assert(std::unsigned_integral<T>, "%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: no precision uses std::to_chars’ minimal width for exact representation instead of default precision of 6 + template<std::floating_point T> + struct formatter<T> { + static constexpr void format(const format_output auto& out, T t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<char, (std::numeric_limits<T>::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<format_specifier spec> + static constexpr void format(const format_output auto& out, T t, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<typename T> + concept stringy = requires(T t) { std::string_view{t}; }; + + template<stringy S> + struct formatter<S> { + static constexpr void format(const format_output auto& out, const S& s, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<format_specifier spec> + static constexpr void format(const format_output auto& out, const S& s, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<typename T> + concept pointer = std::is_pointer_v<T>; + + template<pointer T> + struct formatter<T> { + static constexpr void format(const format_output auto& out, const std::remove_pointer_t<T>* t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision) + { + if (t == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + constexpr format_specifier spec{.conversion = 'x', .alternative = true}; + formatter<std::uintptr_t>::template format<spec>(out, std::bit_cast<std::uintptr_t>(t), minWidth, std::nullopt); + } + } + + template<format_specifier spec> + static constexpr void format(const format_output auto& out, const std::remove_pointer_t<T>* t, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<std::nullptr_t> { + static constexpr void format(const format_output auto& out, std::nullptr_t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + + template<format_specifier spec> + static constexpr void format(const format_output auto& out, std::nullptr_t, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<const char*> { + static constexpr void format(const format_output auto& out, const char* s, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t> precision) + { + if (spec.conversion == 'p') + { + formatter<void*>::format(out, static_cast<const void*>(s), spec, minWidth, precision); + } + else + { + assert(spec.conversion == 's' || spec.alternative == 'v'); + + if (s == nullptr) + { + formatPadded(out, "(nil)", ' ', spec.leftJustified, minWidth); + } + else + { + formatter<std::string_view>::format(out, s, spec, minWidth, precision); + } + } + } + + template<format_specifier spec> + static constexpr void format(const format_output auto& out, const char* s, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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<char*> : formatter<const char*> {}; + + template<typename T> + concept ostreamable = requires(T t, std::ostream& out) { {out << t} -> std::same_as<std::ostream&>; }; + + template<typename T> + concept has_ostream = requires(T t) { {t.stream()} -> std::same_as<std::ostream&>; }; + + template<ostreamable T> + struct formatter<T, true> { + static void format(const format_output auto& out, const T& t, format_specifier spec, optional_int<std::size_t> minWidth, optional_int<std::size_t>) + { + if constexpr (has_ostream<decltype(out)>) + { + if (!minWidth) + { + out.stream() << t; + return; + } + } + + std::ostringstream stream; + stream << t; + formatPadded(out, stream.view(), ' ', spec.leftJustified, minWidth); + } + + template<format_specifier spec> + static constexpr void format(const format_output auto& out, const T& t, optional_int<std::size_t> minWidth, optional_int<std::size_t> 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 new file mode 100644 index 0000000..c4bad42 --- /dev/null +++ b/cxxformat/helpers.hpp @@ -0,0 +1,259 @@ +#pragma once + +#include <cassert> +#include <concepts> +#include <memory> +#include <optional> +#include <type_traits> +#include <utility> + +namespace detail { + template<typename T> + class FunctorHelper : T { + using T::operator(); + }; +} + +template<typename T> +concept functor = requires { + std::declval<detail::FunctorHelper<T>>(); +}; + +template<functor... T> +class overloaded_callable : public T... +{ +public: + overloaded_callable(T&&... bases) : T{std::forward<T>(bases)}... {} + using T::operator()...; +}; + +template<std::size_t i> +struct nth_argument_s +{ + template<typename Arg, typename... Args> + 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>(arg); + } + else + { + return nth_argument_s<i - 1>{}(std::forward<Args>(args)...); + } + } +}; + +template<std::size_t i> +constexpr inline auto nth_argument = nth_argument_s<i>{}; + +template<std::size_t i, typename Arg, typename... Args> +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<i - 1, Args...>::type; +}; + +template<typename Arg, typename... Args> +struct nth_type_s<0, Arg, Args...> +{ + using type = Arg; +}; + +template<std::size_t i, typename... Args> +using nth_type = typename nth_type_s<i, Args...>::type; + +template<auto... x> +struct holder {}; + +namespace helper_concepts { +template<typename T, typename U, template <typename> class optional> +concept optional_ctor_ok = requires { + !std::is_constructible_v<T, optional<U>&>; + !std::is_constructible_v<T, const optional<U>&>; + !std::is_constructible_v<T, optional<U>&&>; + !std::is_constructible_v<T, const optional<U>&&>; + !std::is_convertible_v<optional<U>&, T>; + !std::is_convertible_v<const optional<U>&, T>; + !std::is_convertible_v<optional<U>&&, T>; + !std::is_convertible_v<const optional<U>&&, T>; +}; +} + +template<typename T> +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<T>) requires(std::copy_constructible<T>) : hasValue{true}, value{value} {} + constexpr structural_optional(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>) requires(std::move_constructible<T>) : hasValue{true}, value{std::move(value)} {} + template<typename... Args> + constexpr structural_optional(std::in_place_t, Args&&... value) noexcept(std::is_nothrow_constructible_v<T, Args...>) requires(std::is_constructible_v<T, Args...>) : hasValue{true}, value{std::forward<Args>(value)...} {} + template<typename U> + constexpr structural_optional(U&& value) noexcept(std::is_nothrow_convertible_v<U&&, T>) requires(std::is_convertible_v<U&&, T>) : hasValue{true}, value{std::forward<U>(value)} {} + template<typename U> + constexpr structural_optional(const structural_optional<U>& other) noexcept(std::is_nothrow_constructible_v<T, const U&>) requires(std::is_constructible_v<T, const U&> && helper_concepts::optional_ctor_ok<T, U, structural_optional>) : hasValue{other}, empty{} + { + if (hasValue) + { + std::construct_at(&value, *other); + } + } + template<typename U> + constexpr structural_optional(structural_optional<U>&& other) noexcept(std::is_nothrow_constructible_v<T, const U&>) requires(std::is_constructible_v<T, U&&> && helper_concepts::optional_ctor_ok<T, U, structural_optional>) : 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<T>) requires(std::copy_constructible<T>) : 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<T>) requires(std::move_constructible<T>) : 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<T>) requires(std::assignable_from<T&, const T&>) + { + emplace(val); + return *this; + } + + constexpr structural_optional& operator=(T&& val) noexcept(std::is_nothrow_move_constructible_v<T>) requires(std::assignable_from<T, T&&>) + { + emplace(std::move(val)); + return *this; + } + + constexpr structural_optional& operator=(const structural_optional& other) noexcept(std::is_nothrow_copy_assignable_v<T>) requires(std::assignable_from<T&, const T&>) + { + if (other.hasValue) + { + emplace(other.value); + } + else + { + clear(); + } + return *this; + } + + constexpr structural_optional& operator=(structural_optional&& other) noexcept(std::is_nothrow_move_assignable_v<T>) requires(std::assignable_from<T, T&&>) + { + 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<typename... Args> + constexpr T& emplace(Args&&... args) noexcept + { + clear(); + hasValue = true; + std::construct_at(&value, std::forward<Args>(args)...); + return value; + } + + constexpr ~structural_optional() + { + clear(); + } +}; + +template<std::integral T> +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<std::convertible_to<T> U> + constexpr optional_int(const optional_int<U>& other) noexcept : val{other ? other.val : emptyVal} {} + constexpr optional_int& operator=(const optional_int&) noexcept = default; + template<std::convertible_to<T> U> + constexpr optional_int& operator=(optional_int<U> other) noexcept + { + val = other ? other.val : emptyVal; + return *this; + } + + static constexpr inline T emptyVal{std::signed_integral<T> ? std::numeric_limits<T>::min() : std::numeric_limits<T>::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 new file mode 100644 index 0000000..830da52 --- /dev/null +++ b/cxxformat/ostream.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "core.hpp" + +#include <iostream> + +namespace format { + namespace { + class OstreamOutput : public FormatOutputBase<OstreamOutput> { + 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 new file mode 100644 index 0000000..c0fa565 --- /dev/null +++ b/cxxformat/string.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "core.hpp" +#include "ostream.hpp" + +#include <sstream> + +namespace format { + namespace { + namespace compile_time { + template<str fmt> + consteval auto operator ""_format() noexcept + { + return [formatter = parseFormat<fmt, 0, 0, 0>()]<typename... Args>(Args&&... args) -> std::string + { + std::ostringstream out; + formatter(out, std::forward<Args>(args)...); + return out.str(); + }; + } + } + } + + using compile_time::operator ""_format; +} @@ -1,54 +1,106 @@ -#include "cxxformat.hpp" -#include <array> -#include <utility> #include <iostream> +#include <sstream> -template<size_t N, typename T> -struct format::FormatConvert<std::string, T[N], std::enable_if_t<!std::is_same_v<std::remove_cvref_t<T>, char>>> +#include "cxxformat/cxxformat.hpp" + +void numberTests() { - template<size_t _N> - static std::string accessConvert(const T *from) - { - if constexpr (_N == N) - { - return "}"; - } - else - { - return format::format("%v", from[_N]) + (_N + 1 < N ? ", " : "") + accessConvert<_N + 1>(from); - } - } - - static std::string convert(const T *from) - { - return "array[" + std::to_string(N) + "]{" + accessConvert<0>(from); - } -}; - -template<size_t N, typename T> -struct format::FormatConvert<std::string, std::array<T, N>> + using format::parseNumber; + static_assert(!parseNumber("", 0).first); + static_assert(!parseNumber("-", 0).first); + static_assert(parseNumber("1", 0).first == 1); + static_assert(parseNumber("10", 0).first == 10); + static_assert(parseNumber("10907", 0).first == 10907); + static_assert(parseNumber("-1", 0).first == -1); + static_assert(parseNumber("-11097", 0).first == -11097); +} + +int main(int argc, char* argv[]) { - static std::string convert(const std::array<T, N> &from) - { - return format::FormatConvert<std::string, T[N]>::convert(from.data()); - } -}; + using namespace format; + "-%20g-\n"_format_to(stdout, 0x1.8p0); + "-%20.0a-\n"_format_to(stdout, 0.0); + "-%#+20.0a-\n"_format_to(stdout, 1.0); + "-%#+20.0e-\n"_format_to(stdout, 1.0); + "-%#+20.0f-\n"_format_to(stdout, 1.0); + "-%#+20.0g-\n"_format_to(stdout, 1.0); + "-%#+20.3g-\n"_format_to(stdout, 1.0); + "-%+20.0a-\n"_format_to(stdout, 1.0); + "-%+20.0e-\n"_format_to(stdout, 1.0); + "-%+20.0f-\n"_format_to(stdout, 1.0); + "-%+20.3g-\n"_format_to(stdout, 1.0); + "-%020.3e-\n"_format_to(stdout, 1.2); + "-%020.3f-\n"_format_to(stdout, 1.2); + "-%020.3g-\n"_format_to(stdout, 1.2); + "-%020.3a-\n"_format_to(stdout, 1.2); + "-%020.3a-\n"_format_to(stdout, -1.2); + "-%20.3a-\n"_format_to(stdout, -1.2); + "-%-20.3a-\n"_format_to(stdout, -1.2); + "-%+020.3a-\n"_format_to(stdout, -1.2); + "-%+020.3a-\n"_format_to(stdout, 1.2); + "-% 020.3a-\n"_format_to(stdout, 1.2); + "-%20.3e-%20.3e-\n"_format_to(stdout, 1.234567890123456789, 1.2); + "-%p-\n"_format_to(stdout, nullptr); + "-%p-\n"_format_to(stdout, argv); + "-%p-\n"_format_to(stdout, argv[0]); + "-%s-\n"_format_to(stdout, argv[0]); + "-%#05x-\n"_format_to(stdout, 10u); + "-%1$5.3s--%1$-5.3s-\n"_format_to(stdout, "Hallo"); + "-%1$5.10s--%1$-5.10s-\n"_format_to(stdout, "Hallo"); + "%1$d %1$-*3$d %1$d %2$d\n"_format_to(stdout, 1, 4, 10u); + "%1$u %*d %1$u %d\n"_format_to(stdout, 5u, 2, 3); + "%1$u %-*d %1$u %d\n"_format_to(stdout, 5u, 2, 3); + "%1$u %0*d %1$u %d\n"_format_to(stdout, 5u, 2, 3); + "%1$u %0-*d %1$u %d\n"_format_to(stdout, 5u, 2, 3); + "%1$u %-0*d %1$u %d\n"_format_to(stdout, 5u, 2, 3); + "%u % d % d %d\n"_format_to(stdout, 5u, 2, -2, 4); + "%u %+d %+d %d\n"_format_to(stdout, 5u, 2, -2, 4); + "%d %.0d %.d %0d\n"_format_to(stdout, 0, 0, 0, 1); + std::cout << "Hallo %%du %1$s %s%c %s %u = %4$#x = %4$#X = %4$#o %f!\n"_format("Knirp", 's', argv[0], 42u, 1.337) << std::flush; + "-%#09x-\n"_format_to(std::cout, 10u); -template<size_t N, typename T> -struct format::AutoConversion<std::array<T, N>> : format::SimpleAutoConversion<'s', const std::array<T, N> &> {}; +#define TEST(fmt, ...) do{ char* plain; asprintf(&plain, fmt, __VA_ARGS__); assert(std::string_view{plain} == operator""_format<fmt>()(__VA_ARGS__)); free(plain); }while(0) -template<size_t N, typename T> -struct format::AutoConversion<T[N], std::enable_if_t<!std::is_same_v<std::remove_cvref_t<T>, char>>> : format::SimpleAutoConversion<'s', const T(&)[N]> {}; + TEST("%d", 1); + TEST("%1$d %1$-*3$d %1$d %2$d\n", 1, 2, 10u); + TEST("%d %.0d %.d %0d\n", 0, 0, 0, 1); + TEST("%1$a %1$A\n", 1.5); + TEST("%1$a %1$A\n", 0.0/0.0); + TEST("-%1$5.3s--%1$-5.3s-\n", "Hallo"); + TEST("-%1$5.10s--%1$-5.10s-\n", "Hallo"); + TEST("-%1$5c--%1$-5c-\n", 55); + TEST("-%#09.3x-", 10u); + TEST("-%#05x-\n", 10u); + TEST("-%#05o-\n", 10u); + TEST("-%.0o-\n", 0u); + TEST("-%.0x-\n", 0u); + TEST("-%.0X-\n", 0u); + TEST("-%#.0o-\n", 0u); + TEST("-%#.0x-\n", 0u); + TEST("-%#.0X-\n", 0u); + TEST("-%.0u-\n", 0u); + TEST("-%.0i-\n", 0); + TEST("-%.0d-\n", 0); -int main(int, char *[]) -{ - constexpr std::array<int, 5> array[] = {std::array{1, 2, 3, 4, 5}, std::array{1, 2, 3, 4, 5}, std::array{1, 2, 3, 4, 5}, std::array{1, 2, 3, 4, 5}, std::array{1, 2, 3, 4, 5}}; - - std::cout << format::format<"Hello %s: %d %03.5f %V">("World", 7, 3.5, array) << std::endl; - std::cout << format::format<"%v%v%v%v%v">('H', 'e', 'l', 'l', 'o') << std::endl; - std::cout << format::format("%V", &array) << std::endl; - std::cout << format::format("%s", std::array{1, 2, 3, 4, 5}) << std::endl; - std::cout << format::format("Hello %v: %-7v %v", "World", 3.5, 5) << std::endl; - std::cout << format::format("Hello %s: %d %03.5f", "World", 7, 3.5) << std::endl; - return 0; + TEST("-%020.3e-\n", -1.2); + TEST("-%020.3f-\n", -1.2); + TEST("-%020.3g-\n", -1.2); + TEST("-%+020.3a-\n", 1.2); + TEST("-% 020.3a-\n", 1.2); + TEST("-% 20.3a-\n", 1.2); + TEST("-%+20.3a-\n", 1.2); + TEST("-% -20.3a-\n", 1.2); + + TEST("-%0.0a-\n", 0.0); + TEST("-%#0.0e-\n", 0.0); + TEST("-%0.0f-\n", 0.0); + TEST("-%#020.3g-\n", -1.2); + TEST("-%+-20.3a-\n", 1.2); + TEST("-%#+20.3g-\n", 1.0); + + // these might fail, because %p format is implementation defined, although POSIX seems to define it as %#x + TEST("-%p-\n", nullptr); + TEST("-%p-\n", argv); + TEST("-%p-\n", argv[0]); +#undef TEST } |
