summaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
Diffstat (limited to 'include')
-rw-r--r--include/cxxformat/core.hpp572
-rw-r--r--include/cxxformat/cxxformat7
-rw-r--r--include/cxxformat/file_ptr.hpp23
-rw-r--r--include/cxxformat/formatters.hpp486
-rw-r--r--include/cxxformat/helpers.hpp260
-rw-r--r--include/cxxformat/ostream.hpp27
-rw-r--r--include/cxxformat/string.hpp25
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;
+}