diff --git a/include/libipc/imp/fmt.h b/include/libipc/imp/fmt.h new file mode 100644 index 0000000..af0a5c4 --- /dev/null +++ b/include/libipc/imp/fmt.h @@ -0,0 +1,176 @@ +/** + * \file libipc/fmt.h + * \author mutouyun (orz@orzz.org) + * \brief String formatting. + * + * \remarks The current performance is not high, + * because I use std::sprintf directly for formatting for convenience. + */ +#pragma once + +#include +#include +#include +#include // std::chrono::time_point +#include +#include +#include // std::tm, std::localtime + +#include "libipc/imp/fmt_cpo.h" +#include "libipc/imp/span.h" +#include "libipc/imp/detect_plat.h" +#include "libipc/imp/export.h" + +namespace ipc { + +/** + * \brief The format string reference wrapper. + */ +template +struct fmt_ref { + span fstr; + T param; +}; + +/** + * \brief Conversion specifiers. + * + * \remarks Just like printf, the format string is of the form + * [flags][field_width][.precision][conversion_character] + * + * \see http://personal.ee.surrey.ac.uk/Personal/R.Bowden/C/printf.html + */ +template +auto spec(char const (&fstr)[N]) noexcept { + return [&fstr](auto &&arg) noexcept { + using arg_t = decltype(arg); + return fmt_ref {{fstr}, static_cast(arg)}; + }; +} + +/** + * \brief String formatting function. + * + * \param args arguments that support the fmt output + * \return an empty string if the fmt output fails + */ +template +LIBIPC_NODISCARD std::string fmt(A &&...args) { + std::string joined; + fmt_context ctx(joined); + if (fmt_to(ctx, std::forward(args)...)) { + return ctx.finish() ? joined : ""; + } + return {}; +} + +/// \brief String types. +IPC_EXPORT bool to_string(fmt_context &ctx, char const * a) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, std::string const &a) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, char const * a, span fstr) noexcept; + inline bool to_string(fmt_context &ctx, std::string const &a, span fstr) noexcept { return to_string(ctx, a.c_str(), fstr); } + +/// \brief Character to string conversion. +IPC_EXPORT bool to_string(fmt_context &ctx, char a) noexcept; +#if defined(LIBIPC_CPP_20) + inline bool to_string(fmt_context &ctx, char8_t a) noexcept { return to_string(ctx, (char)a); } +#endif // defined(LIBIPC_CPP_20) +IPC_EXPORT bool to_string(fmt_context &ctx, wchar_t a) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, char16_t a) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, char32_t a) noexcept; + +/// \brief Conversion of numeric types to strings. +IPC_EXPORT bool to_string(fmt_context &ctx, signed short a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, unsigned short a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, signed int a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, unsigned int a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, signed long a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, unsigned long a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, signed long long a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, unsigned long long a, span fstr = {}) noexcept; + inline bool to_string(fmt_context &ctx, signed char a, span fstr = {}) noexcept { return to_string(ctx, (int)a, fstr); } + inline bool to_string(fmt_context &ctx, unsigned char a, span fstr = {}) noexcept { return to_string(ctx, (unsigned)a, fstr); } + +/// \brief Conversion of floating point type to strings. +IPC_EXPORT bool to_string(fmt_context &ctx, double a, span fstr = {}) noexcept; +IPC_EXPORT bool to_string(fmt_context &ctx, long double a, span fstr = {}) noexcept; + inline bool to_string(fmt_context &ctx, float a, span fstr = {}) noexcept { return to_string(ctx, (double)a, fstr); } + +/// \brief Pointer. +IPC_EXPORT bool to_string(fmt_context &ctx, std::nullptr_t) noexcept; +template ::value>> +IPC_EXPORT bool to_string(fmt_context &ctx, T const volatile *a) noexcept; + +/// \brief Date and time. +IPC_EXPORT bool to_string(fmt_context &ctx, std::tm const &a, span fstr = {}) noexcept; + +namespace detail_fmt { + +/** + * \brief Convert std::time_t to std::string. + * \return an empty string if the conversion fails + */ +inline bool time_to_string(fmt_context &ctx, std::time_t tt, span fstr) noexcept { +#if defined(LIBIPC_CC_MSVC) + /// \see https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-s-localtime32-s-localtime64-s + std::tm tm {}; + if (::localtime_s(&tm, &tt) != 0) { + return {}; + } + return to_string(ctx, tm, fstr); +#else + return to_string(ctx, *std::localtime(&tt), fstr); +#endif +} + +} // namespace detail_fmt + +template +bool to_string(fmt_context &ctx, std::chrono::time_point const &a, span fstr = {}) noexcept { + return detail_fmt::time_to_string(ctx, std::chrono::system_clock::to_time_t(a), fstr); +} + +/** + * \brief Predefined fmt_to method + */ +namespace detail_tag_invoke { + +template +auto tag_invoke(decltype(ipc::fmt_to), fmt_context &ctx, T &&arg) noexcept + -> decltype(ipc::to_string(ctx, std::forward(arg))) { + return ipc::to_string(ctx, std::forward(arg)); +} + +template +auto tag_invoke(decltype(ipc::fmt_to), fmt_context &ctx, fmt_ref arg) noexcept + -> decltype(ipc::to_string(ctx, static_cast(arg.param), arg.fstr)) { + return ipc::to_string(ctx, static_cast(arg.param), arg.fstr); +} + +template +bool tag_invoke(decltype(ipc::fmt_to), fmt_context &ctx, span s) { + if (s.empty()) { + return false; + } + if (!fmt_to(ctx, s[0])) { + return false; + } + for (std::size_t i = 1; i < s.size(); ++i) { + if (!fmt_to(ctx, ' ', s[i])) return false; + } + return true; +} + +template +bool unfold_tuple_fmt_to(fmt_context &ctx, Tp const &tp, std::index_sequence) { + return fmt_to(ctx, std::get(tp)...); +} + +template +bool tag_invoke(decltype(ipc::fmt_to), fmt_context &ctx, std::tuple const &tp) { + return unfold_tuple_fmt_to(ctx, tp, std::index_sequence_for{}); +} + +} // namespace detail_tag_invoke +} // namespace ipc diff --git a/include/libipc/imp/fmt_cpo.h b/include/libipc/imp/fmt_cpo.h new file mode 100644 index 0000000..e7be313 --- /dev/null +++ b/include/libipc/imp/fmt_cpo.h @@ -0,0 +1,66 @@ +/** + * \file libipc/fmt_cpo.h + * \author mutouyun (orz@orzz.org) + * \brief String formatting CPO. + */ +#pragma once + +#include +#include +#include + +#include "libipc/imp/generic.h" +#include "libipc/imp/detect_plat.h" +#include "libipc/imp/span.h" +#include "libipc/imp/export.h" + +namespace ipc { + +/** + * \class class IPC_EXPORT fmt_context + * \brief The context of fmt. + */ +class IPC_EXPORT fmt_context { + std::array sbuf_; ///< stack buffer + + std::string &joined_; + std::size_t offset_; + +public: + fmt_context(std::string &j) noexcept; + + std::size_t capacity() noexcept; + void reset() noexcept; + bool finish() noexcept; + span buffer(std::size_t sz) noexcept; + void expend(std::size_t sz) noexcept; + bool append(span const &str) noexcept; +}; + +/// \brief Supports custom fmt_to methods for ipc::fmt. +namespace detail_tag_invoke { + +class fmt_to_t { + template + bool get_result(fmt_context &ctx, A1 && a1) const { + return ipc::tag_invoke(fmt_to_t{}, ctx, std::forward(a1)); + } + + template + bool get_result(fmt_context &ctx, A1 && a1, A &&...args) const { + return get_result(ctx, std::forward(a1)) + && get_result(ctx, std::forward(args)...); + } + +public: + template + bool operator()(fmt_context &ctx, A &&...args) const { + return get_result(ctx, std::forward(args)...); + } +}; + +} // namespace detail_tag_invoke + +constexpr detail_tag_invoke::fmt_to_t fmt_to{}; + +} // namespace ipc diff --git a/include/libipc/imp/generic.h b/include/libipc/imp/generic.h index e31e404..50184cb 100644 --- a/include/libipc/imp/generic.h +++ b/include/libipc/imp/generic.h @@ -44,7 +44,7 @@ constexpr in_place_t in_place {}; * \brief A general pattern for supporting customisable functions * \see https://www.open-std.org/jtc1/sc22/WG21/docs/papers/2019/p1895r0.pdf */ -namespace detail { +namespace detail_tag_invoke { void tag_invoke(); @@ -57,15 +57,15 @@ struct tag_invoke_t { } }; -} // namespace detail +} // namespace detail_tag_invoke -constexpr detail::tag_invoke_t tag_invoke {}; +constexpr detail_tag_invoke::tag_invoke_t tag_invoke{}; /** * \brief Circumventing forwarding reference may override copy and move constructs. * \see https://mpark.github.io/programming/2014/06/07/beware-of-perfect-forwarding-constructors/ */ -namespace detail { +namespace detail_not_match { template struct is_same_first : std::false_type {}; @@ -73,11 +73,11 @@ struct is_same_first : std::false_type {}; template struct is_same_first : std::true_type {}; -} // namespace detail +} // namespace detail_not_match template using not_match = - typename std::enable_if::type...>::value, bool>::type; /** diff --git a/src/libipc/imp/fmt.cpp b/src/libipc/imp/fmt.cpp new file mode 100644 index 0000000..deb344c --- /dev/null +++ b/src/libipc/imp/fmt.cpp @@ -0,0 +1,326 @@ + +#include // std::snprintf +#include // std::put_time +#include // std::ostringstream +#include +#include // std::memcpy +#include // std::min +#include +#include + +#include "libipc/imp/fmt.h" +#include "libipc/imp/codecvt.h" +#include "libipc/imp/detect_plat.h" + +namespace ipc { + +/** + * \brief Format conversions helpers. + * \see http://personal.ee.surrey.ac.uk/Personal/R.Bowden/C/printf.html + * https://en.cppreference.com/w/cpp/io/c/fprintf + */ +namespace { + +struct sfmt_policy { + static constexpr std::size_t aligned_size = 32U; +}; + +template +span local_fmt_sbuf() noexcept { + thread_local std::array sbuf; + return sbuf; +} + +span normalize(span const &a) { + if (a.empty()) return {}; + return a.first(a.size() - (a.back() == '\0' ? 1 : 0)); +} + +span smem_cpy(span const &sbuf, span a) noexcept { + if (sbuf.empty()) return {}; + a = normalize(a); + auto sz = (std::min)(sbuf.size() - 1, a.size()); + if (sz != 0) std::memcpy(sbuf.data(), a.data(), sz); + return sbuf.first(sz); +} + +span sbuf_cpy(span sbuf, span const &a) noexcept { + sbuf = smem_cpy(sbuf, a); + *sbuf.end() = '\0'; + return sbuf; +} + +span sbuf_cat(span const &sbuf, std::initializer_list> args) noexcept { + std::size_t remain = sbuf.size(); + for (auto s : args) { + remain -= smem_cpy(sbuf.last(remain), s).size(); + } + auto sz = sbuf.size() - remain; + sbuf[sz] = '\0'; + return sbuf.first(sz); +} + +char const *as_cstr(span const &a) { + if (a.empty()) return ""; + if (a.back() == '\0') return a.data(); + return sbuf_cpy(local_fmt_sbuf(), a).data(); +} + +span fmt_of(span const &fstr, span const &s) { + return sbuf_cat(local_fmt_sbuf(), {"%", fstr, s}); +} + +span fmt_of_unsigned(span fstr, span const &l) { + if (fstr.empty()) { + return fmt_of(l, "u"); + } + fstr = normalize(fstr); + switch (fstr.back()) { + case 'o': + case 'x': + case 'X': + case 'u': return sbuf_cat(local_fmt_sbuf(), {"%", fstr.first(fstr.size() - 1), l, fstr.last(1)}); + default : return sbuf_cat(local_fmt_sbuf(), {"%", fstr, l, "u"}); + } +} + +span fmt_of_signed(span fstr, span const &l) { + if (fstr.empty()) { + return fmt_of(l, "d"); + } + fstr = normalize(fstr); + switch (fstr.back()) { + case 'o': + case 'x': + case 'X': + case 'u': return fmt_of_unsigned(fstr, l); + default : return sbuf_cat(local_fmt_sbuf(), {"%", fstr, l, "d"}); + } +} + +span fmt_of_float(span fstr, span const &l) { + if (fstr.empty()) { + return fmt_of(l, "f"); + } + fstr = normalize(fstr); + switch (fstr.back()) { + case 'e': + case 'E': + case 'g': + case 'G': return sbuf_cat(local_fmt_sbuf(), {"%", fstr.first(fstr.size() - 1), l, fstr.last(1)}); + default : return sbuf_cat(local_fmt_sbuf(), {"%", fstr, l, "f"}); + } +} + +template +int sprintf(fmt_context &ctx, span const &sfmt, A a) { + for (std::int32_t sz = -1;;) { + auto sbuf = ctx.buffer(sz + 1); + if (sbuf.size() < std::size_t(sz + 1)) { + return -1; + } + sz = std::snprintf(sbuf.data(), sbuf.size(), sfmt.data(), a); + if (sz <= 0) { + return sz; + } + if (std::size_t(sz) < sbuf.size()) { + ctx.expend(sz); + return sz; + } + } +} + +template +bool sprintf(fmt_context &ctx, F fop, span const &fstr, span const &s, A a) noexcept { + LIBIPC_TRY { + return ipc::sprintf(ctx, fop(fstr, s), a) >= 0; + } LIBIPC_CATCH(...) { + return false; + } +} + +} // namespace + +/// \brief The context of fmt. + +fmt_context::fmt_context(std::string &j) noexcept + : joined_(j) + , offset_(0) {} + +std::size_t fmt_context::capacity() noexcept { + return (offset_ < sbuf_.size()) ? sbuf_.size() : joined_.size(); +} + +void fmt_context::reset() noexcept { + offset_ = 0; +} + +bool fmt_context::finish() noexcept { + LIBIPC_TRY { + if (offset_ < sbuf_.size()) { + joined_.assign(sbuf_.data(), offset_); + } else { + joined_.resize(offset_); + } + return true; + } LIBIPC_CATCH(...) { + return false; + } +} + +span fmt_context::buffer(std::size_t sz) noexcept { + auto roundup = [](std::size_t sz) noexcept { + constexpr std::size_t fmt_context_aligned_size = 512U; + return (sz & ~(fmt_context_aligned_size - 1)) + fmt_context_aligned_size; + }; + auto sbuf = make_span(sbuf_); + LIBIPC_TRY { + if (offset_ < sbuf.size()) { + if ((offset_ + sz) < sbuf.size()) { + return sbuf.subspan(offset_); + } else { + /// \remark switch the cache to std::string + joined_.assign(sbuf.data(), offset_); + joined_.resize(roundup(offset_ + sz)); + } + } else if ((offset_ + sz) >= joined_.size()) { + joined_.resize(roundup(offset_ + sz)); + } + return {&joined_[offset_], joined_.size() - offset_}; + } LIBIPC_CATCH(...) { + return {}; + } +} + +void fmt_context::expend(std::size_t sz) noexcept { + offset_ += sz; +} + +bool fmt_context::append(span const &str) noexcept { + auto sz = str.size(); + if (str.back() == '\0') --sz; + auto sbuf = buffer(sz); + if (sbuf.size() < sz) { + return false; + } + std::memcpy(sbuf.data(), str.data(), sz); + offset_ += sz; + return true; +} + +/// \brief To string conversion. + +bool to_string(fmt_context &ctx, char const *a) noexcept { + return to_string(ctx, a, {}); +} + +bool to_string(fmt_context &ctx, std::string const &a) noexcept { + return ctx.append(a); +} + +bool to_string(fmt_context &ctx, char const *a, span fstr) noexcept { + if (a == nullptr) return false; + return ipc::sprintf(ctx, fmt_of, fstr, "s", a); +} + +bool to_string(fmt_context &ctx, char a) noexcept { + return ipc::sprintf(ctx, fmt_of, {}, "c", a); +} + +bool to_string(fmt_context &ctx, wchar_t a) noexcept { + LIBIPC_TRY { + std::string des; + cvt_sstr(std::wstring{a}, des); + return ctx.append(des); + } LIBIPC_CATCH(...) { + return false; + } +} + +bool to_string(fmt_context &ctx, char16_t a) noexcept { + LIBIPC_TRY { + std::string des; + cvt_sstr(std::u16string{a}, des); + return ctx.append(des); + } LIBIPC_CATCH(...) { + return false; + } +} + +bool to_string(fmt_context &ctx, char32_t a) noexcept { + LIBIPC_TRY { + std::string des; + cvt_sstr(std::u32string{a}, des); + return ctx.append(des); + } LIBIPC_CATCH(...) { + return false; + } +} + +bool to_string(fmt_context &ctx, signed short a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_signed, fstr, "h", a); +} + +bool to_string(fmt_context &ctx, unsigned short a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_unsigned, fstr, "h", a); +} + +bool to_string(fmt_context &ctx, signed int a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_signed, fstr, "", a); +} + +bool to_string(fmt_context &ctx, unsigned int a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_unsigned, fstr, "", a); +} + +bool to_string(fmt_context &ctx, signed long a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_signed, fstr, "l", a); +} + +bool to_string(fmt_context &ctx, unsigned long a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_unsigned, fstr, "l", a); +} + +bool to_string(fmt_context &ctx, signed long long a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_signed, fstr, "ll", a); +} + +bool to_string(fmt_context &ctx, unsigned long long a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_unsigned, fstr, "ll", a); +} + +bool to_string(fmt_context &ctx, double a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_float, fstr, "", a); +} + +bool to_string(fmt_context &ctx, long double a, span fstr) noexcept { + return ipc::sprintf(ctx, fmt_of_float, fstr, "L", a); +} + +bool to_string(fmt_context &ctx, std::nullptr_t) noexcept { + return ctx.append("null"); +} + +template <> +bool to_string(fmt_context &ctx, void const volatile *a) noexcept { + if (a == nullptr) { + return to_string(ctx, nullptr); + } + return ipc::sprintf(ctx, fmt_of, "", "p", a); +} + +bool to_string(fmt_context &ctx, std::tm const &a, span fstr) noexcept { + if (fstr.empty()) { + fstr = "%Y-%m-%d %H:%M:%S"; + } + LIBIPC_TRY { + std::ostringstream ss; + ss << std::put_time(&a, as_cstr(fstr)); + return ctx.append(ss.str()); + } LIBIPC_CATCH(...) { + return {}; + } +} + +} // namespace ipc diff --git a/test/imp/test_imp_fmt.cpp b/test/imp/test_imp_fmt.cpp new file mode 100644 index 0000000..04acd6d --- /dev/null +++ b/test/imp/test_imp_fmt.cpp @@ -0,0 +1,125 @@ +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include + +#include "test.h" + +#include "libipc/imp/fmt.h" + +TEST(fmt, spec) { + EXPECT_STREQ(ipc::spec("hello")(123).fstr.data(), "hello"); + EXPECT_EQ(ipc::spec("hello")(123).param , 123); + EXPECT_STREQ(ipc::spec("hello")("world").fstr.data(), "hello"); + EXPECT_STREQ(ipc::spec("hello")("world").param , "world"); +} + +TEST(fmt, to_string) { + std::string joined; + ipc::fmt_context ctx(joined); + + auto check = [&](auto &&txt, auto &&...val) { + ctx.reset(); + EXPECT_TRUE(ipc::to_string(ctx, std::forward(val)...)); + ctx.finish(); + EXPECT_EQ(joined, std::forward(txt)); + }; + + /// \brief string + check("", ""); + check("%what%", "%what%"); + check(" %what%", "%what%", "10"); + check("%what% ", "%what%", "-10"); + + /// \brief character + check("A", 'A'); + check("A", L'A'); + check("A", u'A'); + check("A", U'A'); + + /// \brief numeric + check("123" , (signed char)123 ); + check("-65" , (signed char)-321 ); + check("123" , (unsigned char)123 ); + check("65" , (unsigned char)321 ); + check("123" , (short)123 ); + check("-321" , (short)-321 ); + check("123" , (unsigned short)123 ); + check("321" , (unsigned short)321 ); + check("-7949" , (short)123123 ); + check("6359" , (short)-321321 ); + check("57587" , (unsigned short)123123); + check("59177" , (unsigned short)321321); + check("123123" , 123123 ); + check("-321321" , -321321 ); + check("123123" , 123123u ); + check("321321" , 321321u ); + check("123123" , 123123ll ); + check("-321321" , -321321ll ); + check("123123" , 123123ull ); + check("321321" , 321321ull ); + check("1e0f3" , 123123, "x" ); + check("1e0f3" , 123123u, "x" ); + check("1CAAB5C3B3", 123123123123ll, "X" ); + check("1CAAB5C3B3", 123123123123ull, "X" ); + + /// \brief floating point + check("123.123" , 123.123f, ".3"); + check("0123.12300" , 123.123, "010.5"); + check("123.123000" , 123.123l, "010.6"); + check("1.500000e+00", 1.5, "e"); + check("1.500000E+00", 1.5, "E"); + double r = 0.0; + ctx.reset(); EXPECT_TRUE(ipc::to_string(ctx, 0.0/r)); ctx.finish(); + std::cout << joined << "\n"; + ctx.reset(); EXPECT_TRUE(ipc::to_string(ctx, 1.0/r)); ctx.finish(); + std::cout << joined << "\n"; + + /// \brief pointer + check("null", nullptr); + int *p = (int *)0x0f013a04; + ctx.reset(); EXPECT_TRUE(ipc::to_string(ctx, (void *)p)); ctx.finish(); + std::cout << joined << "\n"; + + /// \brief date and time + auto tp = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(tp); + auto tm = *std::localtime(&tt); + ctx.reset(); EXPECT_TRUE(ipc::to_string(ctx, tm)); ctx.finish(); + std::cout << joined << "\n"; + std::string tm_str = joined; + ctx.reset(); EXPECT_TRUE(ipc::to_string(ctx, tp)); ctx.finish(); + EXPECT_EQ(tm_str, joined); +} + +TEST(fmt, fmt) { + char const txt[] = "hello world."; + + /// \brief hello world + auto s = ipc::fmt("hello", " ", "world", "."); + EXPECT_EQ(s, txt); + + /// \brief chrono + std::cout << ipc::fmt('[', std::chrono::system_clock::now(), "] ", s) << "\n"; + + /// \brief long string + s = ipc::fmt(ipc::spec("4096")(txt)); + std::string test(4096, ' '); + std::memcpy(&test[test.size() - sizeof(txt) + 1], txt, sizeof(txt) - 1); + EXPECT_EQ(s, test); +} + +namespace { + +class foo {}; + +bool tag_invoke(decltype(ipc::fmt_to), ipc::fmt_context &, foo arg) noexcept(false) { + throw arg; + return {}; +} + +} // namespace + +TEST(fmt, throw) { + EXPECT_THROW(std::ignore = ipc::fmt(foo{}), foo); +}