diff options
| author | Markus Mittendrein <maxmitti@maxmitti.tk> | 2022-01-06 19:19:34 +0100 |
|---|---|---|
| committer | Markus Mittendrein <maxmitti@maxmitti.tk> | 2022-01-06 19:19:34 +0100 |
| commit | 51424295ae9ea77486270d7eca5c64eb9ea0bd99 (patch) | |
| tree | 6dcd18e17ca24d1539bffca803c630c6b376367d /include | |
| parent | bbd7c7a5d6bfb12df3898c4589753b59ce674576 (diff) | |
| download | cxxformat-51424295ae9ea77486270d7eca5c64eb9ea0bd99.tar.gz cxxformat-51424295ae9ea77486270d7eca5c64eb9ea0bd99.zip | |
Move cxxformat include-directory into a separate include directory
Diffstat (limited to 'include')
| -rw-r--r-- | include/cxxformat/core.hpp | 572 | ||||
| -rw-r--r-- | include/cxxformat/cxxformat | 7 | ||||
| -rw-r--r-- | include/cxxformat/file_ptr.hpp | 23 | ||||
| -rw-r--r-- | include/cxxformat/formatters.hpp | 486 | ||||
| -rw-r--r-- | include/cxxformat/helpers.hpp | 260 | ||||
| -rw-r--r-- | include/cxxformat/ostream.hpp | 27 | ||||
| -rw-r--r-- | include/cxxformat/string.hpp | 25 |
7 files changed, 1400 insertions, 0 deletions
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 <optional> +#include <stdexcept> +#include <string_view> + +#include <cxxformat/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/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 <cxxformat/core.hpp> +#include <cxxformat/formatters.hpp> +#include <cxxformat/file_ptr.hpp> +#include <cxxformat/ostream.hpp> +#include <cxxformat/string.hpp> 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 <cxxformat/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/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 <cxxformat/core.hpp> + +#include <array> +#include <bit> +#include <cmath> +#include <cassert> +#include <charconv> +#include <concepts> +#include <limits> + +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: uses std::to_chars’ minimal width for exact representation when no precision is specified 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/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 <cassert> +#include <concepts> +#include <limits> +#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/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 <cxxformat/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/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 <cxxformat/core.hpp> +#include <cxxformat/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; +} |
