From 08bfe2af099f6acc78772e7b315a6cf922626b76 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Wed, 24 Jun 2026 13:03:29 +0200 Subject: kstd: implement kstd::formatted_size --- libs/kstd/CMakeLists.txt | 2 +- libs/kstd/kstd/bits/format/string.hpp | 8 +- libs/kstd/kstd/bits/format/vformat.cpp | 224 +++++++++++++++++++++++++++++++++ libs/kstd/kstd/bits/format/vformat.hpp | 41 +++++- libs/kstd/kstd/format | 36 +++--- libs/kstd/kstd/format.test.cpp | 68 +++++++++- libs/kstd/kstd/vformat.cpp | 209 ------------------------------ 7 files changed, 347 insertions(+), 241 deletions(-) create mode 100644 libs/kstd/kstd/bits/format/vformat.cpp delete mode 100644 libs/kstd/kstd/vformat.cpp (limited to 'libs') diff --git a/libs/kstd/CMakeLists.txt b/libs/kstd/CMakeLists.txt index 5a957573..002fb548 100644 --- a/libs/kstd/CMakeLists.txt +++ b/libs/kstd/CMakeLists.txt @@ -18,7 +18,7 @@ add_library("kstd::lib" ALIAS "kstd") target_sources("kstd" PRIVATE "kstd/os/error.cpp" "kstd/mutex.cpp" - "kstd/vformat.cpp" + "kstd/bits/format/vformat.cpp" ) file(GLOB_RECURSE KSTD_HEADERS diff --git a/libs/kstd/kstd/bits/format/string.hpp b/libs/kstd/kstd/bits/format/string.hpp index e7e40886..a2b298d4 100644 --- a/libs/kstd/kstd/bits/format/string.hpp +++ b/libs/kstd/kstd/bits/format/string.hpp @@ -72,8 +72,7 @@ namespace kstd template struct format_string { - template - consteval format_string(char const (&str)[Size]) noexcept(false) // NOLINT + consteval format_string(std::string_view const & str) noexcept(false) : str_view{str} { using namespace bits::format; @@ -140,6 +139,11 @@ namespace kstd } } + template + consteval format_string(char const (&str)[Size]) noexcept(false) // NOLINT + : format_string{std::string_view{str}} + {} + std::string_view str_view; }; diff --git a/libs/kstd/kstd/bits/format/vformat.cpp b/libs/kstd/kstd/bits/format/vformat.cpp new file mode 100644 index 00000000..1bf293a4 --- /dev/null +++ b/libs/kstd/kstd/bits/format/vformat.cpp @@ -0,0 +1,224 @@ +#include +#include + +#include +#include +#include +#include + +namespace kstd::bits::format +{ + + auto vformat_to(output_buffer & buffer, std::string_view format, format_args args) -> void + { + auto context = kstd::format_context{buffer, args}; + auto parse_context = kstd::format_parse_context{format, args.size()}; + + auto it = parse_context.begin(); + auto end = parse_context.end(); + + while (it != end) + { + if (*it != '{' && *it != '}') + { + auto start = it; + while (it != end && *it != '{' && *it != '}') + { + std::advance(it, 1); + } + parse_context.advance_to(it); + context.push(std::string_view(start, it - start)); + continue; + } + + if (*it == '{') + { + std::advance(it, 1); + if (it != end && *it == '{') + { + context.push('{'); + std::advance(it, 1); + parse_context.advance_to(it); + continue; + } + + parse_context.advance_to(it); + auto index = 0uz; + + if (it != end && *it >= '0' && *it <= '9') + { + while (it != end && *it >= '0' && *it <= '9') + { + index = index * 10 + static_cast(*it - '0'); + std::advance(it, 1); + } + parse_context.check_arg_id(index); + } + else + { + index = parse_context.next_arg_id(); + } + + if (it != end && *it == ':') + { + std::advance(it, 1); + } + + parse_context.advance_to(it); + + if (index < args.size()) + { + auto const & arg = args[index]; + switch (arg.type) + { + case kstd::bits::format::arg_type::boolean: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.boolean, context); + break; + } + case kstd::bits::format::arg_type::character: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.character, context); + break; + } + case kstd::bits::format::arg_type::integer: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.integer, context); + break; + } + case kstd::bits::format::arg_type::unsigned_integer: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.unsigned_integer, context); + break; + } + case kstd::bits::format::arg_type::string_view: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.string_view, context); + break; + } + case kstd::bits::format::arg_type::c_string: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.c_string, context); + break; + } + case kstd::bits::format::arg_type::pointer: + { + auto fmt = kstd::formatter{}; + auto const parsed = fmt.parse(parse_context); + parse_context.advance_to(parsed); + fmt.format(arg.value.pointer, context); + break; + } + case kstd::bits::format::arg_type::user_defined: + { + if (arg.value.user_defined.format) + { + arg.value.user_defined.format(arg.value.user_defined.pointer, parse_context, context); + } + else + { + context.push("{?}"); + } + break; + } + default: + { + context.push("{fmt-err: unknown-type}"); + break; + } + } + } + else + { + context.push("{fmt-err: bound}"); + } + + it = parse_context.begin(); + + if (it != end && *it == '}') + { + std::advance(it, 1); + parse_context.advance_to(it); + } + else + { + context.push("{fmt-err: unconsumed}"); + while (it != end && *it != '}') + { + std::advance(it, 1); + } + + if (it != end) + { + std::advance(it, 1); + parse_context.advance_to(it); + } + } + } + else if (*it == '}') + { + std::advance(it, 1); + if (it != end && *it == '}') + { + context.push('}'); + std::advance(it, 1); + parse_context.advance_to(it); + } + else + { + context.push("{fmt-err: unescaped}"); + parse_context.advance_to(it); + } + } + } + } + + auto string_writer::push(std::string_view text) -> void + { + m_result.append(text); + } + + auto string_writer::push(char character) -> void + { + m_result.push_back(character); + } + + auto string_writer::release() -> string && + { + return std::move(m_result); + } + + auto size_recorder::push(std::string_view text) -> void + { + m_size += text.size(); + } + + auto size_recorder::push(char) -> void + { + ++m_size; + } + + auto size_recorder::size() const noexcept -> std::size_t + { + return m_size; + } + +} // namespace kstd::bits::format \ No newline at end of file diff --git a/libs/kstd/kstd/bits/format/vformat.hpp b/libs/kstd/kstd/bits/format/vformat.hpp index 994fae50..2db56d17 100644 --- a/libs/kstd/kstd/bits/format/vformat.hpp +++ b/libs/kstd/kstd/bits/format/vformat.hpp @@ -3,12 +3,13 @@ // IWYU pragma: private, include +#include #include #include #include -#include #include +#include #include #include #include @@ -29,13 +30,25 @@ namespace kstd struct string_writer : output_buffer { - auto push(std::string_view text) -> void override; - auto push(char character) -> void override; + auto push(std::string_view text) -> void final; + auto push(char character) -> void final; - auto release() -> string &&; + auto release() -> basic_string &&; private: - string m_result{}; + basic_string m_result{}; + }; + + //! A buffer that merely records the size of the output written to it. + struct size_recorder : output_buffer + { + auto push(std::string_view text) -> void final; + auto push(char character) -> void final; + + [[nodiscard]] auto size() const noexcept -> std::size_t; + + private: + std::size_t m_size{}; }; template Output> @@ -72,7 +85,8 @@ namespace kstd //! @param args The arguments for the format string. //! @return A new string containing the result of the format operation. template - auto format(format_string...> format, ArgumentTypes &&... args) -> string + auto format(format_string...> format, ArgumentTypes &&... args) + -> basic_string { auto buffer = bits::format::string_writer{}; bits::format::vformat_to(buffer, format.str_view, make_format_args(std::forward(args)...).args); @@ -94,6 +108,21 @@ namespace kstd return buffer.iterator(); } + //! Determine the number of characters required to format the format string with the given arguments. + //! + //! @param format The format string. + //! @param args The arguments for the format string. + //! @tparam ArgumentTypes The type of the format string arguments. + //! @return The number of characters required to format @p format with @p args + template + auto formatted_size(format_string...> format, ArgumentTypes &&... args) + -> std::size_t + { + auto buffer = bits::format::size_recorder{}; + bits::format::vformat_to(buffer, format.str_view, make_format_args(std::forward(args)...).args); + return buffer.size(); + } + } // namespace kstd #endif \ No newline at end of file diff --git a/libs/kstd/kstd/format b/libs/kstd/kstd/format index e04b79a9..5ed798db 100644 --- a/libs/kstd/kstd/format +++ b/libs/kstd/kstd/format @@ -1,22 +1,24 @@ #ifndef KSTD_FORMAT_HPP #define KSTD_FORMAT_HPP -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export -#include // IWYU pragma: export +// IWYU pragma: begin_exports +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// IWYU pragma: end_exports #endif \ No newline at end of file diff --git a/libs/kstd/kstd/format.test.cpp b/libs/kstd/kstd/format.test.cpp index 624779af..6126605f 100644 --- a/libs/kstd/kstd/format.test.cpp +++ b/libs/kstd/kstd/format.test.cpp @@ -4,14 +4,17 @@ #include #include +#include + +using namespace std::string_view_literals; SCENARIO("Formatting to a new string", "[format]") { GIVEN("a format string without any placeholders") { - auto const & fmt = "This is a test"; + constexpr auto fmt = "This is a test"sv; - WHEN("calling format with without any arguments.") + WHEN("calling format without any arguments.") { auto result = kstd::format(fmt); @@ -34,7 +37,7 @@ SCENARIO("Formatting to a new string", "[format]") GIVEN("a format string with placeholders") { - auto const & fmt = "Here are some placeholders: {} {} {}"; + constexpr auto fmt = "Here are some placeholders: {} {} {}"sv; WHEN("calling format with the same number of arguments as there are placeholders") { @@ -64,9 +67,9 @@ SCENARIO("Formatting to an output iterator", "[format]") GIVEN("a format string without any placeholders") { - auto const & fmt = "This is a test"; + constexpr auto fmt = "This is a test"sv; - WHEN("calling format with without any arguments.") + WHEN("calling format without any arguments.") { kstd::format_to(std::ostream_iterator{buffer}, fmt); @@ -89,7 +92,7 @@ SCENARIO("Formatting to an output iterator", "[format]") GIVEN("a format string with placeholders") { - auto const & fmt = "Here are some placeholders: {} {} {}"; + constexpr auto fmt = "Here are some placeholders: {} {} {}"sv; WHEN("calling format with the same number of arguments as there are placeholders") { @@ -111,4 +114,57 @@ SCENARIO("Formatting to an output iterator", "[format]") } } } +} + +SCENARIO("Determining formatted size") +{ + GIVEN("a format string without any placeholders") + { + constexpr auto fmt = "This is a test"sv; + + WHEN("calling formatted_size without any arguments") + { + auto const size = kstd::formatted_size(fmt); + + THEN("the result is equal to the length of the format string") + { + REQUIRE(size == fmt.size()); + } + } + + WHEN("calling format with additional arguments") + { + auto const size = kstd::formatted_size(fmt, 1, 2, 3); + + THEN("the result is equal to the length of the format string") + { + REQUIRE(size == fmt.size()); + } + } + } + + GIVEN("a format string with placeholders") + { + constexpr auto fmt = "Here are some placeholders: {} {} {}"sv; + + WHEN("calling format with the same number of arguments as there are placeholders") + { + auto result = kstd::formatted_size(fmt, 1, true, 'a'); + + THEN("the result is the formatted length") + { + REQUIRE(result == std::string_view{"Here are some placeholders: 1 true a"}.size()); + } + } + + WHEN("calling format with too many arguments") + { + auto result = kstd::formatted_size(fmt, 2, false, 'b', 4, 5, 6); + + THEN("the result is the formatted length") + { + REQUIRE(result == std::string_view{"Here are some placeholders: 2 false b"}.size()); + } + } + } } \ No newline at end of file diff --git a/libs/kstd/kstd/vformat.cpp b/libs/kstd/kstd/vformat.cpp deleted file mode 100644 index b7c51218..00000000 --- a/libs/kstd/kstd/vformat.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include -#include - -#include -#include -#include -#include - -namespace kstd::bits::format -{ - - auto vformat_to(output_buffer & buffer, std::string_view format, format_args args) -> void - { - auto context = kstd::format_context{buffer, args}; - auto parse_context = kstd::format_parse_context{format, args.size()}; - - auto it = parse_context.begin(); - auto end = parse_context.end(); - - while (it != end) - { - if (*it != '{' && *it != '}') - { - auto start = it; - while (it != end && *it != '{' && *it != '}') - { - std::advance(it, 1); - } - parse_context.advance_to(it); - context.push(std::string_view(start, it - start)); - continue; - } - - if (*it == '{') - { - std::advance(it, 1); - if (it != end && *it == '{') - { - context.push('{'); - std::advance(it, 1); - parse_context.advance_to(it); - continue; - } - - parse_context.advance_to(it); - auto index = 0uz; - - if (it != end && *it >= '0' && *it <= '9') - { - while (it != end && *it >= '0' && *it <= '9') - { - index = index * 10 + static_cast(*it - '0'); - std::advance(it, 1); - } - parse_context.check_arg_id(index); - } - else - { - index = parse_context.next_arg_id(); - } - - if (it != end && *it == ':') - { - std::advance(it, 1); - } - - parse_context.advance_to(it); - - if (index < args.size()) - { - auto const & arg = args[index]; - switch (arg.type) - { - case kstd::bits::format::arg_type::boolean: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.boolean, context); - break; - } - case kstd::bits::format::arg_type::character: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.character, context); - break; - } - case kstd::bits::format::arg_type::integer: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.integer, context); - break; - } - case kstd::bits::format::arg_type::unsigned_integer: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.unsigned_integer, context); - break; - } - case kstd::bits::format::arg_type::string_view: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.string_view, context); - break; - } - case kstd::bits::format::arg_type::c_string: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.c_string, context); - break; - } - case kstd::bits::format::arg_type::pointer: - { - auto fmt = kstd::formatter{}; - auto const parsed = fmt.parse(parse_context); - parse_context.advance_to(parsed); - fmt.format(arg.value.pointer, context); - break; - } - case kstd::bits::format::arg_type::user_defined: - { - if (arg.value.user_defined.format) - { - arg.value.user_defined.format(arg.value.user_defined.pointer, parse_context, context); - } - else - { - context.push("{?}"); - } - break; - } - default: - { - context.push("{fmt-err: unknown-type}"); - break; - } - } - } - else - { - context.push("{fmt-err: bound}"); - } - - it = parse_context.begin(); - - if (it != end && *it == '}') - { - std::advance(it, 1); - parse_context.advance_to(it); - } - else - { - context.push("{fmt-err: unconsumed}"); - while (it != end && *it != '}') - { - std::advance(it, 1); - } - - if (it != end) - { - std::advance(it, 1); - parse_context.advance_to(it); - } - } - } - else if (*it == '}') - { - std::advance(it, 1); - if (it != end && *it == '}') - { - context.push('}'); - std::advance(it, 1); - parse_context.advance_to(it); - } - else - { - context.push("{fmt-err: unescaped}"); - parse_context.advance_to(it); - } - } - } - } - - auto string_writer::push(std::string_view text) -> void - { - m_result.append(text); - } - - auto string_writer::push(char character) -> void - { - m_result.push_back(character); - } - - auto string_writer::release() -> string && - { - return std::move(m_result); - } - -} // namespace kstd::bits::format \ No newline at end of file -- cgit v1.2.3