Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 112 additions & 11 deletions include/iris/string_algo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

#include <string>
#include <string_view>
#include <ranges>
#include <algorithm>
#include <type_traits>

#include <cassert>

namespace iris {

Expand All @@ -36,14 +38,16 @@ struct string_algo_traits<char32_t>

} // detail

template<class CharT, class Traits>
// TODO: make these CPO so that they can be passed to `std::views::transform`

template<class CharT, class TraitsT>
constexpr void trim_edges(
std::basic_string<CharT, Traits>& input,
std::basic_string_view<CharT, Traits> const spaces = detail::string_algo_traits<CharT>::ordinary_spaces
std::basic_string<CharT, TraitsT>& input,
std::type_identity_t<std::basic_string_view<CharT, TraitsT>> const spaces = detail::string_algo_traits<CharT>::ordinary_spaces
)
{
auto const first = input.find_first_not_of(spaces);
if (first == std::basic_string<CharT, Traits>::npos) {
if (first == std::basic_string<CharT, TraitsT>::npos) {
input.clear();
return;
}
Expand Down Expand Up @@ -76,10 +80,10 @@ template<int = 0>
}


template<class CharT, class Traits>
template<class CharT, class TraitsT>
constexpr void normalize_spaces(
std::basic_string<CharT, Traits>& input,
std::basic_string_view<CharT, Traits> const space_like_variant_chars = detail::string_algo_traits<CharT>::space_like_variant_chars,
std::basic_string<CharT, TraitsT>& input,
std::type_identity_t<std::basic_string_view<CharT, TraitsT>> const space_like_variant_chars = detail::string_algo_traits<CharT>::space_like_variant_chars,
CharT const to_space = detail::string_algo_traits<CharT>::space
)
{
Expand Down Expand Up @@ -113,10 +117,10 @@ template<int = 0>
}


template<class CharT, class Traits>
template<class CharT, class TraitsT>
constexpr void compact_spaces(
std::basic_string<CharT, Traits>& input,
std::basic_string_view<CharT, Traits> const spaces = detail::string_algo_traits<CharT>::ordinary_spaces,
std::basic_string<CharT, TraitsT>& input,
std::type_identity_t<std::basic_string_view<CharT, TraitsT>> const spaces = detail::string_algo_traits<CharT>::ordinary_spaces,
CharT const to_space = detail::string_algo_traits<CharT>::space
)
{
Expand Down Expand Up @@ -165,6 +169,103 @@ template<int = 0>
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<class CharT, class TraitsT>
constexpr void escape(
std::basic_string<CharT, TraitsT>& input,
CharT const leader /* e.g. '\' */,
std::type_identity_t<std::basic_string_view<CharT, TraitsT>> 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<CharT, TraitsT>::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<int = 0>
[[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<int = 0>
[[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<class CharT, class TraitsT>
constexpr void unescape(
std::basic_string<CharT, TraitsT>& input,
CharT const leader /* e.g. '\' */
)
{
std::size_t const first = input.find(leader);
if (first == std::basic_string<CharT, TraitsT>::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<int = 0>
[[nodiscard]] constexpr std::string unescape_copy(
std::string_view input,
char const leader
)
{
std::string buf(input);
iris::unescape(buf, leader);
return buf;
}

template<int = 0>
[[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
77 changes: 77 additions & 0 deletions test/string_algo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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)
Loading