From e5756d10e8646d0e18d8478ddf701daf6464db27 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:40:35 +0900 Subject: [PATCH] Add escape/unescape string algo --- include/iris/string_algo.hpp | 123 +++++++++++++++++++++++++++++++---- test/string_algo.cpp | 77 ++++++++++++++++++++++ 2 files changed, 189 insertions(+), 11 deletions(-) diff --git a/include/iris/string_algo.hpp b/include/iris/string_algo.hpp index e001baa..7b1aea6 100644 --- a/include/iris/string_algo.hpp +++ b/include/iris/string_algo.hpp @@ -8,8 +8,10 @@ #include #include -#include #include +#include + +#include namespace iris { @@ -36,14 +38,16 @@ struct string_algo_traits } // detail -template +// TODO: make these CPO so that they can be passed to `std::views::transform` + +template constexpr void trim_edges( - std::basic_string& input, - std::basic_string_view const spaces = detail::string_algo_traits::ordinary_spaces + std::basic_string& input, + std::type_identity_t> const spaces = detail::string_algo_traits::ordinary_spaces ) { auto const first = input.find_first_not_of(spaces); - if (first == std::basic_string::npos) { + if (first == std::basic_string::npos) { input.clear(); return; } @@ -76,10 +80,10 @@ template } -template +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, + std::basic_string& input, + std::type_identity_t> const space_like_variant_chars = detail::string_algo_traits::space_like_variant_chars, CharT const to_space = detail::string_algo_traits::space ) { @@ -113,10 +117,10 @@ template } -template +template constexpr void compact_spaces( - std::basic_string& input, - std::basic_string_view const spaces = detail::string_algo_traits::ordinary_spaces, + std::basic_string& input, + std::type_identity_t> const spaces = detail::string_algo_traits::ordinary_spaces, CharT const to_space = detail::string_algo_traits::space ) { @@ -165,6 +169,103 @@ template return buf; } +// --------------------------------------------- + +// Replaces all occurrences of each elem of `escape_targets` with `{leader, each elem of escape_targets}`. +// `leader` is automatically included in escape targets by default; you don't need to +// specify it in `escape_targets`. +template +constexpr void escape( + std::basic_string& input, + CharT const leader /* e.g. '\' */, + std::type_identity_t> escape_targets /* e.g. "\"" */ +) +{ + assert(!escape_targets.contains(leader) && "you don't need to include `leader` in `escape_targets`"); + + auto const is_leader_needed = [&](CharT c) noexcept { + return c == leader || escape_targets.find(c) != std::basic_string_view::npos; + }; + std::size_t const extra = std::ranges::count_if(input, is_leader_needed); + if (extra == 0) return; + + input.resize(input.size() + extra); + auto out = input.rbegin(); + for (auto in = input.rbegin() + extra; in != input.rend(); ++in) { + *out++ = *in; + if (is_leader_needed(*in)) { + *out++ = leader; + } + } +} + +template +[[nodiscard]] constexpr std::string escape_copy( + std::string_view input, + char const leader, + std::string_view const escape_targets +) +{ + std::string buf(input); + iris::escape(buf, leader, escape_targets); + return buf; +} + +template +[[nodiscard]] constexpr std::u32string escape_copy( + std::u32string_view input, + char32_t const leader, + std::u32string_view const escape_targets +) +{ + std::u32string buf(input); + iris::escape(buf, leader, escape_targets); + return buf; +} + +// Inverse of `escape(...)`. +template +constexpr void unescape( + std::basic_string& input, + CharT const leader /* e.g. '\' */ +) +{ + std::size_t const first = input.find(leader); + if (first == std::basic_string::npos) return; + + auto out = input.begin() + first; + auto const end = input.end(); + for (auto in = out; in != end; ++in, ++out) { + if (*in == leader && std::next(in) != end) { + ++in; // skip the leader, copy the escaped char below + } + *out = *in; + } + input.erase(out, end); +} + +template +[[nodiscard]] constexpr std::string unescape_copy( + std::string_view input, + char const leader +) +{ + std::string buf(input); + iris::unescape(buf, leader); + return buf; +} + +template +[[nodiscard]] constexpr std::u32string unescape_copy( + std::u32string_view input, + char32_t const leader +) +{ + std::u32string buf(input); + iris::unescape(buf, leader); + return buf; +} + } // iris #endif diff --git a/test/string_algo.cpp b/test/string_algo.cpp index 387063c..420a404 100644 --- a/test/string_algo.cpp +++ b/test/string_algo.cpp @@ -20,6 +20,12 @@ TEST_CASE("string_algo: trim") SetConsoleOutputCP(CP_UTF8); #endif + { + std::string str; + iris::trim_edges(str, ""); + CHECK(str == ""sv); + } + CHECK(iris::trim_edges_copy("") == ""sv); CHECK(iris::trim_edges_copy(" ") == ""sv); CHECK(iris::trim_edges_copy("\t") == ""sv); @@ -56,6 +62,12 @@ TEST_CASE("string_algo: normalize") SetConsoleOutputCP(CP_UTF8); #endif + { + std::string str; + iris::normalize_spaces(str, ""); + CHECK(str == ""sv); + } + CHECK(iris::normalize_spaces_copy("") == ""sv); CHECK(iris::normalize_spaces_copy(" ") == " "sv); CHECK(iris::normalize_spaces_copy("\t") == " "sv); @@ -76,6 +88,12 @@ TEST_CASE("string_algo: compact") SetConsoleOutputCP(CP_UTF8); #endif + { + std::string str; + iris::compact_spaces(str, ""); + CHECK(str == ""sv); + } + CHECK(iris::compact_spaces_copy("") == ""sv); CHECK(iris::compact_spaces_copy(" ") == ""sv); CHECK(iris::compact_spaces_copy("\t") == ""sv); @@ -128,4 +146,63 @@ TEST_CASE("string_algo: compact") CHECK(iris::compact_spaces_copy(U" a b ") == U"a b"sv); } +TEST_CASE("string_algo: escape") +{ +#ifdef _MSC_VER + SetConsoleOutputCP(CP_UTF8); +#endif + + { + std::string str; + iris::escape(str, '!', ""); + CHECK(str == ""sv); + } + +#define IRIS_TEST_ESCAPE(str, leader, escape_targets, expected) \ + do { \ + auto const escaped_str = iris::escape_copy(str, leader, escape_targets); \ + CHECK(escaped_str == expected ## sv); \ + if (escaped_str == expected ## sv) { \ + CHECK(iris::unescape_copy(escaped_str, leader) == str ## sv); \ + } \ + } while (false) + + IRIS_TEST_ESCAPE("", '!', {}, ""); + IRIS_TEST_ESCAPE("a", '!', {}, "a"); + IRIS_TEST_ESCAPE("!", '!', {}, "!!"); + IRIS_TEST_ESCAPE("!a", '!', {}, "!!a"); + IRIS_TEST_ESCAPE("a!", '!', {}, "a!!"); + IRIS_TEST_ESCAPE("a!a", '!', {}, "a!!a"); + + IRIS_TEST_ESCAPE("", '!', "/", ""); + IRIS_TEST_ESCAPE("a", '!', "/", "a"); + IRIS_TEST_ESCAPE("!", '!', "/", "!!"); + IRIS_TEST_ESCAPE("!a", '!', "/", "!!a"); + IRIS_TEST_ESCAPE("a!", '!', "/", "a!!"); + IRIS_TEST_ESCAPE("a!a", '!', "/", "a!!a"); + + IRIS_TEST_ESCAPE("/", '!', "/", "!/"); + + IRIS_TEST_ESCAPE("/a", '!', "/", "!/a"); + IRIS_TEST_ESCAPE("a/", '!', "/", "a!/"); + + IRIS_TEST_ESCAPE("/!", '!', "/", "!/!!"); + IRIS_TEST_ESCAPE("!/", '!', "/", "!!!/"); + + IRIS_TEST_ESCAPE("/!a", '!', "/", "!/!!a"); + IRIS_TEST_ESCAPE("!/a", '!', "/", "!!!/a"); + IRIS_TEST_ESCAPE("!a/", '!', "/", "!!a!/"); + + IRIS_TEST_ESCAPE("/a!", '!', "/", "!/a!!"); + IRIS_TEST_ESCAPE("a/!", '!', "/", "a!/!!"); + IRIS_TEST_ESCAPE("a!/", '!', "/", "a!!!/"); + + IRIS_TEST_ESCAPE("/a!a", '!', "/", "!/a!!a"); + IRIS_TEST_ESCAPE("a/!a", '!', "/", "a!/!!a"); + IRIS_TEST_ESCAPE("a!/a", '!', "/", "a!!!/a"); + IRIS_TEST_ESCAPE("a!a/", '!', "/", "a!!a!/"); + +#undef IRIS_TEST_ESCAPE +} + // NOLINTEND(readability-container-size-empty)