From 4305a13384199902b5ad10b657bef4a63e9b8ecb Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 14 Aug 2026 03:54:01 +0900 Subject: [PATCH] Add string algorithm library --- CMakeLists.txt | 6 ++ include/iris/format.hpp | 27 +++++ include/iris/format_traits.hpp | 8 +- include/iris/string.hpp | 2 + include/iris/string_algo.hpp | 170 ++++++++++++++++++++++++++++++++ include/iris/unicode/string.hpp | 4 +- test/CMakeLists.txt | 1 + test/iris_test.hpp | 14 +++ test/string_algo.cpp | 131 ++++++++++++++++++++++++ 9 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 include/iris/format.hpp create mode 100644 include/iris/string_algo.hpp create mode 100644 test/string_algo.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c3e824..4ab03ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,12 @@ if(MSVC) _iris_cxx_best_practices INTERFACE /W4 /analyze /analyze:external- + $<$:/Zi /Zo> + ) + target_link_options( + _iris_cxx_best_practices + INTERFACE + $<$:/DEBUG:FULL> ) else() diff --git a/include/iris/format.hpp b/include/iris/format.hpp new file mode 100644 index 0000000..5539380 --- /dev/null +++ b/include/iris/format.hpp @@ -0,0 +1,27 @@ +#ifndef IRIS_ZZ_FORMAT_HPP +#define IRIS_ZZ_FORMAT_HPP + +// SPDX-License-Identifier: MIT + +#include + +#include + +namespace iris { + +template +struct no_spec_formatter +{ + static constexpr std::basic_format_parse_context::const_iterator + parse(std::basic_format_parse_context& ctx) + { + auto it = ctx.begin(); + if (it == ctx.end()) return it; + if (*it == format_traits::brace_close) return it; + throw std::format_error("unknown format specifier"); + } +}; + +} // iris + +#endif diff --git a/include/iris/format_traits.hpp b/include/iris/format_traits.hpp index 02489ca..7250068 100644 --- a/include/iris/format_traits.hpp +++ b/include/iris/format_traits.hpp @@ -1,4 +1,4 @@ -#ifndef IRIS_ZZ_FORMAT_TRAITS_HPP +#ifndef IRIS_ZZ_FORMAT_TRAITS_HPP #define IRIS_ZZ_FORMAT_TRAITS_HPP // SPDX-License-Identifier: MIT @@ -16,8 +16,11 @@ template<> struct format_traits { using char_type = char; + static constexpr char_type square_brace_open = '['; + static constexpr char_type paren_close = ')'; static constexpr char_type brace_open = '{'; static constexpr char_type brace_close = '}'; + static constexpr char_type comma = ','; template static constexpr std::basic_format_string> @@ -28,8 +31,11 @@ template<> struct format_traits { using char_type = wchar_t; + static constexpr char_type square_brace_open = L'['; + static constexpr char_type paren_close = L')'; static constexpr char_type brace_open = L'{'; static constexpr char_type brace_close = L'}'; + static constexpr char_type comma = L','; template static constexpr std::basic_format_string> diff --git a/include/iris/string.hpp b/include/iris/string.hpp index 974edad..465cfc9 100644 --- a/include/iris/string.hpp +++ b/include/iris/string.hpp @@ -3,6 +3,8 @@ // SPDX-License-Identifier: MIT +#include + #include #include #include diff --git a/include/iris/string_algo.hpp b/include/iris/string_algo.hpp new file mode 100644 index 0000000..e001baa --- /dev/null +++ b/include/iris/string_algo.hpp @@ -0,0 +1,170 @@ +#ifndef IRIS_ZZ_STRING_ALGO_HPP +#define IRIS_ZZ_STRING_ALGO_HPP + +// SPDX-License-Identifier: MIT + +#include +#include + +#include +#include +#include +#include + +namespace iris { + +namespace detail { + +template +struct string_algo_traits; + +template<> +struct string_algo_traits +{ + static constexpr char space = ' '; + static constexpr std::string_view ordinary_spaces{"\u0020\t\r\n"}; + static constexpr std::string_view space_like_variant_chars{"\t\r\n"}; +}; + +template<> +struct string_algo_traits +{ + static constexpr char32_t space = U' '; + static constexpr std::u32string_view ordinary_spaces{U"\u0020\u3000\t\r\n"}; // includes Japanese space + static constexpr std::u32string_view space_like_variant_chars{U"\u3000\t\r\n"}; +}; + +} // detail + +template +constexpr void trim_edges( + std::basic_string& input, + std::basic_string_view const spaces = detail::string_algo_traits::ordinary_spaces +) +{ + auto const first = input.find_first_not_of(spaces); + if (first == std::basic_string::npos) { + input.clear(); + return; + } + input.erase(0, first); + + auto const last = input.find_last_not_of(spaces); + input.erase(last + 1); +} + +template +[[nodiscard]] constexpr std::string trim_edges_copy( + std::string_view input, + std::string_view const spaces = detail::string_algo_traits::ordinary_spaces +) +{ + std::string buf(input); + iris::trim_edges(buf, spaces); + return buf; +} + +template +[[nodiscard]] constexpr std::u32string trim_edges_copy( + std::u32string_view input, + std::u32string_view const spaces = detail::string_algo_traits::ordinary_spaces +) +{ + std::u32string buf(input); + iris::trim_edges(buf, spaces); + return buf; +} + + +template +constexpr void normalize_spaces( + std::basic_string& input, + std::basic_string_view const space_like_variant_chars = detail::string_algo_traits::space_like_variant_chars, + CharT const to_space = detail::string_algo_traits::space +) +{ + std::ranges::replace_if(input, [&](CharT const ch) { + return space_like_variant_chars.contains(ch); + }, to_space); +} + +template +[[nodiscard]] constexpr std::string normalize_spaces_copy( + std::string_view input, + std::string_view const space_like_variant_chars = detail::string_algo_traits::space_like_variant_chars, + char const to_space = detail::string_algo_traits::space +) +{ + std::string buf(input); + iris::normalize_spaces(buf, space_like_variant_chars, to_space); + return buf; +} + +template +[[nodiscard]] constexpr std::u32string normalize_spaces_copy( + std::u32string_view input, + std::u32string_view const space_like_variant_chars = detail::string_algo_traits::space_like_variant_chars, + char32_t const to_space = detail::string_algo_traits::space +) +{ + std::u32string buf(input); + iris::normalize_spaces(buf, space_like_variant_chars, to_space); + return buf; +} + + +template +constexpr void compact_spaces( + std::basic_string& input, + std::basic_string_view const spaces = detail::string_algo_traits::ordinary_spaces, + CharT const to_space = detail::string_algo_traits::space +) +{ + iris::trim_edges(input, spaces); + if (input.empty()) return; + + std::size_t w = 0; + bool in_space = false; + + for (CharT const c : input) { + if (spaces.contains(c)) { + if (!in_space) { + input[w++] = to_space; + } + in_space = true; + + } else { + input[w++] = c; + in_space = false; + } + } + input.resize(w); +} + +template +[[nodiscard]] constexpr std::string compact_spaces_copy( + std::string_view input, + std::string_view const spaces = detail::string_algo_traits::ordinary_spaces, + char const to_space = detail::string_algo_traits::space +) +{ + std::string buf(input); + iris::compact_spaces(buf, spaces, to_space); + return buf; +} + +template +[[nodiscard]] constexpr std::u32string compact_spaces_copy( + std::u32string_view input, + std::u32string_view const spaces = detail::string_algo_traits::ordinary_spaces, + char32_t const to_space = detail::string_algo_traits::space +) +{ + std::u32string buf(input); + iris::compact_spaces(buf, spaces, to_space); + return buf; +} + +} // iris + +#endif diff --git a/include/iris/unicode/string.hpp b/include/iris/unicode/string.hpp index 952e173..5ed3033 100644 --- a/include/iris/unicode/string.hpp +++ b/include/iris/unicode/string.hpp @@ -25,8 +25,8 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -#ifndef IRIS_UNICODE_STRING_HPP -#define IRIS_UNICODE_STRING_HPP +#ifndef IRIS_ZZ_UNICODE_STRING_HPP +#define IRIS_ZZ_UNICODE_STRING_HPP #include #include diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4e2b3e7..66a345c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -183,6 +183,7 @@ if(PROJECT_IS_TOP_LEVEL) indirect colorize_format preprocess + string_algo ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) diff --git a/test/iris_test.hpp b/test/iris_test.hpp index 55b2f65..b641494 100644 --- a/test/iris_test.hpp +++ b/test/iris_test.hpp @@ -6,5 +6,19 @@ #include #include // IWYU pragma: export +#include + +#include +#include + +template T> + requires (!std::ranges::range) +struct Catch::StringMaker +{ + static std::string convert(T const& value) + { + return std::format("{}", value); + } +}; #endif diff --git a/test/string_algo.cpp b/test/string_algo.cpp new file mode 100644 index 0000000..387063c --- /dev/null +++ b/test/string_algo.cpp @@ -0,0 +1,131 @@ +#include "iris_test.hpp" + +#include + +#include +#include +#include + +#ifdef _MSC_VER +# include +#endif + +using namespace std::string_view_literals; + +// NOLINTBEGIN(readability-container-size-empty) + +TEST_CASE("string_algo: trim") +{ +#ifdef _MSC_VER + SetConsoleOutputCP(CP_UTF8); +#endif + + CHECK(iris::trim_edges_copy("") == ""sv); + CHECK(iris::trim_edges_copy(" ") == ""sv); + CHECK(iris::trim_edges_copy("\t") == ""sv); + CHECK(iris::trim_edges_copy("\r") == ""sv); + CHECK(iris::trim_edges_copy("\n") == ""sv); + + CHECK(iris::trim_edges_copy(U"") == U""sv); + CHECK(iris::trim_edges_copy(U" ") == U""sv); + CHECK(iris::trim_edges_copy(U" ") == U""sv); // Japanese space + CHECK(iris::trim_edges_copy(U"\t") == U""sv); + CHECK(iris::trim_edges_copy(U"\r") == U""sv); + CHECK(iris::trim_edges_copy(U"\n") == U""sv); + + CHECK(iris::trim_edges_copy(" a") == "a"sv); + CHECK(iris::trim_edges_copy("a ") == "a"sv); + CHECK(iris::trim_edges_copy(" a ") == "a"sv); + + CHECK(iris::trim_edges_copy(U" a") == U"a"sv); + CHECK(iris::trim_edges_copy(U"a ") == U"a"sv); + CHECK(iris::trim_edges_copy(U" a ") == U"a"sv); + + CHECK(iris::trim_edges_copy(" a b") == "a b"sv); + CHECK(iris::trim_edges_copy("a b ") == "a b"sv); + CHECK(iris::trim_edges_copy(" a b ") == "a b"sv); + + CHECK(iris::trim_edges_copy(U" a b") == U"a b"sv); + CHECK(iris::trim_edges_copy(U"a b ") == U"a b"sv); + CHECK(iris::trim_edges_copy(U" a b ") == U"a b"sv); +} + +TEST_CASE("string_algo: normalize") +{ +#ifdef _MSC_VER + SetConsoleOutputCP(CP_UTF8); +#endif + + CHECK(iris::normalize_spaces_copy("") == ""sv); + CHECK(iris::normalize_spaces_copy(" ") == " "sv); + CHECK(iris::normalize_spaces_copy("\t") == " "sv); + CHECK(iris::normalize_spaces_copy("\r") == " "sv); + CHECK(iris::normalize_spaces_copy("\n") == " "sv); + + CHECK(iris::normalize_spaces_copy(U"") == U""sv); + CHECK(iris::normalize_spaces_copy(U" ") == U" "sv); + CHECK(iris::normalize_spaces_copy(U" ") == U" "sv); // Japanese space + CHECK(iris::normalize_spaces_copy(U"\t") == U" "sv); + CHECK(iris::normalize_spaces_copy(U"\r") == U" "sv); + CHECK(iris::normalize_spaces_copy(U"\n") == U" "sv); +} + +TEST_CASE("string_algo: compact") +{ +#ifdef _MSC_VER + SetConsoleOutputCP(CP_UTF8); +#endif + + CHECK(iris::compact_spaces_copy("") == ""sv); + CHECK(iris::compact_spaces_copy(" ") == ""sv); + CHECK(iris::compact_spaces_copy("\t") == ""sv); + CHECK(iris::compact_spaces_copy("\r") == ""sv); + CHECK(iris::compact_spaces_copy("\n") == ""sv); + + CHECK(iris::compact_spaces_copy(U"") == U""sv); + CHECK(iris::compact_spaces_copy(U" ") == U""sv); + CHECK(iris::compact_spaces_copy(U" ") == U""sv); // Japanese space + CHECK(iris::compact_spaces_copy(U"\t") == U""sv); + CHECK(iris::compact_spaces_copy(U"\r") == U""sv); + CHECK(iris::compact_spaces_copy(U"\n") == U""sv); + + // ------------------------------------------ + // single spaces + + CHECK(iris::compact_spaces_copy(" a") == "a"sv); + CHECK(iris::compact_spaces_copy("a ") == "a"sv); + CHECK(iris::compact_spaces_copy(" a ") == "a"sv); + + CHECK(iris::compact_spaces_copy(U" a") == U"a"sv); + CHECK(iris::compact_spaces_copy(U"a ") == U"a"sv); + CHECK(iris::compact_spaces_copy(U" a ") == U"a"sv); + + CHECK(iris::compact_spaces_copy(" a b") == "a b"sv); + CHECK(iris::compact_spaces_copy("a b ") == "a b"sv); + CHECK(iris::compact_spaces_copy(" a b ") == "a b"sv); + + CHECK(iris::compact_spaces_copy(U" a b") == U"a b"sv); + CHECK(iris::compact_spaces_copy(U"a b ") == U"a b"sv); + CHECK(iris::compact_spaces_copy(U" a b ") == U"a b"sv); + + // ------------------------------------------ + // multiple spaces + + CHECK(iris::compact_spaces_copy(" a") == "a"sv); + CHECK(iris::compact_spaces_copy("a ") == "a"sv); + CHECK(iris::compact_spaces_copy(" a ") == "a"sv); + + CHECK(iris::compact_spaces_copy(U" a") == U"a"sv); + CHECK(iris::compact_spaces_copy(U"a ") == U"a"sv); + CHECK(iris::compact_spaces_copy(U" a ") == U"a"sv); + + CHECK(iris::compact_spaces_copy(" a b") == "a b"sv); + CHECK(iris::compact_spaces_copy("a b ") == "a b"sv); + CHECK(iris::compact_spaces_copy(" a b ") == "a b"sv); + + CHECK(iris::compact_spaces_copy(U" a b") == U"a b"sv); + CHECK(iris::compact_spaces_copy(U"a b ") == U"a b"sv); + CHECK(iris::compact_spaces_copy(U" a b ") == U"a b"sv); +} + +// NOLINTEND(readability-container-size-empty)