diff --git a/include/beman/str_split/CMakeLists.txt b/include/beman/str_split/CMakeLists.txt index a4a50c2..4858f98 100644 --- a/include/beman/str_split/CMakeLists.txt +++ b/include/beman/str_split/CMakeLists.txt @@ -10,6 +10,7 @@ if(BEMAN_STR_SPLIT_USE_MODULES) config.hpp str_split.hpp todo.hpp + split_when.hpp "${PROJECT_BINARY_DIR}/include/beman/str_split/config_generated.hpp" ) else() @@ -21,6 +22,7 @@ else() config.hpp str_split.hpp todo.hpp + split_when.hpp "${PROJECT_BINARY_DIR}/include/beman/str_split/config_generated.hpp" ) endif() diff --git a/include/beman/str_split/detail/non_propagating_cache.hpp b/include/beman/str_split/detail/non_propagating_cache.hpp new file mode 100644 index 0000000..f696dae --- /dev/null +++ b/include/beman/str_split/detail/non_propagating_cache.hpp @@ -0,0 +1,43 @@ +#ifndef BEMAN_STR_SPLIT_DETAIL_NON_PROPAGATING_CACHE_HPP +#define BEMAN_STR_SPLIT_DETAIL_NON_PROPAGATING_CACHE_HPP + +#include +#include + +namespace beman::str_split::detail { + +template + requires std::is_object_v +struct non_propagating_cache : std::optional { + non_propagating_cache() = default; + + constexpr non_propagating_cache(const non_propagating_cache&) noexcept {} + + constexpr non_propagating_cache(non_propagating_cache&& from) noexcept { this->reset(); } + + ~non_propagating_cache() = default; + + constexpr non_propagating_cache& operator=(const non_propagating_cache& from) noexcept { + if (this != std::addressof(from)) { + this->reset(); + } + return *this; + } + + constexpr non_propagating_cache& operator=(non_propagating_cache&& from) noexcept { + this->reset(); + from.reset(); + return *this; + } + + template + requires std::is_constructible_v> + constexpr T& + emplace_deref(const It& it) noexcept(std::is_nothrow_constructible_v>) { + return this->emplace(*it); + } +}; + +} // namespace beman::str_split::detail + +#endif diff --git a/include/beman/str_split/detail/split_iterator.hpp b/include/beman/str_split/detail/split_iterator.hpp new file mode 100644 index 0000000..0b8ebc6 --- /dev/null +++ b/include/beman/str_split/detail/split_iterator.hpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef BEMAN_STR_SPLIT_SPLIT_ITERATOR_HPP +#define BEMAN_STR_SPLIT_SPLIT_ITERATOR_HPP + +#include +#include + +namespace beman::str_split::detail { + +template +class split_sentinel; + +template +class split_iterator +{ + friend split_sentinel; + + using base_iterator = Parent::base_iterator; + +public: + using iterator_concept = std::forward_iterator_tag; + using iterator_category = std::input_iterator_tag; + using value_type = Parent::base_subrange; + using difference_type = std::iter_difference_t; + + split_iterator() = default; + + constexpr split_iterator(Parent& parent, base_iterator current, value_type next) + : parent_(std::addressof(parent)) + , current_(std::move(current)) + , next_(std::move(next)) + { + } + + constexpr base_iterator base() const + { + return current_; + } + + constexpr value_type operator*() const + { + return {current_, next_.begin()}; + } + + constexpr split_iterator& operator++() + { + current_ = next_.begin(); + + if (current_ == std::ranges::end(parent_->base_)) { + trailing_empty_ = false; + } else { + current_ = next_.end(); + if (current_ == std::ranges::end(parent_->base_)) { + trailing_empty_ = true; + next_ = {current_, current_}; + } else { + next_ = parent_->find_next(current_); + } + } + + return *this; + } + + constexpr split_iterator operator++(int) + { + auto prev = *this; + ++(*this); + return prev; + } + + friend constexpr bool operator ==(const split_iterator& lhs, const split_iterator& rhs) + { + return lhs.current_ == rhs.current_ && lhs.trailing_empty_ == rhs.trailing_empty_; + } + +private: + Parent* parent_ = nullptr; + base_iterator current_{}; + value_type next_{}; + bool trailing_empty_ = false; +}; + +template +class split_sentinel +{ +public: + split_sentinel() = default; + + constexpr split_sentinel(Parent& parent) + : end_(std::ranges::end(parent.base_)) + { + } + + friend constexpr bool operator ==(const split_iterator& lhs, const split_sentinel& rhs) + { + return lhs.current_ == rhs.end_ && !lhs.trailing_empty_; + } + +private: + std::ranges::sentinel_t().base())> end_{}; +}; + +} + +#endif diff --git a/include/beman/str_split/split.hpp b/include/beman/str_split/split.hpp new file mode 100644 index 0000000..03b2097 --- /dev/null +++ b/include/beman/str_split/split.hpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef BEMAN_STR_SPLIT_SPLIT_HPP +#define BEMAN_STR_SPLIT_SPLIT_HPP + +#include +#include + +#include +#include + +namespace beman::str_split { + +namespace detail { + +template +concept splittable_view = + std::ranges::view && std::ranges::view && + std::indirectly_comparable, std::ranges::iterator_t, std::ranges::equal_to>; + +template +using single_value_view_t = std::ranges::single_view>; + +} // namespace detail + +template + requires detail::splittable_view +class split_view : public std::ranges::view_interface> { + public: + using iterator = detail::split_iterator; + friend iterator; + + using sentinel = detail::split_sentinel; + friend sentinel; + + split_view() + requires std::default_initializable && std::default_initializable + = default; + + constexpr explicit split_view(V base, Pattern pattern) : base_(std::move(base)), pattern_(std::move(pattern)) {} + + template + requires std::constructible_from> && + std::constructible_from> + constexpr explicit split_view(R&& range, std::ranges::range_value_t value) + : base_(std::ranges::views::all(std::forward(range))), + pattern_(std::ranges::views::single(std::move(value))) {} + + constexpr V base() const& + requires std::copy_constructible + { + return base_; + } + + constexpr V base() && { return std::move(base_); } + + constexpr iterator begin() { + if (!cached_begin_) { + cached_begin_.emplace(find_next(std::ranges::begin(base_))); + } + return {*this, std::ranges::begin(base_), *cached_begin_}; + } + + constexpr auto end() { + if constexpr (std::ranges::common_range) { + return iterator(*this, std::ranges::end(base_), {}); + } else { + return sentinel(*this); + } + } + + private: + using base_iterator = std::ranges::iterator_t; + using base_subrange = std::ranges::subrange; + + constexpr base_subrange find_next(base_iterator it) { + auto [b, e] = std::ranges::search(std::ranges::subrange(it, std::ranges::end(base_)), pattern_); + if (b != std::ranges::end(base_) && std::ranges::empty(pattern_)) { + ++b; + ++e; + } + return {b, e}; + } + + [[no_unique_address]] V base_{}; + [[no_unique_address]] Pattern pattern_{}; + detail::non_propagating_cache cached_begin_{}; +}; + +template +split_view(R&&, Pattern&&) -> split_view, std::ranges::views::all_t>; + +template +split_view(R&&, std::ranges::range_value_t) + -> split_view, detail::single_value_view_t>; + +namespace detail { + +template +using split_view_t = decltype(split_view(std::declval(), std::declval())); + +template +class split_adaptor_closure : public std::ranges::range_adaptor_closure> { + public: + constexpr explicit split_adaptor_closure(Pattern pattern) noexcept(std::is_nothrow_move_constructible_v) + : pattern_(std::move(pattern)) {} + + template + constexpr split_view_t operator()(R&& range) const& noexcept( + std::is_nothrow_constructible_v, R, const Pattern&>) { + return split_view(std::forward(range), pattern_); + } + + template + constexpr split_view_t + operator()(R&& range) && noexcept(std::is_nothrow_constructible_v, R, Pattern>) { + return split_view(std::forward(range), std::move(pattern_)); + } + + private: + Pattern pattern_{}; +}; + +template +using split_adaptor_closure_t = split_adaptor_closure>; + +namespace split { + +struct fn { + template + static constexpr split_view_t + operator()(R&& range, + Pattern&& pattern) noexcept(std::is_nothrow_constructible_v, R, Pattern>) { + return split_view(std::forward(range), std::forward(pattern)); + } + + template + static constexpr split_adaptor_closure_t operator()(Pattern&& pattern) noexcept( + std::is_nothrow_constructible_v, Pattern>) { + return split_adaptor_closure(std::forward(pattern)); + } +}; + +} // namespace split + +} // namespace detail + +namespace views { + +inline namespace rao { + +inline constexpr auto split = detail::split::fn{}; + +} + +} // namespace views + +} // namespace beman::str_split + +#endif diff --git a/include/beman/str_split/split_when.hpp b/include/beman/str_split/split_when.hpp new file mode 100644 index 0000000..47e1556 --- /dev/null +++ b/include/beman/str_split/split_when.hpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#ifndef BEMAN_STR_SPLIT_SPLIT_WHEN_HPP +#define BEMAN_STR_SPLIT_SPLIT_WHEN_HPP + +#include +#include + +#include +#include +#include + +namespace beman::str_split { + +namespace detail { + +template +concept searcher_for = std::is_object_v && std::ranges::view && + requires(T search, std::ranges::subrange> base) { + { auto(search(base)) } -> std::same_as>>; + }; + +} // namespace detail + +template Search> +class split_when_view : public std::ranges::view_interface> { + public: + using iterator = detail::split_iterator; + friend iterator; + + using sentinel = detail::split_sentinel; + friend sentinel; + + split_when_view() + requires std::default_initializable && std::default_initializable + = default; + + constexpr explicit split_when_view(V base, Search search) : base_(std::move(base)), search_(std::move(search)) {} + + constexpr V base() const& + requires std::copy_constructible + { + return base_; + } + + constexpr V base() && { return std::move(base_); } + + constexpr iterator begin() { + if (!cached_begin_) { + cached_begin_.emplace(find_next(std::ranges::begin(base_))); + } + return {*this, std::ranges::begin(base_), *cached_begin_}; + } + + constexpr auto end() { + if constexpr (std::ranges::common_range) { + return iterator(*this, std::ranges::end(base_), {}); + } else { + return sentinel(*this); + } + } + + private: + using base_iterator = std::ranges::iterator_t; + using base_subrange = std::ranges::subrange; + + constexpr base_subrange find_next(base_iterator it) { + return search_(std::ranges::subrange(it, std::ranges::end(base_))); + } + + [[no_unique_address]] V base_{}; + [[no_unique_address]] Search search_{}; + detail::non_propagating_cache cached_begin_{}; +}; + +namespace detail { + +template +using split_when_view_t = decltype(split_when_view(std::declval(), std::declval())); + +template +class split_when_adaptor_closure : public std::ranges::range_adaptor_closure> { + public: + constexpr explicit split_when_adaptor_closure(Search search) noexcept(std::is_nothrow_move_constructible_v) + : search_(std::move(search)) {} + + template + constexpr split_when_view_t operator()(R&& range) const& noexcept( + std::is_nothrow_constructible_v, R, const Search&>) { + return split_when_view(std::forward(range), search_); + } + + template + constexpr split_when_view_t + operator()(R&& range) && noexcept(std::is_nothrow_constructible_v, R, Search>) { + return split_when_view(std::forward(range), std::move(search_)); + } + + private: + Search search_{}; +}; + +template +using split_when_adaptor_closure_t = split_when_adaptor_closure>; + +namespace split_when { + +struct fn { + template + static constexpr split_when_view_t + operator()(R&& range, + Search&& search) noexcept(std::is_nothrow_constructible_v, R, Search>) { + return split_when_view(std::forward(range), std::forward(search)); + } + + template + static constexpr split_when_adaptor_closure_t operator()(Search&& search) noexcept( + std::is_nothrow_constructible_v, Search>) { + return split_when_adaptor_closure(std::forward(search)); + } +}; + +} // namespace split_when + +} // namespace detail + +namespace views { + +inline namespace rao { + +inline constexpr auto split_when = detail::split_when::fn{}; + +} + +} // namespace views + +} // namespace beman::str_split + +#endif // BEMAN_STR_SPLIT_SPLIT_WHEN_HPP diff --git a/tests/beman/str_split/CMakeLists.txt b/tests/beman/str_split/CMakeLists.txt index be75247..758ebc5 100644 --- a/tests/beman/str_split/CMakeLists.txt +++ b/tests/beman/str_split/CMakeLists.txt @@ -2,18 +2,32 @@ find_package(GTest REQUIRED) -add_executable(beman.str_split.tests.todo) -target_sources(beman.str_split.tests.todo PRIVATE todo.test.cpp) +add_executable(beman.str_split.tests.split) +target_sources(beman.str_split.tests.split PRIVATE split.tests.cpp) target_link_libraries( - beman.str_split.tests.todo - PRIVATE beman::str_split GTest::gtest_main + beman.str_split.tests.split + PRIVATE beman::str_split GTest::gtest_main GTest::gmock ) if(BEMAN_EXEMPLAR_USE_MODULES) set_target_properties( - beman.str_split.tests.todo + beman.str_split.tests.split + PROPERTIES CXX_MODULE_STD ON + ) +endif() + +add_executable(beman.str_split.tests.split_when) +target_sources(beman.str_split.tests.split_when PRIVATE split_when.tests.cpp) +target_link_libraries( + beman.str_split.tests.split_when + PRIVATE beman::str_split GTest::gtest_main GTest::gmock +) +if(BEMAN_EXEMPLAR_USE_MODULES) + set_target_properties( + beman.str_split.tests.split_when PROPERTIES CXX_MODULE_STD ON ) endif() include(GoogleTest) -gtest_discover_tests(beman.str_split.tests.todo DISCOVERY_TIMEOUT 60) +gtest_discover_tests(beman.str_split.tests.split DISCOVERY_TIMEOUT 60) +gtest_discover_tests(beman.str_split.tests.split_when DISCOVERY_TIMEOUT 60) diff --git a/tests/beman/str_split/split.tests.cpp b/tests/beman/str_split/split.tests.cpp new file mode 100644 index 0000000..2db0690 --- /dev/null +++ b/tests/beman/str_split/split.tests.cpp @@ -0,0 +1,68 @@ +#include + +#include +#include + +#include + +using namespace std::literals; +namespace bss = beman::str_split; + +TEST(Split, SplitZeroTimesOnSingleValue) { + auto words = ""sv | bss::views::split(' ') | std::ranges::to>(); + EXPECT_THAT(words, testing::ElementsAre()); +} + +TEST(Split, SplitsOnceOnSingleValue) { + auto name = "Alice's"sv; + auto parts = name | bss::views::split('\'') | std::ranges::to>(); + EXPECT_THAT(parts, testing::ElementsAre("Alice"sv, "s"sv)); +} + +TEST(Split, SplitsManyTimesOnSingleValue) { + auto title = "Down the Rabbit-Hole."sv; + auto words = title | bss::views::split(' ') | std::ranges::to>(); + EXPECT_THAT(words, testing::ElementsAre("Down", "the", "Rabbit-Hole.")); +} + +TEST(Split, SplitZeroTimesOnEmptyPattern) { + auto parts = ""sv | bss::views::split(""sv) | std::ranges::to>(); + EXPECT_THAT(parts, testing::ElementsAre()); +} + +TEST(Split, SplitOnceOnEmptyPattern) { + auto parts = "A"sv + | bss::views::split(""sv) + | std::ranges::to>(); + + EXPECT_THAT(parts, testing::ElementsAre("A")); +} + +TEST(Split, SplitsManyTimesEmptyPattern) { + auto parts = "Alice"sv + | bss::views::split(""sv) + | std::ranges::to>(); + + EXPECT_THAT(parts, testing::ElementsAre("A"sv, "l"sv, "i"sv, "c"sv, "e"sv)); +} + +TEST(Split, SplitZeroTimesOnNonEmptyPattern) { + auto parts = ""sv | bss::views::split("the"sv) | std::ranges::to>(); + EXPECT_THAT(parts, testing::ElementsAre()); +} + +TEST(Split, SplitOnceOnNonEmptyPattern) { + auto parts = "Down the Rabbit-Hole."sv + | bss::views::split("the"sv) + | std::ranges::to>(); + + EXPECT_THAT(parts, testing::ElementsAre("Down "sv, " Rabbit-Hole."sv)); +} + +TEST(Split, SplitsManyTimesNonEmptyPattern) { + auto parts = "Alice was beginning to get very tired"sv + | bss::views::split(" t"sv) + | std::ranges::to>(); + + EXPECT_THAT(parts, testing::ElementsAre("Alice was beginning"sv, "o get very"sv, "ired"sv)); +} diff --git a/tests/beman/str_split/split_when.tests.cpp b/tests/beman/str_split/split_when.tests.cpp new file mode 100644 index 0000000..695a6f7 --- /dev/null +++ b/tests/beman/str_split/split_when.tests.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include + +#include +#include + +#include + +using namespace std::literals; +namespace bss = beman::str_split; + +namespace { + +class any_seq_of { +public: + constexpr explicit any_seq_of(std::string_view delimiters) + : delimiters_(delimiters) + { + } + + template + requires std::constructible_from + V operator()(V view) + { + auto str = std::string_view(view); + auto begin_pos = str.find_first_of(delimiters_); + if (begin_pos == str.npos) { + return {std::ranges::end(view), std::ranges::end(view)}; + } + auto end_pos = str.find_first_not_of(delimiters_, begin_pos + 1); + return {std::ranges::begin(view) + begin_pos, std::ranges::begin(view) + end_pos}; + } + +private: + std::string_view delimiters_; +}; + +} + +TEST(SplitWhen, SplitZeroTimes) { + auto words = ""sv + | bss::views::split_when(any_seq_of(" \t"sv)) + | std::ranges::to>(); + + EXPECT_THAT(words, testing::ElementsAre()); +} + +TEST(SplitWhen, SplitsOnce) { + auto parts = "Down the Rabbit-Hole"sv + | bss::views::split_when(any_seq_of("-."sv)) + | std::ranges::to>(); + + EXPECT_THAT(parts, testing::ElementsAre("Down the Rabbit"sv, "Hole"sv)); +} + +TEST(SplitWhen, SplitsManyTimes) { + auto words = "Alice \t was beginning to\tget \tvery tired"sv + | bss::views::split_when(any_seq_of(" \t"sv)) + | std::ranges::to>(); + + EXPECT_THAT(words, testing::ElementsAre("Alice", "was", "beginning", "to", "get", "very", "tired")); +} diff --git a/tests/beman/str_split/todo.test.cpp b/tests/beman/str_split/todo.test.cpp deleted file mode 100644 index f1c0082..0000000 --- a/tests/beman/str_split/todo.test.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -#include -#include -#include - -TEST(TodoTest, todo) { - const bool todo = true; - EXPECT_TRUE(todo); -}