From bb400670856fb0e27560cd0767ee9610ed137298 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:04:57 +0900 Subject: [PATCH] Add some interface for `run_length_sequence` --- include/iris/run_length_sequence.hpp | 231 ++++++++++++++++----------- test/run_length_sequence.cpp | 135 ++++++++++++++++ 2 files changed, 269 insertions(+), 97 deletions(-) diff --git a/include/iris/run_length_sequence.hpp b/include/iris/run_length_sequence.hpp index 4eb3be1..8851a4a 100644 --- a/include/iris/run_length_sequence.hpp +++ b/include/iris/run_length_sequence.hpp @@ -29,10 +29,6 @@ #include // IWYU pragma: keep #include -namespace iris::detail { -struct run_length_sequence_comp; -} // iris::detail - namespace iris { template @@ -146,10 +142,18 @@ class run_length_sequence return {index(), *value_it_}; } + [[nodiscard]] constexpr run_length_run_ref run() const noexcept + { + return {*value_it_, this->run_span()}; + } [[nodiscard]] constexpr T const& run_value() const noexcept { return *value_it_; } + [[nodiscard]] constexpr interval run_span() const noexcept + { + return interval{*ofs_it_, *std::next(ofs_it_)}; + } constexpr void advance_run() noexcept { @@ -331,27 +335,26 @@ class run_length_sequence IRIS_ZZ_RUN_LENGTH_SEQUENCE_INVARIANT_GUARD if (offsets_.empty()) { - return this->emplace_back_on_empty(std::forward(value)); + return this->emplace_run_on_empty(static_cast(1u), std::forward(value)); + } - } else { - assert(!std::ranges::empty(runs_)); - assert(offsets_.size() >= 2); - if (offsets_.back() == max_size()) { - throwf("run_length_sequence capacity exceeded"); - } + assert(!std::ranges::empty(runs_)); + assert(offsets_.size() >= 2); + if (offsets_.back() == max_size()) { + throwf("run_length_sequence capacity exceeded"); + } - if (container::back(std::as_const(runs_)) == std::as_const(value)) { - // Equivalent element already exists; no need to insert. - return {offsets_.back()++, container::back(runs_)}; - } - // Need to insert new element - auto const new_pos = offsets_.back(); - offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel - [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; - auto& elem = container::append_return(runs_, std::forward(value)); - ofs_insertion_guard.clear(); - return {new_pos, elem}; + if (container::back(std::as_const(runs_)) == std::as_const(value)) { + // Equivalent element already exists; no need to insert. + return {offsets_.back()++, container::back(runs_)}; } + // Need to insert new element + auto const new_pos = offsets_.back(); + offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel + [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; + auto& elem = container::append_return(runs_, std::forward(value)); + ofs_insertion_guard.clear(); + return {new_pos, elem}; } template @@ -363,47 +366,46 @@ class run_length_sequence IRIS_ZZ_RUN_LENGTH_SEQUENCE_INVARIANT_GUARD if (offsets_.empty()) { - return this->emplace_back_on_empty(std::forward(args)...); + return this->emplace_run_on_empty(static_cast(1u), std::forward(args)...); + } - } else { - assert(!std::ranges::empty(runs_)); - assert(offsets_.size() >= 2); - if (offsets_.back() == max_size()) { - throwf("run_length_sequence capacity exceeded"); - } - if constexpr (requires { container::erase_back(runs_); }) { - auto& elem = container::append_return(runs_, std::forward(args)...); - [[maybe_unused]] elem_insertion_guard elem_insertion_guard{this}; - - auto const prev_it = std::ranges::prev(std::ranges::end(runs_), 2); - - if (std::as_const(*prev_it) == std::as_const(elem)) { - // Equivalent element already exists; no need to insert. - elem_insertion_guard.clear(); - container::erase_back(runs_); - return {offsets_.back()++, container::back(runs_)}; - } - - // Need to insert new element - auto const new_pos = offsets_.back(); - offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel + assert(!std::ranges::empty(runs_)); + assert(offsets_.size() >= 2); + if (offsets_.back() == max_size()) { + throwf("run_length_sequence capacity exceeded"); + } + if constexpr (requires { container::erase_back(runs_); }) { + auto& elem = container::append_return(runs_, std::forward(args)...); + [[maybe_unused]] elem_insertion_guard elem_insertion_guard{this}; + + auto const prev_it = std::ranges::prev(std::ranges::end(runs_), 2); + + if (std::as_const(*prev_it) == std::as_const(elem)) { + // Equivalent element already exists; no need to insert. elem_insertion_guard.clear(); - return {new_pos, elem}; + container::erase_back(runs_); + return {offsets_.back()++, container::back(runs_)}; + } - } else { - T temp(std::forward(args)...); - if (container::back(std::as_const(runs_)) == std::as_const(temp)) { - // Equivalent element already exists; no need to insert. - return {offsets_.back()++, container::back(runs_)}; - } - // Need to insert new element - auto const new_pos = offsets_.back(); - offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel - [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; - auto& elem = container::append_return(runs_, std::move(temp)); - ofs_insertion_guard.clear(); - return {new_pos, elem}; + // Need to insert new element + auto const new_pos = offsets_.back(); + offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel + elem_insertion_guard.clear(); + return {new_pos, elem}; + + } else { + T temp(std::forward(args)...); + if (container::back(std::as_const(runs_)) == std::as_const(temp)) { + // Equivalent element already exists; no need to insert. + return {offsets_.back()++, container::back(runs_)}; } + // Need to insert new element + auto const new_pos = offsets_.back(); + offsets_.emplace_back(new_pos + static_cast(1u)); // new sentinel + [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; + auto& elem = container::append_return(runs_, std::move(temp)); + ofs_insertion_guard.clear(); + return {new_pos, elem}; } } @@ -425,6 +427,57 @@ class run_length_sequence return this->emplace_back(std::move(value)); } + // Appends `count` logical elements equivalent to `value` at once. + // + // The resulting state is the same as calling `emplace_back(value)` `count` + // times, but at most one comparison and one insertion into `RunContainerT` + // are performed regardless of `count`. + // + // If `count == 0`, this function is no-op. + // + // Throws `std::length_error` when `size() + count` would exceed `max_size()`; + // the sequence is left unchanged in that case. + template + requires std::constructible_from && req::half_equality_comparable + constexpr void append_run(size_type count, U&& value) + { + check_range_concepts(); + static_assert(std::equality_comparable); + IRIS_ZZ_RUN_LENGTH_SEQUENCE_INVARIANT_GUARD + + if (count == 0) return; + + if (offsets_.empty()) { + this->check_append_capacity(count); + this->emplace_run_on_empty(static_cast(count), std::forward(value)); + return; + } + + assert(!std::ranges::empty(runs_)); + assert(offsets_.size() >= 2); + this->check_append_capacity(count); + + if (container::back(std::as_const(runs_)) == std::as_const(value)) { + // Equivalent run already exists; just extend it. + auto const pos = offsets_.back(); + offsets_.back() = static_cast(pos + static_cast(count)); + return; + } + // Need to insert new run + auto const new_pos = offsets_.back(); + offsets_.emplace_back(static_cast(new_pos + static_cast(count))); // new sentinel + [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; + container::append(runs_, std::forward(value)); + ofs_insertion_guard.clear(); + } + + template + requires std::constructible_from && (!req::half_equality_comparable) + constexpr void append_run(size_type count, U&& value) + { + this->append_run(count, T(std::forward(value))); + } + constexpr void pop_back() noexcept(noexcept(container::erase_back(runs_))) requires requires(RunContainerT& runs) { container::erase_back(runs); } @@ -603,6 +656,16 @@ class run_length_sequence swap(offsets_, other.offsets_); } + [[nodiscard]] constexpr bool operator==(run_length_sequence const& other) const + noexcept(noexcept(std::declval() == std::declval())) + { + static_assert(std::equality_comparable); + static_assert(std::equality_comparable); + // Adjacent runs never compare equal (class invariant), so the representation + // is canonical and representational equality is logical equality. + return offsets_ == other.offsets_ && runs_ == other.runs_; + } + private: static constexpr void check_range_concepts() noexcept { @@ -621,13 +684,23 @@ class run_length_sequence }); } + constexpr void check_append_capacity(size_type count) const + { + auto const remaining = max_size() - this->size(); + if (std::cmp_greater(count, remaining)) { + throwf("run_length_sequence capacity exceeded"); + } + } + template - constexpr const_reference emplace_back_on_empty(Args&&... args) IRIS_LIFETIMEBOUND + constexpr const_reference emplace_run_on_empty(IndexT const count, Args&&... args) IRIS_LIFETIMEBOUND { assert(this->empty()); + assert(count != 0); + [[maybe_unused]] ofs_insertion_guard ofs_insertion_guard{this}; offsets_.emplace_back(static_cast(0u)); - offsets_.emplace_back(static_cast(1u)); // sentinel + offsets_.emplace_back(count); // sentinel auto& elem = container::append_return(runs_, std::forward(args)...); ofs_insertion_guard.clear(); return {static_cast(0u), elem}; @@ -718,8 +791,6 @@ class run_length_sequence friend struct check_invariant_guard; #endif - friend struct detail::run_length_sequence_comp; - RunContainerT runs_; offsets_type offsets_; @@ -736,40 +807,6 @@ constexpr void swap( a.swap(b); } - -namespace detail { - -struct run_length_sequence_comp -{ - template class IndexedValuePairTT, class RunContainerT> - [[nodiscard]] static constexpr bool - equals( - run_length_sequence const& a, - run_length_sequence const& b - ) - noexcept(noexcept(std::declval() == std::declval())) - { - static_assert(std::equality_comparable); - static_assert(std::equality_comparable); - // Adjacent runs never compare equal (class invariant), so the representation - // is canonical and representational equality is logical equality. - return a.offsets_ == b.offsets_ && a.runs_ == b.runs_; - } -}; - -} // detail - -template class IndexedValuePairTT, class RunContainerT> -[[nodiscard]] constexpr bool -operator==( - run_length_sequence const& a, - run_length_sequence const& b -) - noexcept(noexcept(detail::run_length_sequence_comp::equals(a, b))) -{ - return detail::run_length_sequence_comp::equals(a, b); -} - } // iris #endif diff --git a/test/run_length_sequence.cpp b/test/run_length_sequence.cpp index 830cc75..dd05405 100644 --- a/test/run_length_sequence.cpp +++ b/test/run_length_sequence.cpp @@ -552,3 +552,138 @@ TEST_CASE("run_length_sequence: sequential insertion") REQUIRE(it == seq.end()); } } + +TEST_CASE("run_length_sequence: append_run") +{ + using RLS = iris::run_length_sequence; + + { + RLS seq; + seq.append_run(3, "foo"); + CHECK(seq.size() == 3); + CHECK(seq.run_count() == 1); + CHECK(seq.front().index == 0); + CHECK(seq.front().value == "foo"); + CHECK(seq.back().index == 2); + } + { + RLS seq; + seq.append_run(3, "foo"); + seq.append_run(2, "foo"); + CHECK(seq.size() == 5); + CHECK(seq.run_count() == 1); + CHECK(seq.back().index == 4); + CHECK(seq.back().value == "foo"); + } + { + RLS seq; + seq.append_run(3, "foo"); + seq.append_run(2, "bar"); + CHECK(seq.size() == 5); + CHECK(seq.run_count() == 2); + CHECK(seq.nth(3).run_span() == iris::interval{3, 5}); + CHECK(seq.back().value == "bar"); + CHECK(seq.runs() == std::vector{"foo", "bar"}); + } + { + RLS seq; + seq.append_run(1, "foo"); + seq.append_run(4, "foo"sv); + CHECK(seq.size() == 5); + CHECK(seq.run_count() == 1); + } + { + RLS seq; + seq.append_run(0, "foo"); + CHECK(seq.empty()); + CHECK(seq.run_count() == 0); + + seq.append_run(2, "foo"); + seq.append_run(0, "bar"); + CHECK(seq.size() == 2); + CHECK(seq.run_count() == 1); + CHECK(seq.runs() == std::vector{"foo"}); + } + { + constexpr auto values = std::array{"a"sv, "a"sv, "b"sv, "c"sv, "c"sv, "c"sv, "a"sv}; + constexpr auto counts = std::array{2u, 3u, 1u, 4u, 1u, 2u, 5u}; + + RLS by_run; + RLS by_elem; + for (auto const [value, count] : std::views::zip(values, counts)) { + by_run.append_run(count, value); + for (auto i = 0u; i < count; ++i) { + by_elem.emplace_back(value); + } + } + CHECK(by_run == by_elem); + CHECK(by_run.size() == 18); + CHECK(by_run.run_count() == 4); + CHECK(by_run.runs() == std::vector{"a", "b", "c", "a"}); + + auto const spans = by_run.run_view() + | std::views::transform([](auto const& r) { return r.span; }) + | std::ranges::to(); + using iv = iris::interval; + CHECK(spans == std::vector{iv{0, 5}, iv{5, 6}, iv{6, 13}, iv{13, 18}}); + } + { + using RLS_int = iris::run_length_sequence; + RLS_int seq; + seq.append_run(1, 7); + seq.append_run(RLS_int::max_size() - 1, 7); + CHECK(seq.size() == RLS_int::max_size()); + CHECK(seq.run_count() == 1); + + CHECK_THROWS_AS(seq.append_run(1, 7), std::length_error); + CHECK_THROWS_AS(seq.append_run(1, 8), std::length_error); + CHECK_THROWS_AS(seq.emplace_back(8), std::length_error); + CHECK(seq.size() == RLS_int::max_size()); + CHECK(seq.run_count() == 1); + } + { + using RLS_int = iris::run_length_sequence; + RLS_int seq; + CHECK_THROWS_AS(seq.append_run(RLS_int::max_size() + 1, 7), std::length_error); + CHECK(seq.empty()); + } +} + +TEST_CASE("run_length_sequence: run_span / run") +{ + using RLS = iris::run_length_sequence; + using iv = iris::interval; + + RLS seq; + seq.append_run(3, 10); // [0, 3) + seq.append_run(1, 20); // [3, 4) + seq.append_run(4, 30); // [4, 8) + + for (auto pos = 0u; pos < 3; ++pos) { + auto const it = seq.nth(pos); + CHECK(it.run_span() == iv{0, 3}); + CHECK(it.run().span == iv{0, 3}); + CHECK(it.run().value == 10); + } + { + auto const it = seq.nth(3); + CHECK(it.run_span() == iv{3, 4}); + CHECK(it.run().value == 20); + } + for (auto pos = 4u; pos < 8; ++pos) { + auto const it = seq.nth(pos); + CHECK(it.run_span() == iv{4, 8}); + CHECK(it.run().value == 30); + } + + { + auto it = seq.begin(); + CHECK(it.run_span() == iv{0, 3}); + it.advance_run(); + CHECK(it.run_span() == iv{3, 4}); + it.advance_run(); + CHECK(it.run_span() == iv{4, 8}); + it.advance_run(); + CHECK(it == seq.end()); + } +}