From d1159c24e595209eabde6ab29fc274440fd78e96 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sat, 29 Nov 2025 11:30:30 +0200 Subject: [PATCH 01/13] Add first implementation of positionless Currently only supporting forward-iterator case --- .clang-format | 56 +-- .clangd | 5 + .vscode/settings.json | 5 +- CMakeLists.txt | 4 - include/positionless/partitioning.hpp | 156 +++++++- test/partitioning_tests.cpp | 547 +++++++++++++++++++++++++- 6 files changed, 704 insertions(+), 69 deletions(-) create mode 100644 .clangd diff --git a/.clang-format b/.clang-format index 74a959a..f284ebd 100644 --- a/.clang-format +++ b/.clang-format @@ -1,54 +1,8 @@ --- -Language: Cpp BasedOnStyle: LLVM -AccessModifierOffset: -4 -AlignConsecutiveAssignments: false -AlignConsecutiveDeclarations: false -AlignOperands: false -AlignTrailingComments: false -AlwaysBreakTemplateDeclarations: Yes -BraceWrapping: - AfterCaseLabel: true - AfterClass: true - AfterControlStatement: true - AfterEnum: true - AfterFunction: true - AfterNamespace: true - AfterStruct: true - AfterUnion: true - AfterExternBlock: false - BeforeCatch: true - BeforeElse: true - BeforeLambdaBody: true - BeforeWhile: true - SplitEmptyFunction: true - SplitEmptyRecord: true - SplitEmptyNamespace: true -BreakBeforeBraces: Custom -BreakConstructorInitializers: AfterColon -BreakConstructorInitializersBeforeComma: false -ColumnLimit: 120 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -IncludeCategories: - - Regex: '^<.*' - Priority: 1 - - Regex: '^".*' - Priority: 2 - - Regex: '.*' - Priority: 3 -IncludeIsMainRegex: '([-_](test|unittest))?$' -IndentCaseBlocks: true -IndentWidth: 4 -InsertNewlineAtEOF: true -MacroBlockBegin: '' -MacroBlockEnd: '' -MaxEmptyLinesToKeep: 2 -NamespaceIndentation: All +--- +Language: Cpp +# Force pointers to the type for C++. +DerivePointerAlignment: false PointerAlignment: Left -SpaceInEmptyParentheses: false -SpacesInAngles: false -SpacesInConditionalStatement: false -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -TabWidth: 4 -... +ColumnLimit: 100 diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..751c65c --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +CompileFlags: + Add: + - "-std=c++23" + - "-stdlib=libc++" + CompilationDatabase: .build diff --git a/.vscode/settings.json b/.vscode/settings.json index 5cbe5ab..a59da4f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,9 @@ { "clangd.arguments": [ - "--compile-commands-dir=${workspaceFolder}/build", + "--compile-commands-dir=${workspaceFolder}/.build", "--background-index", "--clang-tidy", "--completion-style=detailed", - "--header-insertion=iwyu", - "--query-driver=/usr/bin/clang++" + "--header-insertion=iwyu" ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 790012e..621c198 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,10 +34,6 @@ target_compile_options(positionless INTERFACE add_executable(unit_tests test/partitioning_tests.cpp) target_link_libraries(unit_tests PRIVATE positionless doctest::doctest) -target_compile_options(unit_tests PRIVATE - $<$:-Wall -Wextra -Wpedantic> - $<$:/W4> -) enable_testing() add_test(NAME unit_tests COMMAND unit_tests) diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 395e1ee..72aaf69 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -1,14 +1,150 @@ #pragma once -namespace positionless -{ - - template - struct partitioning - { - Iterator begin; - Iterator end; - // TODO - }; +#include +#include +#include +#include +#include + +namespace positionless { + +/// A separation of some collection into multiple contiguous parts. +/// +/// A partitioning is constructed from a range defined by a pair of iterators. +/// The range must remain valid for the lifetime of the partitioning. +/// +/// - Invariant: parts_count() >= 1 +template class partitioning { +public: + using iterator = Iterator; + using value_type = std::iter_value_t; + using difference_type = std::iter_difference_t; + + /// An instance covering the range [begin, end), having just one part. + constexpr partitioning(Iterator begin, Iterator end); + + /// Returns the number of parts in the partitioning. + /// + /// - Invariant: `parts_count() >= 1` + [[nodiscard]] + size_t parts_count() const noexcept; + + /// Returns the iterators delimiting the part at index `part_index`. + /// + /// - Precondition: `part_index < parts_count()` + [[nodiscard]] + std::pair part(size_t part_index) const noexcept; + + /// Returns `true` if the part at index `part_index` is empty. + /// + /// - Precondition: `part_index < parts_count()` + [[nodiscard]] + bool is_part_empty(size_t part_index) const noexcept; + + /// Increases the size of the part at index `part_index` by moving its end + /// boundary forward by one element, and decreasing the size of the next part. + /// + /// - Precondition: `part_index + 1 < parts_count()` + /// - Precondition: !is_part_empty(part_index + 1) + void grow(size_t part_index); + + /// Adds a new empty part at the end of part `part_index`. + /// + /// - Precondition: `part_index < parts_count()` + void add_part_end(size_t part_index); + + /// Adds a new empty part at the begin of part `part_index`. + /// + /// - Precondition: `part_index < parts_count()` + void add_part_begin(size_t part_index); + + /// Adds `count` new empty parts at the end of part `part_index`. + /// + /// - Precondition: `part_index < parts_count()` + void add_parts_end(size_t part_index, size_t count); + + /// Adds `count` new empty parts at the begin of part `part_index`. + /// + /// - Precondition: `part_index < parts_count()` + void add_parts_begin(size_t part_index, size_t count); + + /// Removes the part at index `part_index`, growing the previous part to + /// cover its range. + /// + /// - Precondition: `0 < part_index < parts_count()` + void remove_part(size_t part_index); + +private: + /// The boundaries of each part in the partitioning. + /// + /// The first element is the begin iterator of the range, and the last + /// element is the end iterator of the underlying range. + std::vector boundaries_{}; +}; + +// Inline definitions + +template +inline constexpr partitioning::partitioning(Iterator begin, Iterator end) { + boundaries_.reserve(10); + boundaries_.emplace_back(std::move(begin)); + boundaries_.emplace_back(std::move(end)); +} + +template +inline size_t partitioning::parts_count() const noexcept { + return boundaries_.size() - 1; +} + +template +inline std::pair +partitioning::part(size_t part_index) const noexcept { + assert(part_index < parts_count()); + return {boundaries_[part_index], boundaries_[part_index + 1]}; +} + +template +inline bool partitioning::is_part_empty(size_t part_index) const noexcept { + assert(part_index < parts_count()); + auto [begin, end] = part(part_index); + return begin == end; +} + +template +inline void partitioning::grow(size_t part_index) { + assert(part_index + 1 < parts_count()); + assert(!is_part_empty(part_index + 1)); + boundaries_[part_index + 1]++; +} + +template +inline void partitioning::add_part_end(size_t part_index) { + assert(part_index < parts_count()); + boundaries_.insert(boundaries_.begin() + part_index + 1, boundaries_[part_index + 1]); +} + +template +inline void partitioning::add_part_begin(size_t part_index) { + assert(part_index < parts_count()); + boundaries_.insert(boundaries_.begin() + part_index, boundaries_[part_index]); +} + +template +inline void partitioning::add_parts_end(size_t part_index, size_t count) { + assert(part_index < parts_count()); + boundaries_.insert(boundaries_.begin() + part_index + 1, count, boundaries_[part_index + 1]); +} + +template +inline void partitioning::add_parts_begin(size_t part_index, size_t count) { + assert(part_index < parts_count()); + boundaries_.insert(boundaries_.begin() + part_index, count, boundaries_[part_index]); +} + +template +inline void partitioning::remove_part(size_t part_index) { + assert(part_index < parts_count()); + boundaries_.erase(boundaries_.begin() + part_index + 1); +} } // namespace positionless diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 092e5b4..80b4352 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -3,4 +3,549 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include -TEST_CASE("Example test") { CHECK(1 + 1 == 2); } +#include + +using positionless::partitioning; + +SCENARIO("partitioning - constructor with range") { + GIVEN("a vector with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + + WHEN("constructing a partitioning from the range") { + partitioning p(data.begin(), data.end()); + + THEN("it creates one part initially") { + CHECK(p.parts_count() == 1); + } + + THEN("the first part covers the entire range") { + auto [begin, end] = p.part(0); + CHECK(begin == data.begin()); + CHECK(end == data.end()); + CHECK(std::distance(begin, end) == 5); + } + + THEN("the first part is not empty") { + CHECK_FALSE(p.is_part_empty(0)); + } + } + } +} + +SCENARIO("partitioning - parts_count") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + THEN("it initially has one part") { + CHECK(p.parts_count() == 1); + } + + WHEN("adding a part at index 0") { + p.add_part_end(0); + + THEN("it has two parts") { + CHECK(p.parts_count() == 2); + } + + AND_WHEN("adding another part at index 1") { + p.add_part_end(1); + + THEN("it has three parts") { + CHECK(p.parts_count() == 3); + } + } + } + } +} + +SCENARIO("partitioning - part") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("getting the part at index 0") { + auto [begin, end] = p.part(0); + + THEN("it returns the correct iterators") { + CHECK(begin == data.begin()); + CHECK(end == data.end()); + } + } + + WHEN("adding a part and getting both parts") { + p.add_part_end(0); + auto [begin0, end0] = p.part(0); + auto [begin1, end1] = p.part(1); + + THEN("part 0 still covers the full range") { + CHECK(begin0 == data.begin()); + CHECK(end0 == data.end()); + } + + THEN("part 1 is empty at the end") { + CHECK(begin1 == data.end()); + CHECK(end1 == data.end()); + } + } + } +} + +SCENARIO("partitioning - is_part_empty") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + THEN("the initial part is not empty") { + CHECK_FALSE(p.is_part_empty(0)); + } + + WHEN("adding a new empty part") { + p.add_part_end(0); + + THEN("the original part is still not empty") { + CHECK_FALSE(p.is_part_empty(0)); + } + + THEN("the newly added part is empty") { + CHECK(p.is_part_empty(1)); + } + } + } +} + +SCENARIO("partitioning - grow") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding an empty part at the end") { + p.add_part_end(0); + + THEN("the partitioning has two parts") { + CHECK(p.parts_count() == 2); + } + + THEN("the new part is empty") { + CHECK(p.is_part_empty(1)); + } + + THEN("parts are contiguous") { + auto [begin0, end0] = p.part(0); + auto [begin1, end1] = p.part(1); + CHECK(end0 == begin1); + } + } + } +} + +SCENARIO("partitioning - add_part_end") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding a part at index 0") { + p.add_part_end(0); + + THEN("the partitioning has two parts") { + CHECK(p.parts_count() == 2); + } + + THEN("the new part is empty") { + CHECK(p.is_part_empty(1)); + } + } + + WHEN("adding multiple parts sequentially at index 0") { + p.add_part_end(0); + p.add_part_end(0); + p.add_part_end(0); + + THEN("the partitioning has four parts") { + CHECK(p.parts_count() == 4); + } + + THEN("all new parts are empty") { + CHECK(p.is_part_empty(1)); + CHECK(p.is_part_empty(2)); + CHECK(p.is_part_empty(3)); + } + } + + WHEN("adding parts at different indices") { + p.add_part_end(0); + p.add_part_end(1); + + THEN("the partitioning has three parts") { + CHECK(p.parts_count() == 3); + } + } + } +} + +SCENARIO("partitioning - add_parts_end") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding 3 parts at once") { + p.add_parts_end(0, 3); + + THEN("the partitioning has four parts") { + CHECK(p.parts_count() == 4); + } + + THEN("all new parts are empty") { + CHECK(p.is_part_empty(1)); + CHECK(p.is_part_empty(2)); + CHECK(p.is_part_empty(3)); + } + } + + WHEN("adding 0 parts") { + p.add_parts_end(0, 0); + + THEN("the part count remains unchanged") { + CHECK(p.parts_count() == 1); + } + } + } + + GIVEN("two partitionings with the same data") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p1(data.begin(), data.end()); + partitioning p2(data.begin(), data.end()); + + WHEN("one adds 1 part and the other adds 1 part using add_parts") { + p1.add_part_end(0); + p2.add_parts_end(0, 1); + + THEN("both have the same part count") { + CHECK(p1.parts_count() == p2.parts_count()); + } + } + } +} + +SCENARIO("partitioning - remove_part") { + GIVEN("a partitioning with three parts") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + p.add_part_end(0); + p.add_part_end(0); + + THEN("it has three parts") { + CHECK(p.parts_count() == 3); + } + + WHEN("removing part 1") { + p.remove_part(1); + + THEN("it has two parts") { + CHECK(p.parts_count() == 2); + } + } + } + + GIVEN("a partitioning with three parts created at once") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + p.add_parts_end(0, 2); + + THEN("it has three parts") { + CHECK(p.parts_count() == 3); + } + + WHEN("removing the middle part") { + p.remove_part(1); + + THEN("it has two parts") { + CHECK(p.parts_count() == 2); + } + + THEN("part 0 extends to the original end") { + auto [begin0, end0] = p.part(0); + CHECK(begin0 == data.begin()); + CHECK(end0 == data.end()); + } + } + } +} + +SCENARIO("partitioning - edge cases") { + GIVEN("a vector with a single element") { + std::vector data = {42}; + + WHEN("creating a partitioning from it") { + partitioning p(data.begin(), data.end()); + + THEN("it has one part") { + CHECK(p.parts_count() == 1); + } + + THEN("the part is not empty") { + CHECK_FALSE(p.is_part_empty(0)); + } + + THEN("the part contains the single element") { + auto [begin, end] = p.part(0); + CHECK(std::distance(begin, end) == 1); + CHECK(*begin == 42); + } + } + } + + GIVEN("an empty vector") { + std::vector data; + + WHEN("creating a partitioning from it") { + partitioning p(data.begin(), data.end()); + + THEN("it has one part") { + CHECK(p.parts_count() == 1); + } + + THEN("the part is empty") { + CHECK(p.is_part_empty(0)); + } + } + } + + GIVEN("a partitioning with multiple operations") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding 3 parts and then removing 2") { + p.add_parts_end(0, 3); + + THEN("it has four parts initially") { + CHECK(p.parts_count() == 4); + } + + AND_WHEN("removing part 2") { + p.remove_part(2); + + THEN("it has three parts") { + CHECK(p.parts_count() == 3); + } + + AND_WHEN("removing part 1") { + p.remove_part(1); + + THEN("it has two parts") { + CHECK(p.parts_count() == 2); + } + } + } + } + } +} + +SCENARIO("partitioning - add_part_begin") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding a part at the beginning of part 0") { + p.add_part_begin(0); + + THEN("the partitioning has two parts") { + CHECK(p.parts_count() == 2); + } + + THEN("the new part 0 is empty") { + CHECK(p.is_part_empty(0)); + } + + THEN("the new part 1 contains all elements") { + auto [begin1, end1] = p.part(1); + CHECK(std::distance(begin1, end1) == 5); + } + } + + WHEN("adding multiple parts at the beginning sequentially") { + p.add_part_begin(0); + p.add_part_begin(0); + p.add_part_begin(0); + + THEN("the partitioning has four parts") { + CHECK(p.parts_count() == 4); + } + + THEN("the first three parts are empty") { + CHECK(p.is_part_empty(0)); + CHECK(p.is_part_empty(1)); + CHECK(p.is_part_empty(2)); + } + + THEN("the last part contains all elements") { + auto [begin3, end3] = p.part(3); + CHECK(std::distance(begin3, end3) == 5); + } + } + } +} + +SCENARIO("partitioning - add_parts_begin") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p(data.begin(), data.end()); + + WHEN("adding 3 parts at the beginning at once") { + p.add_parts_begin(0, 3); + + THEN("the partitioning has four parts") { + CHECK(p.parts_count() == 4); + } + + THEN("the first three parts are empty") { + CHECK(p.is_part_empty(0)); + CHECK(p.is_part_empty(1)); + CHECK(p.is_part_empty(2)); + } + + THEN("the last part contains all elements") { + auto [begin3, end3] = p.part(3); + CHECK(std::distance(begin3, end3) == 5); + } + } + + WHEN("adding 0 parts") { + p.add_parts_begin(0, 0); + + THEN("the part count remains unchanged") { + CHECK(p.parts_count() == 1); + } + } + } + + GIVEN("two partitionings with the same data") { + std::vector data = {1, 2, 3, 4, 5}; + partitioning p1(data.begin(), data.end()); + partitioning p2(data.begin(), data.end()); + + WHEN("one adds 1 part with add_part_begin and the other with add_parts_begin") { + p1.add_part_begin(0); + p2.add_parts_begin(0, 1); + + THEN("both have the same part count") { + CHECK(p1.parts_count() == p2.parts_count()); + } + } + } +} + +SCENARIO("partitioning - grow with add_part_begin") { + GIVEN("a partitioning with 10 elements") { + std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + partitioning p(data.begin(), data.end()); + + WHEN("creating a custom partitioning using add_part_begin and grow") { + // Add two empty parts at the beginning + p.add_parts_begin(0, 2); + + THEN("we have three parts, last one with all elements") { + CHECK(p.parts_count() == 3); + CHECK(p.is_part_empty(0)); + CHECK(p.is_part_empty(1)); + CHECK_FALSE(p.is_part_empty(2)); + } + + AND_WHEN("growing part 0 to take 3 elements from part 1") { + // Part 1 is empty, so we need to grow part 1 first from part 2 + for (int i = 0; i < 3; ++i) { + p.grow(1); + } + + THEN("part 1 now has 3 elements") { + auto [b1, e1] = p.part(1); + CHECK(std::distance(b1, e1) == 3); + } + + THEN("part 2 now has 7 elements") { + auto [b2, e2] = p.part(2); + CHECK(std::distance(b2, e2) == 7); + } + + AND_WHEN("growing part 0 to take 2 elements from part 1") { + for (int i = 0; i < 2; ++i) { + p.grow(0); + } + + THEN("part 0 has 2 elements") { + auto [b0, e0] = p.part(0); + CHECK(std::distance(b0, e0) == 2); + std::vector part0_data(b0, e0); + CHECK(part0_data == std::vector{1, 2}); + } + + THEN("part 1 has 1 element") { + auto [b1, e1] = p.part(1); + CHECK(std::distance(b1, e1) == 1); + std::vector part1_data(b1, e1); + CHECK(part1_data == std::vector{3}); + } + + THEN("part 2 has 7 elements") { + auto [b2, e2] = p.part(2); + CHECK(std::distance(b2, e2) == 7); + std::vector part2_data(b2, e2); + CHECK(part2_data == std::vector{4, 5, 6, 7, 8, 9, 10}); + } + } + } + } + } +} + +SCENARIO("partitioning - complex scenario with begin and end operations") { + GIVEN("a partitioning with 10 elements") { + std::vector data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; + partitioning p(data.begin(), data.end()); + + WHEN("creating a 3-part partition: [10,20,30] [40,50,60] [70,80,90,100]") { + // Add empty parts at beginning + p.add_parts_begin(0, 2); + // Now: [empty] [empty] [10,20,30,40,50,60,70,80,90,100] + + // Grow first part to get 3 elements from part 1 + // But part 1 is empty, so first grow part 1 from part 2 + for (int i = 0; i < 6; ++i) { + p.grow(1); + } + // Now: [empty] [10,20,30,40,50,60] [70,80,90,100] + + // Now grow first part from part 1 + for (int i = 0; i < 3; ++i) { + p.grow(0); + } + // Now: [10,20,30] [40,50,60] [70,80,90,100] + + THEN("we have three parts with the expected sizes") { + CHECK(p.parts_count() == 3); + + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + CHECK(std::distance(b0, e0) == 3); + CHECK(std::distance(b1, e1) == 3); + CHECK(std::distance(b2, e2) == 4); + } + + THEN("each part contains the expected elements") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + std::vector part0(b0, e0); + std::vector part1(b1, e1); + std::vector part2(b2, e2); + + CHECK(part0 == std::vector{10, 20, 30}); + CHECK(part1 == std::vector{40, 50, 60}); + CHECK(part2 == std::vector{70, 80, 90, 100}); + } + } + } +} From b7c1a710097859014034c42034922294e5f6eba2 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sat, 29 Nov 2025 13:35:40 +0200 Subject: [PATCH 02/13] Add `swap_first` algorithm --- CMakeLists.txt | 2 +- include/positionless/algorithms.hpp | 28 ++ include/positionless/partitioning.hpp | 26 ++ test/algorithms_tests.cpp | 213 +++++++++++ test/partitioning_tests.cpp | 493 +++++++++++++++----------- test/tests_main.cpp | 2 + 6 files changed, 552 insertions(+), 212 deletions(-) create mode 100644 include/positionless/algorithms.hpp create mode 100644 test/algorithms_tests.cpp create mode 100644 test/tests_main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 621c198..64a8608 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ target_compile_options(positionless INTERFACE $<$:/W4> ) -add_executable(unit_tests test/partitioning_tests.cpp) +add_executable(unit_tests test/tests_main.cpp test/partitioning_tests.cpp test/algorithms_tests.cpp) target_link_libraries(unit_tests PRIVATE positionless doctest::doctest) enable_testing() diff --git a/include/positionless/algorithms.hpp b/include/positionless/algorithms.hpp new file mode 100644 index 0000000..d868a6f --- /dev/null +++ b/include/positionless/algorithms.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "positionless/partitioning.hpp" + +#include +#include + +namespace positionless { + +/// Swaps the first element from part `i` with the first element from part `j`. +/// +/// - Precondition: `i < parts_count()` +/// - Precondition: `j < parts_count()` +/// - Precondition: parts `i` and `j` are not empty +template +inline void swap_first(partitioning& p, size_t i, size_t j) { + assert(i < p.parts_count()); + assert(j < p.parts_count()); + assert(!p.is_part_empty(i)); + assert(!p.is_part_empty(j)); + + auto [begin_i, end_i] = p.part(i); + auto [begin_j, end_j] = p.part(j); + + std::iter_swap(begin_i, begin_j); +} + +} // namespace positionless diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 72aaf69..db1b846 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -48,6 +48,14 @@ template class partitioning { /// - Precondition: !is_part_empty(part_index + 1) void grow(size_t part_index); + /// Increases the size of the part at index `part_index` by moving its end + /// boundary forward by `n` elements, and decreasing the size of the next part. + /// + /// - Precondition: `part_index + 1 < parts_count()` + /// - Precondition: size of part `part_index + 1` >= `n` + /// - Complexity: O(n) for forward iterators, O(1) for random access iterators + void grow_by(size_t part_index, size_t n); + /// Adds a new empty part at the end of part `part_index`. /// /// - Precondition: `part_index < parts_count()` @@ -117,6 +125,24 @@ inline void partitioning::grow(size_t part_index) { boundaries_[part_index + 1]++; } +template +inline void partitioning::grow_by(size_t part_index, size_t n) { + assert(part_index + 1 < parts_count()); + + if constexpr (std::random_access_iterator) { + // For random access iterators, we can check size and advance in O(1) + auto [begin, end] = part(part_index + 1); + assert(static_cast(std::distance(begin, end)) >= n); + boundaries_[part_index + 1] += n; + } else { + // For forward iterators, we need to check and advance step by step + for (size_t i = 0; i < n; ++i) { + assert(!is_part_empty(part_index + 1)); + boundaries_[part_index + 1]++; + } + } +} + template inline void partitioning::add_part_end(size_t part_index) { assert(part_index < parts_count()); diff --git a/test/algorithms_tests.cpp b/test/algorithms_tests.cpp new file mode 100644 index 0000000..be1e4f6 --- /dev/null +++ b/test/algorithms_tests.cpp @@ -0,0 +1,213 @@ +#include "positionless/algorithms.hpp" + +#include + +#include + +using positionless::partitioning; +using positionless::swap_first; + +SCENARIO("swap_first - basic functionality") { + GIVEN("a partitioning with two parts") { + std::vector data = {1, 2, 3, 4, 5, 6}; + partitioning p(data.begin(), data.end()); + + // Create two parts: [1, 2, 3] and [4, 5, 6] + p.add_part_begin(0); + p.grow_by(0, 3); + + WHEN("swapping the first elements of both parts") { + swap_first(p, 0, 1); + + THEN("the first element of part 0 is swapped with part 1") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + + CHECK(*b0 == 4); + CHECK(*b1 == 1); + } + + THEN("the data vector is modified") { CHECK(data == std::vector{4, 2, 3, 1, 5, 6}); } + + THEN("other elements remain unchanged") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + + CHECK(*(b0 + 1) == 2); + CHECK(*(b0 + 2) == 3); + CHECK(*(b1 + 1) == 5); + CHECK(*(b1 + 2) == 6); + } + } + } +} + +SCENARIO("swap_first - same part") { + GIVEN("a partitioning with a single part") { + std::vector data = {10, 20, 30}; + partitioning p(data.begin(), data.end()); + + WHEN("swapping the first element with itself") { + swap_first(p, 0, 0); + + THEN("nothing changes") { + auto [b, e] = p.part(0); + CHECK(*b == 10); + CHECK(data == std::vector{10, 20, 30}); + } + } + } +} + +SCENARIO("swap_first - multiple parts") { + GIVEN("a partitioning with three parts") { + std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + partitioning p(data.begin(), data.end()); + + // Create three parts: [1, 2, 3] [4, 5, 6] [7, 8, 9] + p.add_parts_begin(0, 2); + // Now: [empty] [empty] [1, 2, 3, 4, 5, 6, 7, 8, 9] + + // Grow part 1 to get 6 elements from part 2 + p.grow_by(1, 6); + // Now: [empty] [1, 2, 3, 4, 5, 6] [7, 8, 9] + + // Grow part 0 to get 3 elements from part 1 + p.grow_by(0, 3); + // Now: [1, 2, 3] [4, 5, 6] [7, 8, 9] + + WHEN("swapping first elements of parts 0 and 2") { + swap_first(p, 0, 2); + + THEN("part 0 and part 2 first elements are swapped") { + auto [b0, e0] = p.part(0); + auto [b2, e2] = p.part(2); + + CHECK(*b0 == 7); + CHECK(*b2 == 1); + } + + THEN("part 1 is unchanged") { + auto [b1, e1] = p.part(1); + CHECK(*b1 == 4); + CHECK(*(b1 + 1) == 5); + CHECK(*(b1 + 2) == 6); + } + + THEN("the data vector reflects the swap") { + CHECK(data == std::vector{7, 2, 3, 4, 5, 6, 1, 8, 9}); + } + } + + WHEN("swapping first elements of parts 1 and 2") { + swap_first(p, 1, 2); + + THEN("part 1 and part 2 first elements are swapped") { + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + CHECK(*b1 == 7); + CHECK(*b2 == 4); + } + } + } +} + +SCENARIO("swap_first - single element parts") { + GIVEN("a partitioning where each part has exactly one element") { + std::vector data = {10, 20, 30, 40, 50}; + partitioning p(data.begin(), data.end()); + + // Create 5 parts, each with 1 element + p.add_parts_begin(0, 4); + // Now: [empty] [empty] [empty] [empty] [10, 20, 30, 40, 50] + + // Build from right to left - each part needs to "accumulate" elements + // for all parts to its left plus its own element + p.grow_by(3, 4); // Part 3 needs to get 4 elements total (for parts 0,1,2,3) + p.grow_by(2, 3); // Part 2 needs to get 3 elements total (for parts 0,1,2) + p.grow_by(1, 2); // Part 1 needs to get 2 elements total (for parts 0,1) + p.grow_by(0, 1); // Part 0 needs to get 1 element (for itself) + // Now: [10] [20] [30] [40] [50] + + WHEN("swapping first (and only) elements") { + swap_first(p, 1, 3); + + THEN("the single elements are swapped") { + auto [b1, e1] = p.part(1); + auto [b3, e3] = p.part(3); + + CHECK(*b1 == 40); + CHECK(*b3 == 20); + CHECK(std::distance(b1, e1) == 1); + CHECK(std::distance(b3, e3) == 1); + } + + THEN("the data reflects the swap") { CHECK(data == std::vector{10, 40, 30, 20, 50}); } + } + } +} + +SCENARIO("swap_first - different data types") { + GIVEN("a partitioning with strings") { + std::vector data = {"apple", "banana", "cherry", "date", "elderberry"}; + partitioning p(data.begin(), data.end()); + + // Create two parts: ["apple", "banana"] ["cherry", "date", "elderberry"] + p.add_part_begin(0); + p.grow_by(0, 2); + + WHEN("swapping first elements") { + swap_first(p, 0, 1); + + THEN("strings are swapped") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + + CHECK(*b0 == "cherry"); + CHECK(*b1 == "apple"); + } + + THEN("the data vector is correctly modified") { + CHECK(data[0] == "cherry"); + CHECK(data[1] == "banana"); + CHECK(data[2] == "apple"); + CHECK(data[3] == "date"); + CHECK(data[4] == "elderberry"); + } + } + } +} + +SCENARIO("swap_first - sequential swaps") { + GIVEN("a partitioning with three parts") { + std::vector data = {100, 200, 300, 400, 500, 600}; + partitioning p(data.begin(), data.end()); + + // Create three parts: [100, 200] [300, 400] [500, 600] + p.add_parts_begin(0, 2); + // Build from right to left + p.grow_by(1, 4); // Part 1 needs 4 elements (2 for itself + 2 for part 0) + p.grow_by(0, 2); // Part 0 takes 2 elements + + WHEN("performing multiple swaps in sequence") { + swap_first(p, 0, 1); + swap_first(p, 1, 2); + swap_first(p, 0, 2); + + THEN("all swaps are applied correctly") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + CHECK(*b0 == 100); + CHECK(*b1 == 500); + CHECK(*b2 == 300); + } + + THEN("final data state is correct") { + CHECK(data == std::vector{100, 200, 500, 400, 300, 600}); + } + } + } +} diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 80b4352..5953819 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -1,6 +1,5 @@ #include "positionless/partitioning.hpp" -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include @@ -10,24 +9,20 @@ using positionless::partitioning; SCENARIO("partitioning - constructor with range") { GIVEN("a vector with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; - + WHEN("constructing a partitioning from the range") { partitioning p(data.begin(), data.end()); - - THEN("it creates one part initially") { - CHECK(p.parts_count() == 1); - } - + + THEN("it creates one part initially") { CHECK(p.parts_count() == 1); } + THEN("the first part covers the entire range") { auto [begin, end] = p.part(0); CHECK(begin == data.begin()); CHECK(end == data.end()); CHECK(std::distance(begin, end) == 5); } - - THEN("the first part is not empty") { - CHECK_FALSE(p.is_part_empty(0)); - } + + THEN("the first part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } } } } @@ -36,24 +31,18 @@ SCENARIO("partitioning - parts_count") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - - THEN("it initially has one part") { - CHECK(p.parts_count() == 1); - } - + + THEN("it initially has one part") { CHECK(p.parts_count() == 1); } + WHEN("adding a part at index 0") { p.add_part_end(0); - - THEN("it has two parts") { - CHECK(p.parts_count() == 2); - } - + + THEN("it has two parts") { CHECK(p.parts_count() == 2); } + AND_WHEN("adding another part at index 1") { p.add_part_end(1); - - THEN("it has three parts") { - CHECK(p.parts_count() == 3); - } + + THEN("it has three parts") { CHECK(p.parts_count() == 3); } } } } @@ -63,26 +52,26 @@ SCENARIO("partitioning - part") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("getting the part at index 0") { auto [begin, end] = p.part(0); - + THEN("it returns the correct iterators") { CHECK(begin == data.begin()); CHECK(end == data.end()); } } - + WHEN("adding a part and getting both parts") { p.add_part_end(0); auto [begin0, end0] = p.part(0); auto [begin1, end1] = p.part(1); - + THEN("part 0 still covers the full range") { CHECK(begin0 == data.begin()); CHECK(end0 == data.end()); } - + THEN("part 1 is empty at the end") { CHECK(begin1 == data.end()); CHECK(end1 == data.end()); @@ -95,21 +84,15 @@ SCENARIO("partitioning - is_part_empty") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - - THEN("the initial part is not empty") { - CHECK_FALSE(p.is_part_empty(0)); - } - + + THEN("the initial part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } + WHEN("adding a new empty part") { p.add_part_end(0); - - THEN("the original part is still not empty") { - CHECK_FALSE(p.is_part_empty(0)); - } - - THEN("the newly added part is empty") { - CHECK(p.is_part_empty(1)); - } + + THEN("the original part is still not empty") { CHECK_FALSE(p.is_part_empty(0)); } + + THEN("the newly added part is empty") { CHECK(p.is_part_empty(1)); } } } } @@ -118,18 +101,14 @@ SCENARIO("partitioning - grow") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding an empty part at the end") { p.add_part_end(0); - - THEN("the partitioning has two parts") { - CHECK(p.parts_count() == 2); - } - - THEN("the new part is empty") { - CHECK(p.is_part_empty(1)); - } - + + THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } + + THEN("the new part is empty") { CHECK(p.is_part_empty(1)); } + THEN("parts are contiguous") { auto [begin0, end0] = p.part(0); auto [begin1, end1] = p.part(1); @@ -143,42 +122,34 @@ SCENARIO("partitioning - add_part_end") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding a part at index 0") { p.add_part_end(0); - - THEN("the partitioning has two parts") { - CHECK(p.parts_count() == 2); - } - - THEN("the new part is empty") { - CHECK(p.is_part_empty(1)); - } + + THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } + + THEN("the new part is empty") { CHECK(p.is_part_empty(1)); } } - + WHEN("adding multiple parts sequentially at index 0") { p.add_part_end(0); p.add_part_end(0); p.add_part_end(0); - - THEN("the partitioning has four parts") { - CHECK(p.parts_count() == 4); - } - + + THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } + THEN("all new parts are empty") { CHECK(p.is_part_empty(1)); CHECK(p.is_part_empty(2)); CHECK(p.is_part_empty(3)); } } - + WHEN("adding parts at different indices") { p.add_part_end(0); p.add_part_end(1); - - THEN("the partitioning has three parts") { - CHECK(p.parts_count() == 3); - } + + THEN("the partitioning has three parts") { CHECK(p.parts_count() == 3); } } } } @@ -187,42 +158,36 @@ SCENARIO("partitioning - add_parts_end") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding 3 parts at once") { p.add_parts_end(0, 3); - - THEN("the partitioning has four parts") { - CHECK(p.parts_count() == 4); - } - + + THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } + THEN("all new parts are empty") { CHECK(p.is_part_empty(1)); CHECK(p.is_part_empty(2)); CHECK(p.is_part_empty(3)); } } - + WHEN("adding 0 parts") { p.add_parts_end(0, 0); - - THEN("the part count remains unchanged") { - CHECK(p.parts_count() == 1); - } + + THEN("the part count remains unchanged") { CHECK(p.parts_count() == 1); } } } - + GIVEN("two partitionings with the same data") { std::vector data = {1, 2, 3, 4, 5}; partitioning p1(data.begin(), data.end()); partitioning p2(data.begin(), data.end()); - + WHEN("one adds 1 part and the other adds 1 part using add_parts") { p1.add_part_end(0); p2.add_parts_end(0, 1); - - THEN("both have the same part count") { - CHECK(p1.parts_count() == p2.parts_count()); - } + + THEN("both have the same part count") { CHECK(p1.parts_count() == p2.parts_count()); } } } } @@ -233,36 +198,28 @@ SCENARIO("partitioning - remove_part") { partitioning p(data.begin(), data.end()); p.add_part_end(0); p.add_part_end(0); - - THEN("it has three parts") { - CHECK(p.parts_count() == 3); - } - + + THEN("it has three parts") { CHECK(p.parts_count() == 3); } + WHEN("removing part 1") { p.remove_part(1); - - THEN("it has two parts") { - CHECK(p.parts_count() == 2); - } + + THEN("it has two parts") { CHECK(p.parts_count() == 2); } } } - + GIVEN("a partitioning with three parts created at once") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); p.add_parts_end(0, 2); - - THEN("it has three parts") { - CHECK(p.parts_count() == 3); - } - + + THEN("it has three parts") { CHECK(p.parts_count() == 3); } + WHEN("removing the middle part") { p.remove_part(1); - - THEN("it has two parts") { - CHECK(p.parts_count() == 2); - } - + + THEN("it has two parts") { CHECK(p.parts_count() == 2); } + THEN("part 0 extends to the original end") { auto [begin0, end0] = p.part(0); CHECK(begin0 == data.begin()); @@ -275,18 +232,14 @@ SCENARIO("partitioning - remove_part") { SCENARIO("partitioning - edge cases") { GIVEN("a vector with a single element") { std::vector data = {42}; - + WHEN("creating a partitioning from it") { partitioning p(data.begin(), data.end()); - - THEN("it has one part") { - CHECK(p.parts_count() == 1); - } - - THEN("the part is not empty") { - CHECK_FALSE(p.is_part_empty(0)); - } - + + THEN("it has one part") { CHECK(p.parts_count() == 1); } + + THEN("the part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } + THEN("the part contains the single element") { auto [begin, end] = p.part(0); CHECK(std::distance(begin, end) == 1); @@ -294,47 +247,37 @@ SCENARIO("partitioning - edge cases") { } } } - + GIVEN("an empty vector") { std::vector data; - + WHEN("creating a partitioning from it") { partitioning p(data.begin(), data.end()); - - THEN("it has one part") { - CHECK(p.parts_count() == 1); - } - - THEN("the part is empty") { - CHECK(p.is_part_empty(0)); - } + + THEN("it has one part") { CHECK(p.parts_count() == 1); } + + THEN("the part is empty") { CHECK(p.is_part_empty(0)); } } } - + GIVEN("a partitioning with multiple operations") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding 3 parts and then removing 2") { p.add_parts_end(0, 3); - - THEN("it has four parts initially") { - CHECK(p.parts_count() == 4); - } - + + THEN("it has four parts initially") { CHECK(p.parts_count() == 4); } + AND_WHEN("removing part 2") { p.remove_part(2); - - THEN("it has three parts") { - CHECK(p.parts_count() == 3); - } - + + THEN("it has three parts") { CHECK(p.parts_count() == 3); } + AND_WHEN("removing part 1") { p.remove_part(1); - - THEN("it has two parts") { - CHECK(p.parts_count() == 2); - } + + THEN("it has two parts") { CHECK(p.parts_count() == 2); } } } } @@ -345,39 +288,33 @@ SCENARIO("partitioning - add_part_begin") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding a part at the beginning of part 0") { p.add_part_begin(0); - - THEN("the partitioning has two parts") { - CHECK(p.parts_count() == 2); - } - - THEN("the new part 0 is empty") { - CHECK(p.is_part_empty(0)); - } - + + THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } + + THEN("the new part 0 is empty") { CHECK(p.is_part_empty(0)); } + THEN("the new part 1 contains all elements") { auto [begin1, end1] = p.part(1); CHECK(std::distance(begin1, end1) == 5); } } - + WHEN("adding multiple parts at the beginning sequentially") { p.add_part_begin(0); p.add_part_begin(0); p.add_part_begin(0); - - THEN("the partitioning has four parts") { - CHECK(p.parts_count() == 4); - } - + + THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } + THEN("the first three parts are empty") { CHECK(p.is_part_empty(0)); CHECK(p.is_part_empty(1)); CHECK(p.is_part_empty(2)); } - + THEN("the last part contains all elements") { auto [begin3, end3] = p.part(3); CHECK(std::distance(begin3, end3) == 5); @@ -390,47 +327,41 @@ SCENARIO("partitioning - add_parts_begin") { GIVEN("a partitioning with 5 elements") { std::vector data = {1, 2, 3, 4, 5}; partitioning p(data.begin(), data.end()); - + WHEN("adding 3 parts at the beginning at once") { p.add_parts_begin(0, 3); - - THEN("the partitioning has four parts") { - CHECK(p.parts_count() == 4); - } - + + THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } + THEN("the first three parts are empty") { CHECK(p.is_part_empty(0)); CHECK(p.is_part_empty(1)); CHECK(p.is_part_empty(2)); } - + THEN("the last part contains all elements") { auto [begin3, end3] = p.part(3); CHECK(std::distance(begin3, end3) == 5); } } - + WHEN("adding 0 parts") { p.add_parts_begin(0, 0); - - THEN("the part count remains unchanged") { - CHECK(p.parts_count() == 1); - } + + THEN("the part count remains unchanged") { CHECK(p.parts_count() == 1); } } } - + GIVEN("two partitionings with the same data") { std::vector data = {1, 2, 3, 4, 5}; partitioning p1(data.begin(), data.end()); partitioning p2(data.begin(), data.end()); - + WHEN("one adds 1 part with add_part_begin and the other with add_parts_begin") { p1.add_part_begin(0); p2.add_parts_begin(0, 1); - - THEN("both have the same part count") { - CHECK(p1.parts_count() == p2.parts_count()); - } + + THEN("both have the same part count") { CHECK(p1.parts_count() == p2.parts_count()); } } } } @@ -439,53 +370,49 @@ SCENARIO("partitioning - grow with add_part_begin") { GIVEN("a partitioning with 10 elements") { std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; partitioning p(data.begin(), data.end()); - + WHEN("creating a custom partitioning using add_part_begin and grow") { // Add two empty parts at the beginning p.add_parts_begin(0, 2); - + THEN("we have three parts, last one with all elements") { CHECK(p.parts_count() == 3); CHECK(p.is_part_empty(0)); CHECK(p.is_part_empty(1)); CHECK_FALSE(p.is_part_empty(2)); } - + AND_WHEN("growing part 0 to take 3 elements from part 1") { // Part 1 is empty, so we need to grow part 1 first from part 2 - for (int i = 0; i < 3; ++i) { - p.grow(1); - } - + p.grow_by(1, 3); + THEN("part 1 now has 3 elements") { auto [b1, e1] = p.part(1); CHECK(std::distance(b1, e1) == 3); } - + THEN("part 2 now has 7 elements") { auto [b2, e2] = p.part(2); CHECK(std::distance(b2, e2) == 7); } - + AND_WHEN("growing part 0 to take 2 elements from part 1") { - for (int i = 0; i < 2; ++i) { - p.grow(0); - } - + p.grow_by(0, 2); + THEN("part 0 has 2 elements") { auto [b0, e0] = p.part(0); CHECK(std::distance(b0, e0) == 2); std::vector part0_data(b0, e0); CHECK(part0_data == std::vector{1, 2}); } - + THEN("part 1 has 1 element") { auto [b1, e1] = p.part(1); CHECK(std::distance(b1, e1) == 1); std::vector part1_data(b1, e1); CHECK(part1_data == std::vector{3}); } - + THEN("part 2 has 7 elements") { auto [b2, e2] = p.part(2); CHECK(std::distance(b2, e2) == 7); @@ -502,46 +429,42 @@ SCENARIO("partitioning - complex scenario with begin and end operations") { GIVEN("a partitioning with 10 elements") { std::vector data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; partitioning p(data.begin(), data.end()); - + WHEN("creating a 3-part partition: [10,20,30] [40,50,60] [70,80,90,100]") { // Add empty parts at beginning p.add_parts_begin(0, 2); // Now: [empty] [empty] [10,20,30,40,50,60,70,80,90,100] - + // Grow first part to get 3 elements from part 1 // But part 1 is empty, so first grow part 1 from part 2 - for (int i = 0; i < 6; ++i) { - p.grow(1); - } + p.grow_by(1, 6); // Now: [empty] [10,20,30,40,50,60] [70,80,90,100] - + // Now grow first part from part 1 - for (int i = 0; i < 3; ++i) { - p.grow(0); - } + p.grow_by(0, 3); // Now: [10,20,30] [40,50,60] [70,80,90,100] - + THEN("we have three parts with the expected sizes") { CHECK(p.parts_count() == 3); - + auto [b0, e0] = p.part(0); auto [b1, e1] = p.part(1); auto [b2, e2] = p.part(2); - + CHECK(std::distance(b0, e0) == 3); CHECK(std::distance(b1, e1) == 3); CHECK(std::distance(b2, e2) == 4); } - + THEN("each part contains the expected elements") { auto [b0, e0] = p.part(0); auto [b1, e1] = p.part(1); auto [b2, e2] = p.part(2); - + std::vector part0(b0, e0); std::vector part1(b1, e1); std::vector part2(b2, e2); - + CHECK(part0 == std::vector{10, 20, 30}); CHECK(part1 == std::vector{40, 50, 60}); CHECK(part2 == std::vector{70, 80, 90, 100}); @@ -549,3 +472,151 @@ SCENARIO("partitioning - complex scenario with begin and end operations") { } } } + +SCENARIO("partitioning - grow_by basic functionality") { + GIVEN("a partitioning with 10 elements") { + std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + partitioning p(data.begin(), data.end()); + + WHEN("adding an empty part at the beginning and growing it by 3") { + p.add_part_begin(0); + // Now: [empty] [1,2,3,4,5,6,7,8,9,10] + p.grow_by(0, 3); + // Now: [1,2,3] [4,5,6,7,8,9,10] + + THEN("part 0 has 3 elements") { + auto [b0, e0] = p.part(0); + CHECK(std::distance(b0, e0) == 3); + std::vector part0_data(b0, e0); + CHECK(part0_data == std::vector{1, 2, 3}); + } + + THEN("part 1 has 7 elements") { + auto [b1, e1] = p.part(1); + CHECK(std::distance(b1, e1) == 7); + std::vector part1_data(b1, e1); + CHECK(part1_data == std::vector{4, 5, 6, 7, 8, 9, 10}); + } + } + + WHEN("growing by 0 elements") { + p.add_part_begin(0); + p.grow_by(0, 0); + + THEN("part 0 remains empty") { CHECK(p.is_part_empty(0)); } + + THEN("part 1 still has all 10 elements") { + auto [b1, e1] = p.part(1); + CHECK(std::distance(b1, e1) == 10); + } + } + } +} + +SCENARIO("partitioning - grow_by with multiple parts") { + GIVEN("a partitioning with 12 elements") { + std::vector data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}; + partitioning p(data.begin(), data.end()); + + WHEN("creating three parts using grow_by") { + p.add_parts_begin(0, 2); + // Now: [empty] [empty] [10,20,30,40,50,60,70,80,90,100,110,120] + + p.grow_by(1, 8); // Part 1 gets 8 elements + // Now: [empty] [10,20,30,40,50,60,70,80] [90,100,110,120] + + p.grow_by(0, 3); // Part 0 gets 3 elements from part 1 + // Now: [10,20,30] [40,50,60,70,80] [90,100,110,120] + + THEN("all parts have the expected sizes") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + CHECK(std::distance(b0, e0) == 3); + CHECK(std::distance(b1, e1) == 5); + CHECK(std::distance(b2, e2) == 4); + } + + THEN("each part contains the expected elements") { + auto [b0, e0] = p.part(0); + auto [b1, e1] = p.part(1); + auto [b2, e2] = p.part(2); + + std::vector part0(b0, e0); + std::vector part1(b1, e1); + std::vector part2(b2, e2); + + CHECK(part0 == std::vector{10, 20, 30}); + CHECK(part1 == std::vector{40, 50, 60, 70, 80}); + CHECK(part2 == std::vector{90, 100, 110, 120}); + } + } + } +} + +SCENARIO("partitioning - grow_by compared to multiple grow calls") { + GIVEN("two identical partitionings with 8 elements") { + std::vector data1 = {1, 2, 3, 4, 5, 6, 7, 8}; + std::vector data2 = {1, 2, 3, 4, 5, 6, 7, 8}; + partitioning p1(data1.begin(), data1.end()); + partitioning p2(data2.begin(), data2.end()); + + WHEN("using grow_by vs multiple grow calls") { + p1.add_part_begin(0); + p2.add_part_begin(0); + + // Use grow_by + p1.grow_by(0, 5); + + // Use multiple grow calls + for (int i = 0; i < 5; ++i) { + p2.grow(0); + } + + THEN("both produce the same partitioning") { + CHECK(p1.parts_count() == p2.parts_count()); + + auto [b1_0, e1_0] = p1.part(0); + auto [b2_0, e2_0] = p2.part(0); + CHECK(std::distance(b1_0, e1_0) == std::distance(b2_0, e2_0)); + + std::vector part1_0(b1_0, e1_0); + std::vector part2_0(b2_0, e2_0); + CHECK(part1_0 == part2_0); + } + + THEN("the data vectors are modified identically") { CHECK(data1 == data2); } + } + } +} + +SCENARIO("partitioning - grow_by edge cases") { + GIVEN("a partitioning with 5 elements") { + std::vector data = {100, 200, 300, 400, 500}; + partitioning p(data.begin(), data.end()); + + WHEN("growing by exactly the size of the next part") { + p.add_part_begin(0); + p.grow_by(0, 5); + + THEN("part 0 gets all elements") { + auto [b0, e0] = p.part(0); + CHECK(std::distance(b0, e0) == 5); + } + + THEN("part 1 becomes empty") { CHECK(p.is_part_empty(1)); } + } + + WHEN("using grow_by with a single element") { + p.add_part_begin(0); + p.grow_by(0, 1); + + THEN("it behaves like grow") { + auto [b0, e0] = p.part(0); + CHECK(std::distance(b0, e0) == 1); + CHECK(*b0 == 100); + } + } + } +} diff --git a/test/tests_main.cpp b/test/tests_main.cpp new file mode 100644 index 0000000..0a3f254 --- /dev/null +++ b/test/tests_main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include From 604223280e7052d4249cd75dc444c117ce29e176 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sat, 29 Nov 2025 22:07:44 +0200 Subject: [PATCH 03/13] Use rapidcheck to enable property-based testing --- .vscode/settings.json | 5 +- CMakeLists.txt | 23 +- include/positionless/partitioning.hpp | 2 +- test/algorithms_tests.cpp | 382 ++++++------ test/detail/rapidcheck_wrapper.hpp | 10 + test/detail/vector_partitioning.hpp | 78 +++ test/partitioning_tests.cpp | 831 +++++++------------------- 7 files changed, 521 insertions(+), 810 deletions(-) create mode 100644 test/detail/rapidcheck_wrapper.hpp create mode 100644 test/detail/vector_partitioning.hpp diff --git a/.vscode/settings.json b/.vscode/settings.json index a59da4f..21288fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "--clang-tidy", "--completion-style=detailed", "--header-insertion=iwyu" - ] + ], + "files.associations": { + "algorithm": "cpp" + } } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 64a8608..9cca122 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,21 @@ CPMAddPackage( VERSION 2.4.12 ) +# Add RapidCheck for property-based testing +CPMAddPackage( + NAME rapidcheck + GITHUB_REPOSITORY emil-e/rapidcheck + GIT_TAG master + OPTIONS "RC_ENABLE_TESTS OFF" "RC_ENABLE_EXAMPLES OFF" +) + +# Suppress deprecation warnings originating from RapidCheck headers only +if (TARGET rapidcheck) + target_compile_options(rapidcheck INTERFACE + $<$:-Wno-deprecated-declarations> + ) +endif() + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_library(positionless INTERFACE) @@ -32,8 +47,12 @@ target_compile_options(positionless INTERFACE $<$:/W4> ) -add_executable(unit_tests test/tests_main.cpp test/partitioning_tests.cpp test/algorithms_tests.cpp) -target_link_libraries(unit_tests PRIVATE positionless doctest::doctest) +add_executable(unit_tests + test/tests_main.cpp + test/partitioning_tests.cpp + test/algorithms_tests.cpp +) +target_link_libraries(unit_tests PRIVATE positionless doctest::doctest rapidcheck) enable_testing() add_test(NAME unit_tests COMMAND unit_tests) diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index db1b846..a6a2854 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -170,7 +170,7 @@ inline void partitioning::add_parts_begin(size_t part_index, size_t co template inline void partitioning::remove_part(size_t part_index) { assert(part_index < parts_count()); - boundaries_.erase(boundaries_.begin() + part_index + 1); + boundaries_.erase(boundaries_.begin() + part_index); } } // namespace positionless diff --git a/test/algorithms_tests.cpp b/test/algorithms_tests.cpp index be1e4f6..96a8bcc 100644 --- a/test/algorithms_tests.cpp +++ b/test/algorithms_tests.cpp @@ -1,213 +1,209 @@ #include "positionless/algorithms.hpp" -#include +#include "detail/rapidcheck_wrapper.hpp" +#include "detail/vector_partitioning.hpp" #include using positionless::partitioning; using positionless::swap_first; -SCENARIO("swap_first - basic functionality") { - GIVEN("a partitioning with two parts") { - std::vector data = {1, 2, 3, 4, 5, 6}; - partitioning p(data.begin(), data.end()); - - // Create two parts: [1, 2, 3] and [4, 5, 6] - p.add_part_begin(0); - p.grow_by(0, 3); - - WHEN("swapping the first elements of both parts") { - swap_first(p, 0, 1); - - THEN("the first element of part 0 is swapped with part 1") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - - CHECK(*b0 == 4); - CHECK(*b1 == 1); - } - - THEN("the data vector is modified") { CHECK(data == std::vector{4, 2, 3, 1, 5, 6}); } - - THEN("other elements remain unchanged") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - - CHECK(*(b0 + 1) == 2); - CHECK(*(b0 + 2) == 3); - CHECK(*(b1 + 1) == 5); - CHECK(*(b1 + 2) == 6); - } +TEST_PROPERTY("`swap_first` swaps the first elements of two parts", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 2); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } } -} - -SCENARIO("swap_first - same part") { - GIVEN("a partitioning with a single part") { - std::vector data = {10, 20, 30}; - partitioning p(data.begin(), data.end()); - - WHEN("swapping the first element with itself") { - swap_first(p, 0, 0); - - THEN("nothing changes") { - auto [b, e] = p.part(0); - CHECK(*b == 10); - CHECK(data == std::vector{10, 20, 30}); - } - } + RC_PRE(non_empty_parts.size() >= 2); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Get the original first elements + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + const auto original_first_i = *part_i.first; + const auto original_first_j = *part_j.first; + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify the swap occurred + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + if (i == j) { + // Swapping with itself should leave it unchanged + RC_ASSERT(*after_part_i.first == original_first_i); + } else { + // First elements should be swapped + RC_ASSERT(*after_part_i.first == original_first_j); + RC_ASSERT(*after_part_j.first == original_first_i); } -} - -SCENARIO("swap_first - multiple parts") { - GIVEN("a partitioning with three parts") { - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9}; - partitioning p(data.begin(), data.end()); - - // Create three parts: [1, 2, 3] [4, 5, 6] [7, 8, 9] - p.add_parts_begin(0, 2); - // Now: [empty] [empty] [1, 2, 3, 4, 5, 6, 7, 8, 9] - - // Grow part 1 to get 6 elements from part 2 - p.grow_by(1, 6); - // Now: [empty] [1, 2, 3, 4, 5, 6] [7, 8, 9] - - // Grow part 0 to get 3 elements from part 1 - p.grow_by(0, 3); - // Now: [1, 2, 3] [4, 5, 6] [7, 8, 9] - - WHEN("swapping first elements of parts 0 and 2") { - swap_first(p, 0, 2); - - THEN("part 0 and part 2 first elements are swapped") { - auto [b0, e0] = p.part(0); - auto [b2, e2] = p.part(2); - - CHECK(*b0 == 7); - CHECK(*b2 == 1); - } - - THEN("part 1 is unchanged") { - auto [b1, e1] = p.part(1); - CHECK(*b1 == 4); - CHECK(*(b1 + 1) == 5); - CHECK(*(b1 + 2) == 6); - } - - THEN("the data vector reflects the swap") { - CHECK(data == std::vector{7, 2, 3, 4, 5, 6, 1, 8, 9}); - } +}); + +TEST_PROPERTY("`swap_first` can be used multiple times without error", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 2); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } - - WHEN("swapping first elements of parts 1 and 2") { - swap_first(p, 1, 2); - - THEN("part 1 and part 2 first elements are swapped") { - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - CHECK(*b1 == 7); - CHECK(*b2 == 4); - } + } + RC_PRE(non_empty_parts.size() >= 2); + + // Generate multiple swap operations + const auto num_swaps = *rc::gen::inRange(1, 20); + + for (int k = 0; k < num_swaps; ++k) { + // Pick two random non-empty parts + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // This should not throw or violate any invariants + swap_first(vp.partitioning_, i, j); + } + + // Verify the partitioning is still valid + RC_ASSERT(vp.partitioning_.parts_count() >= 1); + + // Verify parts still cover the entire data + size_t total_size = 0; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + total_size += vp.part_size(idx); + } + RC_ASSERT(total_size == vp.data_.size()); +}); + +TEST_PROPERTY("`swap_first` on the same part should be a no-op", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 1); + + // Find a non-empty part + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } } -} - -SCENARIO("swap_first - single element parts") { - GIVEN("a partitioning where each part has exactly one element") { - std::vector data = {10, 20, 30, 40, 50}; - partitioning p(data.begin(), data.end()); - - // Create 5 parts, each with 1 element - p.add_parts_begin(0, 4); - // Now: [empty] [empty] [empty] [empty] [10, 20, 30, 40, 50] - - // Build from right to left - each part needs to "accumulate" elements - // for all parts to its left plus its own element - p.grow_by(3, 4); // Part 3 needs to get 4 elements total (for parts 0,1,2,3) - p.grow_by(2, 3); // Part 2 needs to get 3 elements total (for parts 0,1,2) - p.grow_by(1, 2); // Part 1 needs to get 2 elements total (for parts 0,1) - p.grow_by(0, 1); // Part 0 needs to get 1 element (for itself) - // Now: [10] [20] [30] [40] [50] - - WHEN("swapping first (and only) elements") { - swap_first(p, 1, 3); - - THEN("the single elements are swapped") { - auto [b1, e1] = p.part(1); - auto [b3, e3] = p.part(3); - - CHECK(*b1 == 40); - CHECK(*b3 == 20); - CHECK(std::distance(b1, e1) == 1); - CHECK(std::distance(b3, e3) == 1); - } - - THEN("the data reflects the swap") { CHECK(data == std::vector{10, 40, 30, 20, 50}); } + RC_PRE(non_empty_parts.size() >= 1); + + // Pick a random non-empty part + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Swap part with itself + swap_first(vp.partitioning_, i, i); + + // Verify data is unchanged + RC_ASSERT(vp.data_ == original_data); +}); + +TEST_PROPERTY("`swap_first` twice returns to original state (idempotent)", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 2); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } } -} - -SCENARIO("swap_first - different data types") { - GIVEN("a partitioning with strings") { - std::vector data = {"apple", "banana", "cherry", "date", "elderberry"}; - partitioning p(data.begin(), data.end()); - - // Create two parts: ["apple", "banana"] ["cherry", "date", "elderberry"] - p.add_part_begin(0); - p.grow_by(0, 2); - - WHEN("swapping first elements") { - swap_first(p, 0, 1); - - THEN("strings are swapped") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - - CHECK(*b0 == "cherry"); - CHECK(*b1 == "apple"); - } - - THEN("the data vector is correctly modified") { - CHECK(data[0] == "cherry"); - CHECK(data[1] == "banana"); - CHECK(data[2] == "apple"); - CHECK(data[3] == "date"); - CHECK(data[4] == "elderberry"); - } + RC_PRE(non_empty_parts.size() >= 2); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Swap twice + swap_first(vp.partitioning_, i, j); + swap_first(vp.partitioning_, i, j); + + // Verify data is back to original state + RC_ASSERT(vp.data_ == original_data); +}); + +TEST_PROPERTY("`swap_first` preserves data as a permutation", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 2); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } } -} - -SCENARIO("swap_first - sequential swaps") { - GIVEN("a partitioning with three parts") { - std::vector data = {100, 200, 300, 400, 500, 600}; - partitioning p(data.begin(), data.end()); - - // Create three parts: [100, 200] [300, 400] [500, 600] - p.add_parts_begin(0, 2); - // Build from right to left - p.grow_by(1, 4); // Part 1 needs 4 elements (2 for itself + 2 for part 0) - p.grow_by(0, 2); // Part 0 takes 2 elements - - WHEN("performing multiple swaps in sequence") { - swap_first(p, 0, 1); - swap_first(p, 1, 2); - swap_first(p, 0, 2); - - THEN("all swaps are applied correctly") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - CHECK(*b0 == 100); - CHECK(*b1 == 500); - CHECK(*b2 == 300); - } - - THEN("final data state is correct") { - CHECK(data == std::vector{100, 200, 500, 400, 300, 600}); - } + RC_PRE(non_empty_parts.size() >= 2); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify it's still a permutation of the original data + RC_ASSERT(std::is_permutation(vp.data_.begin(), vp.data_.end(), original_data.begin())); +}); + +TEST_PROPERTY("`swap_first` only modifies first elements of the two parts", [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= 2); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); } } -} + RC_PRE(non_empty_parts.size() >= 2); + + // Generate two part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save all elements except the first from each part (if they have more than 1 element) + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + std::vector rest_of_i(part_i.first + 1, part_i.second); + std::vector rest_of_j(part_j.first + 1, part_j.second); + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify non-first elements remain unchanged + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + std::vector new_rest_of_i(after_part_i.first + 1, after_part_i.second); + std::vector new_rest_of_j(after_part_j.first + 1, after_part_j.second); + + RC_ASSERT(new_rest_of_i == rest_of_i); + RC_ASSERT(new_rest_of_j == rest_of_j); +}); + diff --git a/test/detail/rapidcheck_wrapper.hpp b/test/detail/rapidcheck_wrapper.hpp new file mode 100644 index 0000000..b43dade --- /dev/null +++ b/test/detail/rapidcheck_wrapper.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include +#include + +/// Defines a test case that uses RapidCheck to check a property. +#define TEST_PROPERTY(name, body) \ + TEST_CASE(name) { CHECK(rc::check(name, body)); } diff --git a/test/detail/vector_partitioning.hpp b/test/detail/vector_partitioning.hpp new file mode 100644 index 0000000..52a69dc --- /dev/null +++ b/test/detail/vector_partitioning.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "positionless/partitioning.hpp" +#include "rapidcheck_wrapper.hpp" + +#include +#include +#include + +// A `std::vector` with a partitioning over its elements. +template struct vector_partitioning { + using vector_t = std::vector; + using iterator_t = typename vector_t::iterator; + + /// The underlying data. + vector_t data_; + /// The partitioning over the data. + positionless::partitioning partitioning_; + + /// An instance with `k` parts over the elements of `d`. + explicit vector_partitioning(vector_t d, size_t k) + : data_(std::move(d)), partitioning_(data_.begin(), data_.end()) { + if (k > 1) { + partitioning_.add_parts_begin(0, k - 1); + } + } + + /// Returns the size of part `i`. + size_t part_size(size_t i) const { + auto [b, e] = partitioning_.part(i); + return static_cast(std::distance(b, e)); + } +}; + +namespace rc { + +/// RapidCheck generator for vector_partitioning +template struct Arbitrary> { + static Gen> arbitrary() { + return gen::exec([]() { + const auto n = *gen::inRange(0, 64); + auto data = *gen::container>(n, gen::arbitrary()); + + const auto maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); + const auto k = *gen::inRange(1, maxK + 1); + + // Initially have only one part covering all data. + vector_partitioning r(std::move(data), 1); + + if (k == 1) { + return r; + } + + // Generate k+1 cut points in [0, n], include 0 and n, sort. + std::vector sizes = + *gen::container>(k - 1, gen::inRange(0, n + 1)); + sizes.push_back(0); + sizes.push_back(n); + std::sort(sizes.begin(), sizes.end()); + + // Transform in-place to adjacent differences; + // erase the first element (0), and the last element (to n). + std::adjacent_difference(sizes.begin(), sizes.end(), sizes.begin()); + sizes.erase(sizes.begin()); + sizes.pop_back(); + + // Add the parts, with the generated sizes. + for (size_t part_len : sizes) { + r.partitioning_.add_part_begin(r.partitioning_.parts_count() - 1); + r.partitioning_.grow_by(r.partitioning_.parts_count() - 2, part_len); + } + + return r; + }); + } +}; + +} // namespace rc diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 5953819..5c301e6 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -1,622 +1,227 @@ #include "positionless/partitioning.hpp" -#include +#include "detail/rapidcheck_wrapper.hpp" +#include "detail/vector_partitioning.hpp" #include using positionless::partitioning; -SCENARIO("partitioning - constructor with range") { - GIVEN("a vector with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; +TEST_PROPERTY("parts of a partitioning cover the entire data", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += vp.part_size(i); + } + RC_ASSERT(sum == vp.data_.size()); + return true; +}) + +TEST_PROPERTY("partitioning allows accessing all the elements", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + std::vector reconstructed; + for (size_t i = 0; i < k; ++i) { + const auto part = vp.partitioning_.part(i); + reconstructed.insert(reconstructed.end(), part.first, part.second); + } + RC_ASSERT(reconstructed == vp.data_); + return true; +}) + +TEST_PROPERTY("partitioning part count matches vector_partitioning", [](vector_partitioning vp) { + RC_ASSERT(vp.partitioning_.parts_count() >= 1); + return true; +}) + +TEST_PROPERTY("`is_part_empty` returns true for empty parts, and false otherwise", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + for (size_t i = 0; i < k; ++i) { + const bool empty = vp.partitioning_.is_part_empty(i); + const size_t size = vp.part_size(i); + RC_ASSERT(empty == (size == 0)); + } + return true; +}) + +TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= 2); + + // Find a valid index where next part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i + 1)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); + + const size_t before = vp.part_size(idx); + const size_t next_before = vp.part_size(idx + 1); + vp.partitioning_.grow(idx); + const size_t after = vp.part_size(idx); + const size_t next_after = vp.part_size(idx + 1); + + RC_ASSERT(after == before + 1); + RC_ASSERT(next_after == next_before - 1); + return true; +}) + +TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= 2); + + // Find a valid index where next part is non-empty + size_t idx = 0; + size_t max_grow = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t next_size = vp.part_size(i + 1); + if (next_size > max_grow) { + idx = i; + max_grow = next_size; + } + } + RC_PRE(max_grow > 0); + + const size_t n = *rc::gen::inRange(1, max_grow + 1); + const size_t before = vp.part_size(idx); + const size_t next_before = vp.part_size(idx + 1); + + vp.partitioning_.grow_by(idx, n); + + const size_t after = vp.part_size(idx); + const size_t next_after = vp.part_size(idx + 1); + + RC_ASSERT(after == before + n); + RC_ASSERT(next_after == next_before - n); + return true; +}) + +TEST_PROPERTY("`add_part_end` adds a new empty part at the end `part_index`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + + const size_t size_before = vp.part_size(idx); + vp.partitioning_.add_part_end(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.part_size(idx) == size_before); + RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); + return true; +}) + +TEST_PROPERTY("`add_part_begin` adds a new empty part at the begin `part_index`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + + const size_t size_before = vp.part_size(idx); + vp.partitioning_.add_part_begin(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.part_size(idx + 1) == size_before); + return true; +}) + +TEST_PROPERTY("`add_parts_end` adds `n` empty parts at the end `part_index`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.part_size(idx); + vp.partitioning_.add_parts_end(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + RC_ASSERT(vp.part_size(idx) == size_before); + for (size_t i = 1; i <= n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + return true; +}) + +TEST_PROPERTY("`add_parts_begin` adds `n` empty parts at the begin `part_index`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.part_size(idx); + vp.partitioning_.add_parts_begin(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + for (size_t i = 0; i < n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + RC_ASSERT(vp.part_size(idx + n) == size_before); + return true; +}) + +TEST_PROPERTY("`remove_part` decreases the number of parts by 1", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= 2); + + const size_t idx = *rc::gen::inRange(1, k); + vp.partitioning_.remove_part(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k - 1); + return true; +}) + +TEST_PROPERTY("`remove_part` transfer all the elements of `part_index` to part `part_index-1`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= 2); + + const size_t idx = *rc::gen::inRange(1, k); + const size_t expected_size = vp.part_size(idx - 1) + vp.part_size(idx); + + vp.partitioning_.remove_part(idx); + + RC_ASSERT(vp.part_size(idx - 1) == expected_size); + return true; +}) + +TEST_PROPERTY("`add_parts_end` is equivalent to calling `add_part_end` `n` times", [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const auto k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + // Use add_parts_end + copy1.partitioning_.add_parts_end(idx, n); + + // Use add_part_end n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_end(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.part_size(i) == copy2.part_size(i)); + } + return true; +}) + +TEST_PROPERTY("`add_parts_begin` is equivalent to calling `add_part_begin` `n` times", [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const auto k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + // Use add_parts_begin + copy1.partitioning_.add_parts_begin(idx, n); + + // Use add_part_begin n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_begin(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.part_size(i) == copy2.part_size(i)); + } + return true; +}) - WHEN("constructing a partitioning from the range") { - partitioning p(data.begin(), data.end()); - THEN("it creates one part initially") { CHECK(p.parts_count() == 1); } - - THEN("the first part covers the entire range") { - auto [begin, end] = p.part(0); - CHECK(begin == data.begin()); - CHECK(end == data.end()); - CHECK(std::distance(begin, end) == 5); - } - - THEN("the first part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } - } - } -} - -SCENARIO("partitioning - parts_count") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - THEN("it initially has one part") { CHECK(p.parts_count() == 1); } - - WHEN("adding a part at index 0") { - p.add_part_end(0); - - THEN("it has two parts") { CHECK(p.parts_count() == 2); } - - AND_WHEN("adding another part at index 1") { - p.add_part_end(1); - - THEN("it has three parts") { CHECK(p.parts_count() == 3); } - } - } - } -} - -SCENARIO("partitioning - part") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("getting the part at index 0") { - auto [begin, end] = p.part(0); - - THEN("it returns the correct iterators") { - CHECK(begin == data.begin()); - CHECK(end == data.end()); - } - } - - WHEN("adding a part and getting both parts") { - p.add_part_end(0); - auto [begin0, end0] = p.part(0); - auto [begin1, end1] = p.part(1); - - THEN("part 0 still covers the full range") { - CHECK(begin0 == data.begin()); - CHECK(end0 == data.end()); - } - - THEN("part 1 is empty at the end") { - CHECK(begin1 == data.end()); - CHECK(end1 == data.end()); - } - } - } -} - -SCENARIO("partitioning - is_part_empty") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - THEN("the initial part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } - - WHEN("adding a new empty part") { - p.add_part_end(0); - - THEN("the original part is still not empty") { CHECK_FALSE(p.is_part_empty(0)); } - - THEN("the newly added part is empty") { CHECK(p.is_part_empty(1)); } - } - } -} - -SCENARIO("partitioning - grow") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding an empty part at the end") { - p.add_part_end(0); - - THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } - - THEN("the new part is empty") { CHECK(p.is_part_empty(1)); } - - THEN("parts are contiguous") { - auto [begin0, end0] = p.part(0); - auto [begin1, end1] = p.part(1); - CHECK(end0 == begin1); - } - } - } -} - -SCENARIO("partitioning - add_part_end") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding a part at index 0") { - p.add_part_end(0); - - THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } - - THEN("the new part is empty") { CHECK(p.is_part_empty(1)); } - } - - WHEN("adding multiple parts sequentially at index 0") { - p.add_part_end(0); - p.add_part_end(0); - p.add_part_end(0); - - THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } - - THEN("all new parts are empty") { - CHECK(p.is_part_empty(1)); - CHECK(p.is_part_empty(2)); - CHECK(p.is_part_empty(3)); - } - } - - WHEN("adding parts at different indices") { - p.add_part_end(0); - p.add_part_end(1); - - THEN("the partitioning has three parts") { CHECK(p.parts_count() == 3); } - } - } -} - -SCENARIO("partitioning - add_parts_end") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding 3 parts at once") { - p.add_parts_end(0, 3); - - THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } - - THEN("all new parts are empty") { - CHECK(p.is_part_empty(1)); - CHECK(p.is_part_empty(2)); - CHECK(p.is_part_empty(3)); - } - } - - WHEN("adding 0 parts") { - p.add_parts_end(0, 0); - - THEN("the part count remains unchanged") { CHECK(p.parts_count() == 1); } - } - } - - GIVEN("two partitionings with the same data") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p1(data.begin(), data.end()); - partitioning p2(data.begin(), data.end()); - - WHEN("one adds 1 part and the other adds 1 part using add_parts") { - p1.add_part_end(0); - p2.add_parts_end(0, 1); - - THEN("both have the same part count") { CHECK(p1.parts_count() == p2.parts_count()); } - } - } -} - -SCENARIO("partitioning - remove_part") { - GIVEN("a partitioning with three parts") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - p.add_part_end(0); - p.add_part_end(0); - - THEN("it has three parts") { CHECK(p.parts_count() == 3); } - - WHEN("removing part 1") { - p.remove_part(1); - - THEN("it has two parts") { CHECK(p.parts_count() == 2); } - } - } - - GIVEN("a partitioning with three parts created at once") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - p.add_parts_end(0, 2); - - THEN("it has three parts") { CHECK(p.parts_count() == 3); } - - WHEN("removing the middle part") { - p.remove_part(1); - - THEN("it has two parts") { CHECK(p.parts_count() == 2); } - - THEN("part 0 extends to the original end") { - auto [begin0, end0] = p.part(0); - CHECK(begin0 == data.begin()); - CHECK(end0 == data.end()); - } - } - } -} - -SCENARIO("partitioning - edge cases") { - GIVEN("a vector with a single element") { - std::vector data = {42}; - - WHEN("creating a partitioning from it") { - partitioning p(data.begin(), data.end()); - - THEN("it has one part") { CHECK(p.parts_count() == 1); } - - THEN("the part is not empty") { CHECK_FALSE(p.is_part_empty(0)); } - - THEN("the part contains the single element") { - auto [begin, end] = p.part(0); - CHECK(std::distance(begin, end) == 1); - CHECK(*begin == 42); - } - } - } - - GIVEN("an empty vector") { - std::vector data; - - WHEN("creating a partitioning from it") { - partitioning p(data.begin(), data.end()); - - THEN("it has one part") { CHECK(p.parts_count() == 1); } - - THEN("the part is empty") { CHECK(p.is_part_empty(0)); } - } - } - - GIVEN("a partitioning with multiple operations") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding 3 parts and then removing 2") { - p.add_parts_end(0, 3); - - THEN("it has four parts initially") { CHECK(p.parts_count() == 4); } - - AND_WHEN("removing part 2") { - p.remove_part(2); - - THEN("it has three parts") { CHECK(p.parts_count() == 3); } - - AND_WHEN("removing part 1") { - p.remove_part(1); - - THEN("it has two parts") { CHECK(p.parts_count() == 2); } - } - } - } - } -} - -SCENARIO("partitioning - add_part_begin") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding a part at the beginning of part 0") { - p.add_part_begin(0); - - THEN("the partitioning has two parts") { CHECK(p.parts_count() == 2); } - - THEN("the new part 0 is empty") { CHECK(p.is_part_empty(0)); } - - THEN("the new part 1 contains all elements") { - auto [begin1, end1] = p.part(1); - CHECK(std::distance(begin1, end1) == 5); - } - } - - WHEN("adding multiple parts at the beginning sequentially") { - p.add_part_begin(0); - p.add_part_begin(0); - p.add_part_begin(0); - - THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } - - THEN("the first three parts are empty") { - CHECK(p.is_part_empty(0)); - CHECK(p.is_part_empty(1)); - CHECK(p.is_part_empty(2)); - } - - THEN("the last part contains all elements") { - auto [begin3, end3] = p.part(3); - CHECK(std::distance(begin3, end3) == 5); - } - } - } -} - -SCENARIO("partitioning - add_parts_begin") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p(data.begin(), data.end()); - - WHEN("adding 3 parts at the beginning at once") { - p.add_parts_begin(0, 3); - - THEN("the partitioning has four parts") { CHECK(p.parts_count() == 4); } - - THEN("the first three parts are empty") { - CHECK(p.is_part_empty(0)); - CHECK(p.is_part_empty(1)); - CHECK(p.is_part_empty(2)); - } - - THEN("the last part contains all elements") { - auto [begin3, end3] = p.part(3); - CHECK(std::distance(begin3, end3) == 5); - } - } - - WHEN("adding 0 parts") { - p.add_parts_begin(0, 0); - - THEN("the part count remains unchanged") { CHECK(p.parts_count() == 1); } - } - } - - GIVEN("two partitionings with the same data") { - std::vector data = {1, 2, 3, 4, 5}; - partitioning p1(data.begin(), data.end()); - partitioning p2(data.begin(), data.end()); - - WHEN("one adds 1 part with add_part_begin and the other with add_parts_begin") { - p1.add_part_begin(0); - p2.add_parts_begin(0, 1); - - THEN("both have the same part count") { CHECK(p1.parts_count() == p2.parts_count()); } - } - } -} - -SCENARIO("partitioning - grow with add_part_begin") { - GIVEN("a partitioning with 10 elements") { - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - partitioning p(data.begin(), data.end()); - - WHEN("creating a custom partitioning using add_part_begin and grow") { - // Add two empty parts at the beginning - p.add_parts_begin(0, 2); - - THEN("we have three parts, last one with all elements") { - CHECK(p.parts_count() == 3); - CHECK(p.is_part_empty(0)); - CHECK(p.is_part_empty(1)); - CHECK_FALSE(p.is_part_empty(2)); - } - - AND_WHEN("growing part 0 to take 3 elements from part 1") { - // Part 1 is empty, so we need to grow part 1 first from part 2 - p.grow_by(1, 3); - - THEN("part 1 now has 3 elements") { - auto [b1, e1] = p.part(1); - CHECK(std::distance(b1, e1) == 3); - } - - THEN("part 2 now has 7 elements") { - auto [b2, e2] = p.part(2); - CHECK(std::distance(b2, e2) == 7); - } - - AND_WHEN("growing part 0 to take 2 elements from part 1") { - p.grow_by(0, 2); - - THEN("part 0 has 2 elements") { - auto [b0, e0] = p.part(0); - CHECK(std::distance(b0, e0) == 2); - std::vector part0_data(b0, e0); - CHECK(part0_data == std::vector{1, 2}); - } - - THEN("part 1 has 1 element") { - auto [b1, e1] = p.part(1); - CHECK(std::distance(b1, e1) == 1); - std::vector part1_data(b1, e1); - CHECK(part1_data == std::vector{3}); - } - - THEN("part 2 has 7 elements") { - auto [b2, e2] = p.part(2); - CHECK(std::distance(b2, e2) == 7); - std::vector part2_data(b2, e2); - CHECK(part2_data == std::vector{4, 5, 6, 7, 8, 9, 10}); - } - } - } - } - } -} - -SCENARIO("partitioning - complex scenario with begin and end operations") { - GIVEN("a partitioning with 10 elements") { - std::vector data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; - partitioning p(data.begin(), data.end()); - - WHEN("creating a 3-part partition: [10,20,30] [40,50,60] [70,80,90,100]") { - // Add empty parts at beginning - p.add_parts_begin(0, 2); - // Now: [empty] [empty] [10,20,30,40,50,60,70,80,90,100] - - // Grow first part to get 3 elements from part 1 - // But part 1 is empty, so first grow part 1 from part 2 - p.grow_by(1, 6); - // Now: [empty] [10,20,30,40,50,60] [70,80,90,100] - - // Now grow first part from part 1 - p.grow_by(0, 3); - // Now: [10,20,30] [40,50,60] [70,80,90,100] - - THEN("we have three parts with the expected sizes") { - CHECK(p.parts_count() == 3); - - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - CHECK(std::distance(b0, e0) == 3); - CHECK(std::distance(b1, e1) == 3); - CHECK(std::distance(b2, e2) == 4); - } - - THEN("each part contains the expected elements") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - std::vector part0(b0, e0); - std::vector part1(b1, e1); - std::vector part2(b2, e2); - - CHECK(part0 == std::vector{10, 20, 30}); - CHECK(part1 == std::vector{40, 50, 60}); - CHECK(part2 == std::vector{70, 80, 90, 100}); - } - } - } -} - -SCENARIO("partitioning - grow_by basic functionality") { - GIVEN("a partitioning with 10 elements") { - std::vector data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; - partitioning p(data.begin(), data.end()); - - WHEN("adding an empty part at the beginning and growing it by 3") { - p.add_part_begin(0); - // Now: [empty] [1,2,3,4,5,6,7,8,9,10] - p.grow_by(0, 3); - // Now: [1,2,3] [4,5,6,7,8,9,10] - - THEN("part 0 has 3 elements") { - auto [b0, e0] = p.part(0); - CHECK(std::distance(b0, e0) == 3); - std::vector part0_data(b0, e0); - CHECK(part0_data == std::vector{1, 2, 3}); - } - - THEN("part 1 has 7 elements") { - auto [b1, e1] = p.part(1); - CHECK(std::distance(b1, e1) == 7); - std::vector part1_data(b1, e1); - CHECK(part1_data == std::vector{4, 5, 6, 7, 8, 9, 10}); - } - } - - WHEN("growing by 0 elements") { - p.add_part_begin(0); - p.grow_by(0, 0); - - THEN("part 0 remains empty") { CHECK(p.is_part_empty(0)); } - - THEN("part 1 still has all 10 elements") { - auto [b1, e1] = p.part(1); - CHECK(std::distance(b1, e1) == 10); - } - } - } -} - -SCENARIO("partitioning - grow_by with multiple parts") { - GIVEN("a partitioning with 12 elements") { - std::vector data = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120}; - partitioning p(data.begin(), data.end()); - - WHEN("creating three parts using grow_by") { - p.add_parts_begin(0, 2); - // Now: [empty] [empty] [10,20,30,40,50,60,70,80,90,100,110,120] - - p.grow_by(1, 8); // Part 1 gets 8 elements - // Now: [empty] [10,20,30,40,50,60,70,80] [90,100,110,120] - - p.grow_by(0, 3); // Part 0 gets 3 elements from part 1 - // Now: [10,20,30] [40,50,60,70,80] [90,100,110,120] - - THEN("all parts have the expected sizes") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - CHECK(std::distance(b0, e0) == 3); - CHECK(std::distance(b1, e1) == 5); - CHECK(std::distance(b2, e2) == 4); - } - - THEN("each part contains the expected elements") { - auto [b0, e0] = p.part(0); - auto [b1, e1] = p.part(1); - auto [b2, e2] = p.part(2); - - std::vector part0(b0, e0); - std::vector part1(b1, e1); - std::vector part2(b2, e2); - - CHECK(part0 == std::vector{10, 20, 30}); - CHECK(part1 == std::vector{40, 50, 60, 70, 80}); - CHECK(part2 == std::vector{90, 100, 110, 120}); - } - } - } -} - -SCENARIO("partitioning - grow_by compared to multiple grow calls") { - GIVEN("two identical partitionings with 8 elements") { - std::vector data1 = {1, 2, 3, 4, 5, 6, 7, 8}; - std::vector data2 = {1, 2, 3, 4, 5, 6, 7, 8}; - partitioning p1(data1.begin(), data1.end()); - partitioning p2(data2.begin(), data2.end()); - - WHEN("using grow_by vs multiple grow calls") { - p1.add_part_begin(0); - p2.add_part_begin(0); - - // Use grow_by - p1.grow_by(0, 5); - - // Use multiple grow calls - for (int i = 0; i < 5; ++i) { - p2.grow(0); - } - - THEN("both produce the same partitioning") { - CHECK(p1.parts_count() == p2.parts_count()); - - auto [b1_0, e1_0] = p1.part(0); - auto [b2_0, e2_0] = p2.part(0); - CHECK(std::distance(b1_0, e1_0) == std::distance(b2_0, e2_0)); - - std::vector part1_0(b1_0, e1_0); - std::vector part2_0(b2_0, e2_0); - CHECK(part1_0 == part2_0); - } - - THEN("the data vectors are modified identically") { CHECK(data1 == data2); } - } - } -} - -SCENARIO("partitioning - grow_by edge cases") { - GIVEN("a partitioning with 5 elements") { - std::vector data = {100, 200, 300, 400, 500}; - partitioning p(data.begin(), data.end()); - - WHEN("growing by exactly the size of the next part") { - p.add_part_begin(0); - p.grow_by(0, 5); - - THEN("part 0 gets all elements") { - auto [b0, e0] = p.part(0); - CHECK(std::distance(b0, e0) == 5); - } - - THEN("part 1 becomes empty") { CHECK(p.is_part_empty(1)); } - } - - WHEN("using grow_by with a single element") { - p.add_part_begin(0); - p.grow_by(0, 1); - - THEN("it behaves like grow") { - auto [b0, e0] = p.part(0); - CHECK(std::distance(b0, e0) == 1); - CHECK(*b0 == 100); - } - } - } -} From 2da5c4edd2f0558a700fcf30c5b3bdfdd9ad75a0 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sat, 29 Nov 2025 22:50:43 +0200 Subject: [PATCH 04/13] Use precondition checks instead of assert, ensuring that the precondition is always checked. --- include/positionless/algorithms.hpp | 9 ++++--- include/positionless/detail/precondition.hpp | 16 ++++++++++++ include/positionless/partitioning.hpp | 27 ++++++++++---------- 3 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 include/positionless/detail/precondition.hpp diff --git a/include/positionless/algorithms.hpp b/include/positionless/algorithms.hpp index d868a6f..0480819 100644 --- a/include/positionless/algorithms.hpp +++ b/include/positionless/algorithms.hpp @@ -1,5 +1,6 @@ #pragma once +#include "positionless/detail/precondition.hpp" #include "positionless/partitioning.hpp" #include @@ -14,10 +15,10 @@ namespace positionless { /// - Precondition: parts `i` and `j` are not empty template inline void swap_first(partitioning& p, size_t i, size_t j) { - assert(i < p.parts_count()); - assert(j < p.parts_count()); - assert(!p.is_part_empty(i)); - assert(!p.is_part_empty(j)); + PRECONDITION(i < p.parts_count()); + PRECONDITION(j < p.parts_count()); + PRECONDITION(!p.is_part_empty(i)); + PRECONDITION(!p.is_part_empty(j)); auto [begin_i, end_i] = p.part(i); auto [begin_j, end_j] = p.part(j); diff --git a/include/positionless/detail/precondition.hpp b/include/positionless/detail/precondition.hpp new file mode 100644 index 0000000..045228f --- /dev/null +++ b/include/positionless/detail/precondition.hpp @@ -0,0 +1,16 @@ +#pragma once + +#if defined(NDEBUG) + +/// Assert-like precondition check that calls `std::terminate` on failure. +#define PRECONDITION(expr) \ + do { \ + if (!(expr)) \ + std::terminate(); \ + } while (false) +#else + +#include +#define PRECONDITION(expr) assert(expr) + +#endif \ No newline at end of file diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index a6a2854..418cc08 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include "positionless/detail/precondition.hpp" + #include #include #include @@ -107,37 +108,37 @@ inline size_t partitioning::parts_count() const noexcept { template inline std::pair partitioning::part(size_t part_index) const noexcept { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); return {boundaries_[part_index], boundaries_[part_index + 1]}; } template inline bool partitioning::is_part_empty(size_t part_index) const noexcept { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); auto [begin, end] = part(part_index); return begin == end; } template inline void partitioning::grow(size_t part_index) { - assert(part_index + 1 < parts_count()); - assert(!is_part_empty(part_index + 1)); + PRECONDITION(part_index + 1 < parts_count()); + PRECONDITION(!is_part_empty(part_index + 1)); boundaries_[part_index + 1]++; } template inline void partitioning::grow_by(size_t part_index, size_t n) { - assert(part_index + 1 < parts_count()); + PRECONDITION(part_index + 1 < parts_count()); if constexpr (std::random_access_iterator) { // For random access iterators, we can check size and advance in O(1) auto [begin, end] = part(part_index + 1); - assert(static_cast(std::distance(begin, end)) >= n); + PRECONDITION(static_cast(std::distance(begin, end)) >= n); boundaries_[part_index + 1] += n; } else { // For forward iterators, we need to check and advance step by step for (size_t i = 0; i < n; ++i) { - assert(!is_part_empty(part_index + 1)); + PRECONDITION(!is_part_empty(part_index + 1)); boundaries_[part_index + 1]++; } } @@ -145,31 +146,31 @@ inline void partitioning::grow_by(size_t part_index, size_t n) { template inline void partitioning::add_part_end(size_t part_index) { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); boundaries_.insert(boundaries_.begin() + part_index + 1, boundaries_[part_index + 1]); } template inline void partitioning::add_part_begin(size_t part_index) { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); boundaries_.insert(boundaries_.begin() + part_index, boundaries_[part_index]); } template inline void partitioning::add_parts_end(size_t part_index, size_t count) { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); boundaries_.insert(boundaries_.begin() + part_index + 1, count, boundaries_[part_index + 1]); } template inline void partitioning::add_parts_begin(size_t part_index, size_t count) { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); boundaries_.insert(boundaries_.begin() + part_index, count, boundaries_[part_index]); } template inline void partitioning::remove_part(size_t part_index) { - assert(part_index < parts_count()); + PRECONDITION(part_index < parts_count()); boundaries_.erase(boundaries_.begin() + part_index); } From 8d39ddc2ee14472771aa98061b735f090313d953 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sat, 29 Nov 2025 23:55:55 +0200 Subject: [PATCH 05/13] Add forward_list tests --- include/positionless/partitioning.hpp | 17 +++ test/algorithms_tests.cpp | 2 +- test/detail/forward_list_partitioning.hpp | 72 ++++++++++++ test/detail/vector_partitioning.hpp | 14 +-- test/partitioning_tests.cpp | 130 +++++++++++++++++----- 5 files changed, 198 insertions(+), 37 deletions(-) create mode 100644 test/detail/forward_list_partitioning.hpp diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 418cc08..71b5136 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -42,6 +42,15 @@ template class partitioning { [[nodiscard]] bool is_part_empty(size_t part_index) const noexcept; + /// Returns the size of the part at index `part_index`. + /// + /// Defined only for random access collections. + /// + /// - Precondition: `part_index < parts_count()` + [[nodiscard]] + size_t part_size(size_t part_index) const + requires std::random_access_iterator; + /// Increases the size of the part at index `part_index` by moving its end /// boundary forward by one element, and decreasing the size of the next part. /// @@ -119,6 +128,14 @@ inline bool partitioning::is_part_empty(size_t part_index) const noexc return begin == end; } +template +inline size_t partitioning::part_size(size_t part_index) const + requires std::random_access_iterator +{ + PRECONDITION(part_index < parts_count()); + return static_cast(boundaries_[part_index + 1] - boundaries_[part_index]); +} + template inline void partitioning::grow(size_t part_index) { PRECONDITION(part_index + 1 < parts_count()); diff --git a/test/algorithms_tests.cpp b/test/algorithms_tests.cpp index 96a8bcc..e8c424d 100644 --- a/test/algorithms_tests.cpp +++ b/test/algorithms_tests.cpp @@ -81,7 +81,7 @@ TEST_PROPERTY("`swap_first` can be used multiple times without error", [](vector // Verify parts still cover the entire data size_t total_size = 0; for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - total_size += vp.part_size(idx); + total_size += vp.partitioning_.part_size(idx); } RC_ASSERT(total_size == vp.data_.size()); }); diff --git a/test/detail/forward_list_partitioning.hpp b/test/detail/forward_list_partitioning.hpp new file mode 100644 index 0000000..98e36ec --- /dev/null +++ b/test/detail/forward_list_partitioning.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "positionless/partitioning.hpp" +#include "rapidcheck_wrapper.hpp" + +#include +#include +#include + +// A `std::forward_list` with a partitioning over its elements. +template struct forward_list_partitioning { + using container_t = std::forward_list; + using iterator_t = typename container_t::iterator; + + /// The underlying data. + container_t data_; + /// The partitioning over the data. + positionless::partitioning partitioning_; + + /// An instance with `k` parts over the elements of `d`. + explicit forward_list_partitioning(container_t d, size_t k = 1) + : data_(std::move(d)), partitioning_(data_.begin(), data_.end()) { + if (k > 1) { + partitioning_.add_parts_begin(0, k - 1); + } + } +}; + +namespace rc { + +/// RapidCheck generator for forward_list_partitioning +template struct Arbitrary> { + static Gen> arbitrary() { + return gen::exec([]() { + const auto n = *gen::inRange(0, 64); + auto data = *gen::container>(n, gen::arbitrary()); + + const auto maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); + const auto k = *gen::inRange(1, maxK + 1); + + // Initially have only one part covering all data. + forward_list_partitioning r(typename forward_list_partitioning::container_t(data.begin(), data.end()), 1); + + if (k == 1) { + return r; + } + + // Generate k+1 cut points in [0, n], include 0 and n, sort. + std::vector sizes = + *gen::container>(k - 1, gen::inRange(0, n + 1)); + sizes.push_back(0); + sizes.push_back(n); + std::sort(sizes.begin(), sizes.end()); + + // Transform in-place to adjacent differences; + // erase the first element (0), and the last element (to n). + std::adjacent_difference(sizes.begin(), sizes.end(), sizes.begin()); + sizes.erase(sizes.begin()); + sizes.pop_back(); + + // Add the parts, with the generated sizes. + for (size_t part_len : sizes) { + r.partitioning_.add_part_begin(r.partitioning_.parts_count() - 1); + r.partitioning_.grow_by(r.partitioning_.parts_count() - 2, part_len); + } + + return r; + }); + } +}; + +} // namespace rc diff --git a/test/detail/vector_partitioning.hpp b/test/detail/vector_partitioning.hpp index 52a69dc..2c247ee 100644 --- a/test/detail/vector_partitioning.hpp +++ b/test/detail/vector_partitioning.hpp @@ -9,27 +9,21 @@ // A `std::vector` with a partitioning over its elements. template struct vector_partitioning { - using vector_t = std::vector; - using iterator_t = typename vector_t::iterator; + using container_t = std::vector; + using iterator_t = typename container_t::iterator; /// The underlying data. - vector_t data_; + container_t data_; /// The partitioning over the data. positionless::partitioning partitioning_; /// An instance with `k` parts over the elements of `d`. - explicit vector_partitioning(vector_t d, size_t k) + explicit vector_partitioning(container_t d, size_t k = 1) : data_(std::move(d)), partitioning_(data_.begin(), data_.end()) { if (k > 1) { partitioning_.add_parts_begin(0, k - 1); } } - - /// Returns the size of part `i`. - size_t part_size(size_t i) const { - auto [b, e] = partitioning_.part(i); - return static_cast(std::distance(b, e)); - } }; namespace rc { diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 5c301e6..118da0d 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -2,6 +2,7 @@ #include "detail/rapidcheck_wrapper.hpp" #include "detail/vector_partitioning.hpp" +#include "detail/forward_list_partitioning.hpp" #include @@ -11,7 +12,7 @@ TEST_PROPERTY("parts of a partitioning cover the entire data", [](vector_partiti const auto k = vp.partitioning_.parts_count(); size_t sum = 0; for (size_t i = 0; i < k; ++i) { - sum += vp.part_size(i); + sum += vp.partitioning_.part_size(i); } RC_ASSERT(sum == vp.data_.size()); return true; @@ -29,7 +30,7 @@ TEST_PROPERTY("partitioning allows accessing all the elements", [](vector_partit }) TEST_PROPERTY("partitioning part count matches vector_partitioning", [](vector_partitioning vp) { - RC_ASSERT(vp.partitioning_.parts_count() >= 1); + RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); return true; }) @@ -37,7 +38,7 @@ TEST_PROPERTY("`is_part_empty` returns true for empty parts, and false otherwise const auto k = vp.partitioning_.parts_count(); for (size_t i = 0; i < k; ++i) { const bool empty = vp.partitioning_.is_part_empty(i); - const size_t size = vp.part_size(i); + const size_t size = vp.partitioning_.part_size(i); RC_ASSERT(empty == (size == 0)); } return true; @@ -45,7 +46,7 @@ TEST_PROPERTY("`is_part_empty` returns true for empty parts, and false otherwise TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); - RC_PRE(k >= 2); + RC_PRE(k >= size_t(2)); // Find a valid index where next part is non-empty size_t idx = 0; @@ -57,11 +58,11 @@ TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning } RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); - const size_t before = vp.part_size(idx); - const size_t next_before = vp.part_size(idx + 1); + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); vp.partitioning_.grow(idx); - const size_t after = vp.part_size(idx); - const size_t next_after = vp.part_size(idx + 1); + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); RC_ASSERT(after == before + 1); RC_ASSERT(next_after == next_before - 1); @@ -76,22 +77,22 @@ TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partiti size_t idx = 0; size_t max_grow = 0; for (size_t i = 0; i + 1 < k; ++i) { - const size_t next_size = vp.part_size(i + 1); + const size_t next_size = vp.partitioning_.part_size(i + 1); if (next_size > max_grow) { idx = i; max_grow = next_size; } } - RC_PRE(max_grow > 0); + RC_PRE(max_grow > size_t(0)); const size_t n = *rc::gen::inRange(1, max_grow + 1); - const size_t before = vp.part_size(idx); - const size_t next_before = vp.part_size(idx + 1); + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); vp.partitioning_.grow_by(idx, n); - const size_t after = vp.part_size(idx); - const size_t next_after = vp.part_size(idx + 1); + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); RC_ASSERT(after == before + n); RC_ASSERT(next_after == next_before - n); @@ -102,11 +103,11 @@ TEST_PROPERTY("`add_part_end` adds a new empty part at the end `part_index`", [] const auto k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); - const size_t size_before = vp.part_size(idx); + const size_t size_before = vp.partitioning_.part_size(idx); vp.partitioning_.add_part_end(idx); RC_ASSERT(vp.partitioning_.parts_count() == k + 1); - RC_ASSERT(vp.part_size(idx) == size_before); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); return true; }) @@ -115,12 +116,12 @@ TEST_PROPERTY("`add_part_begin` adds a new empty part at the begin `part_index`" const auto k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); - const size_t size_before = vp.part_size(idx); + const size_t size_before = vp.partitioning_.part_size(idx); vp.partitioning_.add_part_begin(idx); RC_ASSERT(vp.partitioning_.parts_count() == k + 1); RC_ASSERT(vp.partitioning_.is_part_empty(idx)); - RC_ASSERT(vp.part_size(idx + 1) == size_before); + RC_ASSERT(vp.partitioning_.part_size(idx + 1) == size_before); return true; }) @@ -129,11 +130,11 @@ TEST_PROPERTY("`add_parts_end` adds `n` empty parts at the end `part_index`", [] const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); - const size_t size_before = vp.part_size(idx); + const size_t size_before = vp.partitioning_.part_size(idx); vp.partitioning_.add_parts_end(idx, n); RC_ASSERT(vp.partitioning_.parts_count() == k + n); - RC_ASSERT(vp.part_size(idx) == size_before); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); for (size_t i = 1; i <= n; ++i) { RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); } @@ -145,14 +146,14 @@ TEST_PROPERTY("`add_parts_begin` adds `n` empty parts at the begin `part_index`" const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); - const size_t size_before = vp.part_size(idx); + const size_t size_before = vp.partitioning_.part_size(idx); vp.partitioning_.add_parts_begin(idx, n); RC_ASSERT(vp.partitioning_.parts_count() == k + n); for (size_t i = 0; i < n; ++i) { RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); } - RC_ASSERT(vp.part_size(idx + n) == size_before); + RC_ASSERT(vp.partitioning_.part_size(idx + n) == size_before); return true; }) @@ -172,11 +173,11 @@ TEST_PROPERTY("`remove_part` transfer all the elements of `part_index` to part ` RC_PRE(k >= 2); const size_t idx = *rc::gen::inRange(1, k); - const size_t expected_size = vp.part_size(idx - 1) + vp.part_size(idx); + const size_t expected_size = vp.partitioning_.part_size(idx - 1) + vp.partitioning_.part_size(idx); vp.partitioning_.remove_part(idx); - RC_ASSERT(vp.part_size(idx - 1) == expected_size); + RC_ASSERT(vp.partitioning_.part_size(idx - 1) == expected_size); return true; }) @@ -197,7 +198,7 @@ TEST_PROPERTY("`add_parts_end` is equivalent to calling `add_part_end` `n` times RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { - RC_ASSERT(copy1.part_size(i) == copy2.part_size(i)); + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); } return true; }) @@ -219,8 +220,85 @@ TEST_PROPERTY("`add_parts_begin` is equivalent to calling `add_part_begin` `n` t RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { - RC_ASSERT(copy1.part_size(i) == copy2.part_size(i)); + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + return true; +}) + +TEST_PROPERTY("forward_list partitioning covers entire data", [](forward_list_partitioning fp) { + const auto k = fp.partitioning_.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) sum += std::distance(fp.partitioning_.part(i).first, fp.partitioning_.part(i).second); + RC_ASSERT(static_cast(sum) == std::distance(fp.data_.begin(), fp.data_.end())); + return true; +}) + +TEST_PROPERTY("forward_list basic ops: grow/grow_by/add/remove", [](forward_list_partitioning fp) { + const auto k0 = fp.partitioning_.parts_count(); + RC_PRE(k0 >= 1); + + // add_part_begin at a random index + const size_t idx_add = *rc::gen::inRange(0, k0); + fp.partitioning_.add_part_begin(idx_add); + RC_ASSERT(fp.partitioning_.is_part_empty(idx_add)); + + // ensure we have at least two parts to operate grow/grow_by + const auto k1 = fp.partitioning_.parts_count(); + RC_PRE(k1 >= 2); + + // pick an index with non-empty next part for grow + size_t idx_grow = 0; + bool found = false; + for (size_t i = 0; i + 1 < k1; ++i) { + auto next = fp.partitioning_.part(i + 1); + if (next.first != next.second) { idx_grow = i; found = true; break; } + } + RC_PRE(found); + + // measure sizes + auto cur = fp.partitioning_.part(idx_grow); + auto nxt = fp.partitioning_.part(idx_grow + 1); + const size_t cur_before = std::distance(cur.first, cur.second); + const size_t nxt_before = std::distance(nxt.first, nxt.second); + + fp.partitioning_.grow(idx_grow); + + cur = fp.partitioning_.part(idx_grow); + nxt = fp.partitioning_.part(idx_grow + 1); + RC_ASSERT(std::distance(cur.first, cur.second) == cur_before + 1); + RC_ASSERT(std::distance(nxt.first, nxt.second) == nxt_before - 1); + + // grow_by on possibly different index + size_t idx_grow_by = idx_grow; + size_t max_grow = 0; + for (size_t i = 0; i + 1 < k1; ++i) { + auto next2 = fp.partitioning_.part(i + 1); + const size_t ns = std::distance(next2.first, next2.second); + if (ns > max_grow) { max_grow = ns; idx_grow_by = i; } } + RC_PRE(max_grow > size_t(0)); + const size_t n = *rc::gen::inRange(1, max_grow + 1); + auto cur2 = fp.partitioning_.part(idx_grow_by); + auto nxt2 = fp.partitioning_.part(idx_grow_by + 1); + const size_t cur2_before = std::distance(cur2.first, cur2.second); + const size_t nxt2_before = std::distance(nxt2.first, nxt2.second); + fp.partitioning_.grow_by(idx_grow_by, n); + cur2 = fp.partitioning_.part(idx_grow_by); + nxt2 = fp.partitioning_.part(idx_grow_by + 1); + RC_ASSERT(std::distance(cur2.first, cur2.second) == cur2_before + n); + RC_ASSERT(std::distance(nxt2.first, nxt2.second) == nxt2_before - n); + + // remove_part at valid index (>0) + const auto k2 = fp.partitioning_.parts_count(); + RC_PRE(k2 >= 2); + const size_t idx_rem = *rc::gen::inRange(1, k2); + auto left = fp.partitioning_.part(idx_rem - 1); + auto rem = fp.partitioning_.part(idx_rem); + const size_t expected = std::distance(left.first, left.second) + std::distance(rem.first, rem.second); + fp.partitioning_.remove_part(idx_rem); + auto after_left = fp.partitioning_.part(idx_rem - 1); + RC_ASSERT(std::distance(after_left.first, after_left.second) == expected); + RC_ASSERT(fp.partitioning_.parts_count() == k2 - 1); return true; }) From ebefe5db40f26f73ca5aa872449a1b8b6818df0c Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sun, 30 Nov 2025 00:06:01 +0200 Subject: [PATCH 06/13] Playing with clang-format. --- .clang-format | 5 + test/partitioning_tests.cpp | 451 ++++++++++++++++++++---------------- 2 files changed, 252 insertions(+), 204 deletions(-) diff --git a/.clang-format b/.clang-format index f284ebd..42556a6 100644 --- a/.clang-format +++ b/.clang-format @@ -6,3 +6,8 @@ Language: Cpp DerivePointerAlignment: false PointerAlignment: Left ColumnLimit: 100 +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +BinPackArguments: false +BinPackParameters: false +AlignAfterOpenBracket: BlockIndent diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 118da0d..58328c4 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -1,8 +1,8 @@ #include "positionless/partitioning.hpp" +#include "detail/forward_list_partitioning.hpp" #include "detail/rapidcheck_wrapper.hpp" #include "detail/vector_partitioning.hpp" -#include "detail/forward_list_partitioning.hpp" #include @@ -29,25 +29,31 @@ TEST_PROPERTY("partitioning allows accessing all the elements", [](vector_partit return true; }) -TEST_PROPERTY("partitioning part count matches vector_partitioning", [](vector_partitioning vp) { - RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); - return true; -}) +TEST_PROPERTY( + "partitioning part count matches vector_partitioning", + [](vector_partitioning vp) { + RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); + return true; + } +) -TEST_PROPERTY("`is_part_empty` returns true for empty parts, and false otherwise", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - for (size_t i = 0; i < k; ++i) { - const bool empty = vp.partitioning_.is_part_empty(i); - const size_t size = vp.partitioning_.part_size(i); - RC_ASSERT(empty == (size == 0)); - } - return true; -}) +TEST_PROPERTY( + "`is_part_empty` returns true for empty parts, and false otherwise", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + for (size_t i = 0; i < k; ++i) { + const bool empty = vp.partitioning_.is_part_empty(i); + const size_t size = vp.partitioning_.part_size(i); + RC_ASSERT(empty == (size == 0)); + } + return true; + } +) TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t(2)); - + // Find a valid index where next part is non-empty size_t idx = 0; for (size_t i = 0; i + 1 < k; ++i) { @@ -57,22 +63,22 @@ TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning } } RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); - + const size_t before = vp.partitioning_.part_size(idx); const size_t next_before = vp.partitioning_.part_size(idx + 1); vp.partitioning_.grow(idx); const size_t after = vp.partitioning_.part_size(idx); const size_t next_after = vp.partitioning_.part_size(idx + 1); - + RC_ASSERT(after == before + 1); RC_ASSERT(next_after == next_before - 1); return true; }) - + TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); RC_PRE(k >= 2); - + // Find a valid index where next part is non-empty size_t idx = 0; size_t max_grow = 0; @@ -84,222 +90,259 @@ TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partiti } } RC_PRE(max_grow > size_t(0)); - + const size_t n = *rc::gen::inRange(1, max_grow + 1); const size_t before = vp.partitioning_.part_size(idx); const size_t next_before = vp.partitioning_.part_size(idx + 1); - + vp.partitioning_.grow_by(idx, n); - + const size_t after = vp.partitioning_.part_size(idx); const size_t next_after = vp.partitioning_.part_size(idx + 1); - + RC_ASSERT(after == before + n); RC_ASSERT(next_after == next_before - n); return true; }) -TEST_PROPERTY("`add_part_end` adds a new empty part at the end `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - - const size_t size_before = vp.partitioning_.part_size(idx); - vp.partitioning_.add_part_end(idx); - - RC_ASSERT(vp.partitioning_.parts_count() == k + 1); - RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); - RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); - return true; -}) +TEST_PROPERTY( + "`add_part_end` adds a new empty part at the end `part_index`", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); -TEST_PROPERTY("`add_part_begin` adds a new empty part at the begin `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - - const size_t size_before = vp.partitioning_.part_size(idx); - vp.partitioning_.add_part_begin(idx); - - RC_ASSERT(vp.partitioning_.parts_count() == k + 1); - RC_ASSERT(vp.partitioning_.is_part_empty(idx)); - RC_ASSERT(vp.partitioning_.part_size(idx + 1) == size_before); - return true; -}) + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_part_end(idx); -TEST_PROPERTY("`add_parts_end` adds `n` empty parts at the end `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - const size_t n = *rc::gen::inRange(1, 6); - - const size_t size_before = vp.partitioning_.part_size(idx); - vp.partitioning_.add_parts_end(idx, n); - - RC_ASSERT(vp.partitioning_.parts_count() == k + n); - RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); - for (size_t i = 1; i <= n; ++i) { - RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); - } - return true; -}) + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); + RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); + return true; + } +) -TEST_PROPERTY("`add_parts_begin` adds `n` empty parts at the begin `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - const size_t n = *rc::gen::inRange(1, 6); - - const size_t size_before = vp.partitioning_.part_size(idx); - vp.partitioning_.add_parts_begin(idx, n); - - RC_ASSERT(vp.partitioning_.parts_count() == k + n); - for (size_t i = 0; i < n; ++i) { - RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); - } - RC_ASSERT(vp.partitioning_.part_size(idx + n) == size_before); - return true; -}) +TEST_PROPERTY( + "`add_part_begin` adds a new empty part at the begin `part_index`", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_part_begin(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k + 1); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx + 1) == size_before); + return true; + } +) + +TEST_PROPERTY( + "`add_parts_end` adds `n` empty parts at the end `part_index`", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_parts_end(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); + for (size_t i = 1; i <= n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + return true; + } +) + +TEST_PROPERTY( + "`add_parts_begin` adds `n` empty parts at the begin `part_index`", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + const size_t size_before = vp.partitioning_.part_size(idx); + vp.partitioning_.add_parts_begin(idx, n); + + RC_ASSERT(vp.partitioning_.parts_count() == k + n); + for (size_t i = 0; i < n; ++i) { + RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); + } + RC_ASSERT(vp.partitioning_.part_size(idx + n) == size_before); + return true; + } +) TEST_PROPERTY("`remove_part` decreases the number of parts by 1", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); RC_PRE(k >= 2); - + const size_t idx = *rc::gen::inRange(1, k); vp.partitioning_.remove_part(idx); - + RC_ASSERT(vp.partitioning_.parts_count() == k - 1); return true; }) -TEST_PROPERTY("`remove_part` transfer all the elements of `part_index` to part `part_index-1`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - RC_PRE(k >= 2); - - const size_t idx = *rc::gen::inRange(1, k); - const size_t expected_size = vp.partitioning_.part_size(idx - 1) + vp.partitioning_.part_size(idx); - - vp.partitioning_.remove_part(idx); - - RC_ASSERT(vp.partitioning_.part_size(idx - 1) == expected_size); - return true; -}) +TEST_PROPERTY( + "`remove_part` transfer all the elements of `part_index` to part `part_index-1`", + [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= 2); -TEST_PROPERTY("`add_parts_end` is equivalent to calling `add_part_end` `n` times", [](vector_partitioning vp) { - auto copy1 = vp; - auto copy2 = vp; - const auto k = copy1.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - const size_t n = *rc::gen::inRange(1, 6); - - // Use add_parts_end - copy1.partitioning_.add_parts_end(idx, n); - - // Use add_part_end n times - for (size_t i = 0; i < n; ++i) { - copy2.partitioning_.add_part_end(idx); - } - - RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); - for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { - RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); - } - return true; -}) + const size_t idx = *rc::gen::inRange(1, k); + const size_t expected_size = + vp.partitioning_.part_size(idx - 1) + vp.partitioning_.part_size(idx); -TEST_PROPERTY("`add_parts_begin` is equivalent to calling `add_part_begin` `n` times", [](vector_partitioning vp) { - auto copy1 = vp; - auto copy2 = vp; - const auto k = copy1.partitioning_.parts_count(); - const size_t idx = *rc::gen::inRange(0, k); - const size_t n = *rc::gen::inRange(1, 6); - - // Use add_parts_begin - copy1.partitioning_.add_parts_begin(idx, n); - - // Use add_part_begin n times - for (size_t i = 0; i < n; ++i) { - copy2.partitioning_.add_part_begin(idx); - } - - RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); - for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { - RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); - } - return true; -}) + vp.partitioning_.remove_part(idx); -TEST_PROPERTY("forward_list partitioning covers entire data", [](forward_list_partitioning fp) { - const auto k = fp.partitioning_.parts_count(); - size_t sum = 0; - for (size_t i = 0; i < k; ++i) sum += std::distance(fp.partitioning_.part(i).first, fp.partitioning_.part(i).second); - RC_ASSERT(static_cast(sum) == std::distance(fp.data_.begin(), fp.data_.end())); - return true; -}) + RC_ASSERT(vp.partitioning_.part_size(idx - 1) == expected_size); + return true; + } +) -TEST_PROPERTY("forward_list basic ops: grow/grow_by/add/remove", [](forward_list_partitioning fp) { - const auto k0 = fp.partitioning_.parts_count(); - RC_PRE(k0 >= 1); - - // add_part_begin at a random index - const size_t idx_add = *rc::gen::inRange(0, k0); - fp.partitioning_.add_part_begin(idx_add); - RC_ASSERT(fp.partitioning_.is_part_empty(idx_add)); - - // ensure we have at least two parts to operate grow/grow_by - const auto k1 = fp.partitioning_.parts_count(); - RC_PRE(k1 >= 2); - - // pick an index with non-empty next part for grow - size_t idx_grow = 0; - bool found = false; - for (size_t i = 0; i + 1 < k1; ++i) { - auto next = fp.partitioning_.part(i + 1); - if (next.first != next.second) { idx_grow = i; found = true; break; } - } - RC_PRE(found); +TEST_PROPERTY( + "`add_parts_end` is equivalent to calling `add_part_end` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const auto k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); - // measure sizes - auto cur = fp.partitioning_.part(idx_grow); - auto nxt = fp.partitioning_.part(idx_grow + 1); - const size_t cur_before = std::distance(cur.first, cur.second); - const size_t nxt_before = std::distance(nxt.first, nxt.second); + // Use add_parts_end + copy1.partitioning_.add_parts_end(idx, n); - fp.partitioning_.grow(idx_grow); + // Use add_part_end n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_end(idx); + } - cur = fp.partitioning_.part(idx_grow); - nxt = fp.partitioning_.part(idx_grow + 1); - RC_ASSERT(std::distance(cur.first, cur.second) == cur_before + 1); - RC_ASSERT(std::distance(nxt.first, nxt.second) == nxt_before - 1); + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + return true; + } +) - // grow_by on possibly different index - size_t idx_grow_by = idx_grow; - size_t max_grow = 0; - for (size_t i = 0; i + 1 < k1; ++i) { - auto next2 = fp.partitioning_.part(i + 1); - const size_t ns = std::distance(next2.first, next2.second); - if (ns > max_grow) { max_grow = ns; idx_grow_by = i; } - } - RC_PRE(max_grow > size_t(0)); - const size_t n = *rc::gen::inRange(1, max_grow + 1); - auto cur2 = fp.partitioning_.part(idx_grow_by); - auto nxt2 = fp.partitioning_.part(idx_grow_by + 1); - const size_t cur2_before = std::distance(cur2.first, cur2.second); - const size_t nxt2_before = std::distance(nxt2.first, nxt2.second); - fp.partitioning_.grow_by(idx_grow_by, n); - cur2 = fp.partitioning_.part(idx_grow_by); - nxt2 = fp.partitioning_.part(idx_grow_by + 1); - RC_ASSERT(std::distance(cur2.first, cur2.second) == cur2_before + n); - RC_ASSERT(std::distance(nxt2.first, nxt2.second) == nxt2_before - n); - - // remove_part at valid index (>0) - const auto k2 = fp.partitioning_.parts_count(); - RC_PRE(k2 >= 2); - const size_t idx_rem = *rc::gen::inRange(1, k2); - auto left = fp.partitioning_.part(idx_rem - 1); - auto rem = fp.partitioning_.part(idx_rem); - const size_t expected = std::distance(left.first, left.second) + std::distance(rem.first, rem.second); - fp.partitioning_.remove_part(idx_rem); - auto after_left = fp.partitioning_.part(idx_rem - 1); - RC_ASSERT(std::distance(after_left.first, after_left.second) == expected); - RC_ASSERT(fp.partitioning_.parts_count() == k2 - 1); - return true; -}) +TEST_PROPERTY( + "`add_parts_begin` is equivalent to calling `add_part_begin` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const auto k = copy1.partitioning_.parts_count(); + const size_t idx = *rc::gen::inRange(0, k); + const size_t n = *rc::gen::inRange(1, 6); + + // Use add_parts_begin + copy1.partitioning_.add_parts_begin(idx, n); + + // Use add_part_begin n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.add_part_begin(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + return true; + } +) + +TEST_PROPERTY( + "forward_list partitioning covers entire data", + [](forward_list_partitioning fp) { + const auto k = fp.partitioning_.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) + sum += std::distance(fp.partitioning_.part(i).first, fp.partitioning_.part(i).second); + RC_ASSERT( + static_cast(sum) == std::distance(fp.data_.begin(), fp.data_.end()) + ); + return true; + } +) + +TEST_PROPERTY( + "forward_list basic ops: grow/grow_by/add/remove", + [](forward_list_partitioning fp) { + const auto k0 = fp.partitioning_.parts_count(); + RC_PRE(k0 >= 1); + + // add_part_begin at a random index + const size_t idx_add = *rc::gen::inRange(0, k0); + fp.partitioning_.add_part_begin(idx_add); + RC_ASSERT(fp.partitioning_.is_part_empty(idx_add)); + + // ensure we have at least two parts to operate grow/grow_by + const auto k1 = fp.partitioning_.parts_count(); + RC_PRE(k1 >= 2); + // pick an index with non-empty next part for grow + size_t idx_grow = 0; + bool found = false; + for (size_t i = 0; i + 1 < k1; ++i) { + auto next = fp.partitioning_.part(i + 1); + if (next.first != next.second) { + idx_grow = i; + found = true; + break; + } + } + RC_PRE(found); + // measure sizes + auto cur = fp.partitioning_.part(idx_grow); + auto nxt = fp.partitioning_.part(idx_grow + 1); + const size_t cur_before = std::distance(cur.first, cur.second); + const size_t nxt_before = std::distance(nxt.first, nxt.second); + + fp.partitioning_.grow(idx_grow); + + cur = fp.partitioning_.part(idx_grow); + nxt = fp.partitioning_.part(idx_grow + 1); + RC_ASSERT(std::distance(cur.first, cur.second) == cur_before + 1); + RC_ASSERT(std::distance(nxt.first, nxt.second) == nxt_before - 1); + + // grow_by on possibly different index + size_t idx_grow_by = idx_grow; + size_t max_grow = 0; + for (size_t i = 0; i + 1 < k1; ++i) { + auto next2 = fp.partitioning_.part(i + 1); + const size_t ns = std::distance(next2.first, next2.second); + if (ns > max_grow) { + max_grow = ns; + idx_grow_by = i; + } + } + RC_PRE(max_grow > size_t(0)); + const size_t n = *rc::gen::inRange(1, max_grow + 1); + auto cur2 = fp.partitioning_.part(idx_grow_by); + auto nxt2 = fp.partitioning_.part(idx_grow_by + 1); + const size_t cur2_before = std::distance(cur2.first, cur2.second); + const size_t nxt2_before = std::distance(nxt2.first, nxt2.second); + fp.partitioning_.grow_by(idx_grow_by, n); + cur2 = fp.partitioning_.part(idx_grow_by); + nxt2 = fp.partitioning_.part(idx_grow_by + 1); + RC_ASSERT(std::distance(cur2.first, cur2.second) == cur2_before + n); + RC_ASSERT(std::distance(nxt2.first, nxt2.second) == nxt2_before - n); + + // remove_part at valid index (>0) + const auto k2 = fp.partitioning_.parts_count(); + RC_PRE(k2 >= 2); + const size_t idx_rem = *rc::gen::inRange(1, k2); + auto left = fp.partitioning_.part(idx_rem - 1); + auto rem = fp.partitioning_.part(idx_rem); + const size_t expected = + std::distance(left.first, left.second) + std::distance(rem.first, rem.second); + fp.partitioning_.remove_part(idx_rem); + auto after_left = fp.partitioning_.part(idx_rem - 1); + RC_ASSERT(std::distance(after_left.first, after_left.second) == expected); + RC_ASSERT(fp.partitioning_.parts_count() == k2 - 1); + return true; + } +) From 57a3a5edd12426eab408451f11b57da31d762a91 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sun, 30 Nov 2025 19:42:10 +0200 Subject: [PATCH 07/13] Add support for `shrink` and tests for bidirectional partitionings. --- include/positionless/partitioning.hpp | 76 +++-- test/algorithms_tests.cpp | 315 +++++++++++---------- test/detail/forward_list_partitioning.hpp | 72 ----- test/detail/partitioning_generators.hpp | 51 ++++ test/detail/vector_partitioning.hpp | 33 +-- test/partitioning_tests.cpp | 329 ++++++++++++++++------ 6 files changed, 517 insertions(+), 359 deletions(-) delete mode 100644 test/detail/forward_list_partitioning.hpp create mode 100644 test/detail/partitioning_generators.hpp diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 71b5136..766154f 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -12,7 +12,8 @@ namespace positionless { /// A separation of some collection into multiple contiguous parts. /// /// A partitioning is constructed from a range defined by a pair of iterators. -/// The range must remain valid for the lifetime of the partitioning. +/// The range must remain valid for the lifetime of the partitioning and the iterators given to +/// constructor most not be invalidated. /// /// - Invariant: parts_count() >= 1 template class partitioning { @@ -30,35 +31,34 @@ template class partitioning { [[nodiscard]] size_t parts_count() const noexcept; - /// Returns the iterators delimiting the part at index `part_index`. + /// Returns the iterators delimiting the part `part_index`. /// /// - Precondition: `part_index < parts_count()` [[nodiscard]] std::pair part(size_t part_index) const noexcept; - /// Returns `true` if the part at index `part_index` is empty. + /// Returns `true` if the part `part_index` is empty. /// /// - Precondition: `part_index < parts_count()` [[nodiscard]] bool is_part_empty(size_t part_index) const noexcept; - /// Returns the size of the part at index `part_index`. + /// Returns the size of the part `part_index`. /// /// Defined only for random access collections. /// - /// - Precondition: `part_index < parts_count()` + /// Complexity: O(1) for random access iterators, O(n) otherwise. [[nodiscard]] - size_t part_size(size_t part_index) const - requires std::random_access_iterator; + size_t part_size(size_t part_index) const; - /// Increases the size of the part at index `part_index` by moving its end + /// Increases the size of the part `part_index` by moving its end /// boundary forward by one element, and decreasing the size of the next part. /// /// - Precondition: `part_index + 1 < parts_count()` /// - Precondition: !is_part_empty(part_index + 1) void grow(size_t part_index); - /// Increases the size of the part at index `part_index` by moving its end + /// Increases the size of the part `part_index` by moving its end /// boundary forward by `n` elements, and decreasing the size of the next part. /// /// - Precondition: `part_index + 1 < parts_count()` @@ -86,12 +86,29 @@ template class partitioning { /// - Precondition: `part_index < parts_count()` void add_parts_begin(size_t part_index, size_t count); - /// Removes the part at index `part_index`, growing the previous part to + /// Removes the part `part_index`, growing the previous part to /// cover its range. /// /// - Precondition: `0 < part_index < parts_count()` void remove_part(size_t part_index); + /// Decreases the size of the part `part_index` by moving its end + /// boundary back by one element, and increasing the size of the next part. + /// + /// - Precondition: `part_index + 1 < parts_count()` + /// - Precondition: !is_part_empty(part_index) + void shrink(size_t part_index) + requires std::bidirectional_iterator; + + /// Decreases the size of the part `part_index` by moving its end + /// boundary back by `n` elements, and increasing the size of the next part. + /// + /// - Precondition: `part_index + 1 < parts_count()` + /// - Precondition: size of part `part_index` >= `n` + /// - Complexity: O(n) for bidirectional iterators, O(1) for random access iterators + void shrink_by(size_t part_index, size_t n) + requires std::bidirectional_iterator; + private: /// The boundaries of each part in the partitioning. /// @@ -115,8 +132,8 @@ inline size_t partitioning::parts_count() const noexcept { } template -inline std::pair -partitioning::part(size_t part_index) const noexcept { +inline std::pair partitioning::part(size_t part_index +) const noexcept { PRECONDITION(part_index < parts_count()); return {boundaries_[part_index], boundaries_[part_index + 1]}; } @@ -129,11 +146,9 @@ inline bool partitioning::is_part_empty(size_t part_index) const noexc } template -inline size_t partitioning::part_size(size_t part_index) const - requires std::random_access_iterator -{ +inline size_t partitioning::part_size(size_t part_index) const { PRECONDITION(part_index < parts_count()); - return static_cast(boundaries_[part_index + 1] - boundaries_[part_index]); + return std::distance(boundaries_[part_index], boundaries_[part_index + 1]); } template @@ -191,4 +206,33 @@ inline void partitioning::remove_part(size_t part_index) { boundaries_.erase(boundaries_.begin() + part_index); } +template +inline void partitioning::shrink(size_t part_index) + requires std::bidirectional_iterator +{ + PRECONDITION(part_index + 1 < parts_count()); + PRECONDITION(!is_part_empty(part_index)); + boundaries_[part_index + 1]--; +} + +template +inline void partitioning::shrink_by(size_t part_index, size_t n) + requires std::bidirectional_iterator +{ + PRECONDITION(part_index + 1 < parts_count()); + + if constexpr (std::random_access_iterator) { + // For random access iterators, we can check size and advance in O(1) + auto [begin, end] = part(part_index); + PRECONDITION(static_cast(std::distance(begin, end)) >= n); + boundaries_[part_index + 1] -= n; + } else { + // For bidirectional iterators, we need to check and advance step by step + for (size_t i = 0; i < n; ++i) { + PRECONDITION(!is_part_empty(part_index)); + boundaries_[part_index + 1]--; + } + } +} + } // namespace positionless diff --git a/test/algorithms_tests.cpp b/test/algorithms_tests.cpp index e8c424d..edac05d 100644 --- a/test/algorithms_tests.cpp +++ b/test/algorithms_tests.cpp @@ -8,87 +8,93 @@ using positionless::partitioning; using positionless::swap_first; -TEST_PROPERTY("`swap_first` swaps the first elements of two parts", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 2); - - // Find all non-empty parts - std::vector non_empty_parts; - for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - if (!vp.partitioning_.is_part_empty(idx)) { - non_empty_parts.push_back(idx); +TEST_PROPERTY( + "`swap_first` swaps the first elements of two parts", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Get the original first elements + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + const auto original_first_i = *part_i.first; + const auto original_first_j = *part_j.first; + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify the swap occurred + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + if (i == j) { + // Swapping with itself should leave it unchanged + RC_ASSERT(*after_part_i.first == original_first_i); + } else { + // First elements should be swapped + RC_ASSERT(*after_part_i.first == original_first_j); + RC_ASSERT(*after_part_j.first == original_first_i); + } } - } - RC_PRE(non_empty_parts.size() >= 2); - - // Generate two distinct non-empty part indices - const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto i = non_empty_parts[i_idx]; - const auto j = non_empty_parts[j_idx]; - - // Get the original first elements - const auto part_i = vp.partitioning_.part(i); - const auto part_j = vp.partitioning_.part(j); - const auto original_first_i = *part_i.first; - const auto original_first_j = *part_j.first; - - // Perform the swap - swap_first(vp.partitioning_, i, j); - - // Verify the swap occurred - const auto after_part_i = vp.partitioning_.part(i); - const auto after_part_j = vp.partitioning_.part(j); - - if (i == j) { - // Swapping with itself should leave it unchanged - RC_ASSERT(*after_part_i.first == original_first_i); - } else { - // First elements should be swapped - RC_ASSERT(*after_part_i.first == original_first_j); - RC_ASSERT(*after_part_j.first == original_first_i); - } -}); +); -TEST_PROPERTY("`swap_first` can be used multiple times without error", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 2); - - // Find all non-empty parts - std::vector non_empty_parts; - for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - if (!vp.partitioning_.is_part_empty(idx)) { - non_empty_parts.push_back(idx); +TEST_PROPERTY( + "`swap_first` can be used multiple times without error", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate multiple swap operations + const auto num_swaps = *rc::gen::inRange(1, 20); + + for (int k = 0; k < num_swaps; ++k) { + // Pick two random non-empty parts + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // This should not throw or violate any invariants + swap_first(vp.partitioning_, i, j); + } + + // Verify the partitioning is still valid + RC_ASSERT(vp.partitioning_.parts_count() >= size_t{1}); + + // Verify parts still cover the entire data + size_t total_size = 0; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + total_size += vp.partitioning_.part_size(idx); + } + RC_ASSERT(total_size == vp.data_.size()); } - } - RC_PRE(non_empty_parts.size() >= 2); - - // Generate multiple swap operations - const auto num_swaps = *rc::gen::inRange(1, 20); - - for (int k = 0; k < num_swaps; ++k) { - // Pick two random non-empty parts - const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto i = non_empty_parts[i_idx]; - const auto j = non_empty_parts[j_idx]; - - // This should not throw or violate any invariants - swap_first(vp.partitioning_, i, j); - } - - // Verify the partitioning is still valid - RC_ASSERT(vp.partitioning_.parts_count() >= 1); - - // Verify parts still cover the entire data - size_t total_size = 0; - for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - total_size += vp.partitioning_.part_size(idx); - } - RC_ASSERT(total_size == vp.data_.size()); -}); +); TEST_PROPERTY("`swap_first` on the same part should be a no-op", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 1); - + RC_PRE(vp.partitioning_.parts_count() >= size_t{1}); + // Find a non-empty part std::vector non_empty_parts; for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { @@ -96,54 +102,57 @@ TEST_PROPERTY("`swap_first` on the same part should be a no-op", [](vector_parti non_empty_parts.push_back(idx); } } - RC_PRE(non_empty_parts.size() >= 1); - + RC_PRE(non_empty_parts.size() >= size_t{1}); + // Pick a random non-empty part const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); const auto i = non_empty_parts[i_idx]; - + // Save original data const auto original_data = vp.data_; - + // Swap part with itself swap_first(vp.partitioning_, i, i); - + // Verify data is unchanged RC_ASSERT(vp.data_ == original_data); }); -TEST_PROPERTY("`swap_first` twice returns to original state (idempotent)", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 2); - - // Find all non-empty parts - std::vector non_empty_parts; - for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - if (!vp.partitioning_.is_part_empty(idx)) { - non_empty_parts.push_back(idx); +TEST_PROPERTY( + "`swap_first` twice returns to original state (idempotent)", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two distinct non-empty part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + + // Save original data + const auto original_data = vp.data_; + + // Swap twice + swap_first(vp.partitioning_, i, j); + swap_first(vp.partitioning_, i, j); + + // Verify data is back to original state + RC_ASSERT(vp.data_ == original_data); } - } - RC_PRE(non_empty_parts.size() >= 2); - - // Generate two distinct non-empty part indices - const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto i = non_empty_parts[i_idx]; - const auto j = non_empty_parts[j_idx]; - - // Save original data - const auto original_data = vp.data_; - - // Swap twice - swap_first(vp.partitioning_, i, j); - swap_first(vp.partitioning_, i, j); - - // Verify data is back to original state - RC_ASSERT(vp.data_ == original_data); -}); +); TEST_PROPERTY("`swap_first` preserves data as a permutation", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 2); - + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + // Find all non-empty parts std::vector non_empty_parts; for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { @@ -151,59 +160,61 @@ TEST_PROPERTY("`swap_first` preserves data as a permutation", [](vector_partitio non_empty_parts.push_back(idx); } } - RC_PRE(non_empty_parts.size() >= 2); - + RC_PRE(non_empty_parts.size() >= size_t{2}); + // Generate two distinct non-empty part indices const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); const auto i = non_empty_parts[i_idx]; const auto j = non_empty_parts[j_idx]; - + // Save original data const auto original_data = vp.data_; - + // Perform the swap swap_first(vp.partitioning_, i, j); - + // Verify it's still a permutation of the original data RC_ASSERT(std::is_permutation(vp.data_.begin(), vp.data_.end(), original_data.begin())); }); -TEST_PROPERTY("`swap_first` only modifies first elements of the two parts", [](vector_partitioning vp) { - RC_PRE(vp.partitioning_.parts_count() >= 2); - - // Find all non-empty parts - std::vector non_empty_parts; - for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { - if (!vp.partitioning_.is_part_empty(idx)) { - non_empty_parts.push_back(idx); - } - } - RC_PRE(non_empty_parts.size() >= 2); - - // Generate two part indices - const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); - const auto i = non_empty_parts[i_idx]; - const auto j = non_empty_parts[j_idx]; - - // Save all elements except the first from each part (if they have more than 1 element) - const auto part_i = vp.partitioning_.part(i); - const auto part_j = vp.partitioning_.part(j); - std::vector rest_of_i(part_i.first + 1, part_i.second); - std::vector rest_of_j(part_j.first + 1, part_j.second); - - // Perform the swap - swap_first(vp.partitioning_, i, j); - - // Verify non-first elements remain unchanged - const auto after_part_i = vp.partitioning_.part(i); - const auto after_part_j = vp.partitioning_.part(j); - - std::vector new_rest_of_i(after_part_i.first + 1, after_part_i.second); - std::vector new_rest_of_j(after_part_j.first + 1, after_part_j.second); - - RC_ASSERT(new_rest_of_i == rest_of_i); - RC_ASSERT(new_rest_of_j == rest_of_j); -}); +TEST_PROPERTY( + "`swap_first` only modifies first elements of the two parts", + [](vector_partitioning vp) { + RC_PRE(vp.partitioning_.parts_count() >= size_t{2}); + + // Find all non-empty parts + std::vector non_empty_parts; + for (size_t idx = 0; idx < vp.partitioning_.parts_count(); ++idx) { + if (!vp.partitioning_.is_part_empty(idx)) { + non_empty_parts.push_back(idx); + } + } + RC_PRE(non_empty_parts.size() >= size_t{2}); + + // Generate two part indices + const auto i_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto j_idx = *rc::gen::inRange(0, non_empty_parts.size()); + const auto i = non_empty_parts[i_idx]; + const auto j = non_empty_parts[j_idx]; + // Save all elements except the first from each part (if they have more than 1 element) + const auto part_i = vp.partitioning_.part(i); + const auto part_j = vp.partitioning_.part(j); + std::vector rest_of_i(part_i.first + 1, part_i.second); + std::vector rest_of_j(part_j.first + 1, part_j.second); + + // Perform the swap + swap_first(vp.partitioning_, i, j); + + // Verify non-first elements remain unchanged + const auto after_part_i = vp.partitioning_.part(i); + const auto after_part_j = vp.partitioning_.part(j); + + std::vector new_rest_of_i(after_part_i.first + 1, after_part_i.second); + std::vector new_rest_of_j(after_part_j.first + 1, after_part_j.second); + + RC_ASSERT(new_rest_of_i == rest_of_i); + RC_ASSERT(new_rest_of_j == rest_of_j); + } +); diff --git a/test/detail/forward_list_partitioning.hpp b/test/detail/forward_list_partitioning.hpp deleted file mode 100644 index 98e36ec..0000000 --- a/test/detail/forward_list_partitioning.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "positionless/partitioning.hpp" -#include "rapidcheck_wrapper.hpp" - -#include -#include -#include - -// A `std::forward_list` with a partitioning over its elements. -template struct forward_list_partitioning { - using container_t = std::forward_list; - using iterator_t = typename container_t::iterator; - - /// The underlying data. - container_t data_; - /// The partitioning over the data. - positionless::partitioning partitioning_; - - /// An instance with `k` parts over the elements of `d`. - explicit forward_list_partitioning(container_t d, size_t k = 1) - : data_(std::move(d)), partitioning_(data_.begin(), data_.end()) { - if (k > 1) { - partitioning_.add_parts_begin(0, k - 1); - } - } -}; - -namespace rc { - -/// RapidCheck generator for forward_list_partitioning -template struct Arbitrary> { - static Gen> arbitrary() { - return gen::exec([]() { - const auto n = *gen::inRange(0, 64); - auto data = *gen::container>(n, gen::arbitrary()); - - const auto maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); - const auto k = *gen::inRange(1, maxK + 1); - - // Initially have only one part covering all data. - forward_list_partitioning r(typename forward_list_partitioning::container_t(data.begin(), data.end()), 1); - - if (k == 1) { - return r; - } - - // Generate k+1 cut points in [0, n], include 0 and n, sort. - std::vector sizes = - *gen::container>(k - 1, gen::inRange(0, n + 1)); - sizes.push_back(0); - sizes.push_back(n); - std::sort(sizes.begin(), sizes.end()); - - // Transform in-place to adjacent differences; - // erase the first element (0), and the last element (to n). - std::adjacent_difference(sizes.begin(), sizes.end(), sizes.begin()); - sizes.erase(sizes.begin()); - sizes.pop_back(); - - // Add the parts, with the generated sizes. - for (size_t part_len : sizes) { - r.partitioning_.add_part_begin(r.partitioning_.parts_count() - 1); - r.partitioning_.grow_by(r.partitioning_.parts_count() - 2, part_len); - } - - return r; - }); - } -}; - -} // namespace rc diff --git a/test/detail/partitioning_generators.hpp b/test/detail/partitioning_generators.hpp new file mode 100644 index 0000000..2764580 --- /dev/null +++ b/test/detail/partitioning_generators.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "positionless/partitioning.hpp" +#include "rapidcheck_wrapper.hpp" + +#include +#include +#include + +namespace testgen { + +/// Generates `k` parts to fill up `n` elements, and returns the sizes of the first `k-1` parts. +inline std::vector generate_partition_sizes(size_t n, size_t k) { + if (k == 0) + return {}; + if (k == 1) + return {n}; + std::vector r = + *rc::gen::container>(k - 1, rc::gen::inRange(0, n + 1)); + r.push_back(0); + r.push_back(n); + std::sort(r.begin(), r.end()); + std::adjacent_difference(r.begin(), r.end(), r.begin()); + r.erase(r.begin()); + r.pop_back(); + for (size_t s : r) { + PRECONDITION(s <= n); + } + return r; // size k-1; the last part will be inferred +} + +/// Generate an arbitrary split into parts for partitioning `p`. +/// +/// Precondition: `p.parts_count() == 1` +template auto generate_splits(positionless::partitioning& p) { + PRECONDITION(p.parts_count() == 1); + const size_t n = static_cast(std::distance(p.part(0).first, p.part(0).second)); + const size_t maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); + const size_t k = *rc::gen::inRange(1, maxK + 1); + + if (k > 1) { + auto sizes = generate_partition_sizes(n, k); + for (size_t part_len : sizes) { + p.add_part_begin(p.parts_count() - 1); + p.grow_by(p.parts_count() - 2, part_len); + } + } + PRECONDITION(p.parts_count() == k); +} + +} // namespace testgen diff --git a/test/detail/vector_partitioning.hpp b/test/detail/vector_partitioning.hpp index 2c247ee..89d595c 100644 --- a/test/detail/vector_partitioning.hpp +++ b/test/detail/vector_partitioning.hpp @@ -1,5 +1,6 @@ #pragma once +#include "partitioning_generators.hpp" #include "positionless/partitioning.hpp" #include "rapidcheck_wrapper.hpp" @@ -34,36 +35,8 @@ template struct Arbitrary> { return gen::exec([]() { const auto n = *gen::inRange(0, 64); auto data = *gen::container>(n, gen::arbitrary()); - - const auto maxK = std::max(1, n == 0 ? 4 : std::min(n, 8)); - const auto k = *gen::inRange(1, maxK + 1); - - // Initially have only one part covering all data. - vector_partitioning r(std::move(data), 1); - - if (k == 1) { - return r; - } - - // Generate k+1 cut points in [0, n], include 0 and n, sort. - std::vector sizes = - *gen::container>(k - 1, gen::inRange(0, n + 1)); - sizes.push_back(0); - sizes.push_back(n); - std::sort(sizes.begin(), sizes.end()); - - // Transform in-place to adjacent differences; - // erase the first element (0), and the last element (to n). - std::adjacent_difference(sizes.begin(), sizes.end(), sizes.begin()); - sizes.erase(sizes.begin()); - sizes.pop_back(); - - // Add the parts, with the generated sizes. - for (size_t part_len : sizes) { - r.partitioning_.add_part_begin(r.partitioning_.parts_count() - 1); - r.partitioning_.grow_by(r.partitioning_.parts_count() - 2, part_len); - } - + vector_partitioning r(std::move(data)); + testgen::generate_splits(r.partitioning_); return r; }); } diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 58328c4..4ef9986 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -1,9 +1,10 @@ #include "positionless/partitioning.hpp" -#include "detail/forward_list_partitioning.hpp" #include "detail/rapidcheck_wrapper.hpp" #include "detail/vector_partitioning.hpp" +#include +#include #include using positionless::partitioning; @@ -76,8 +77,8 @@ TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning }) TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); - RC_PRE(k >= 2); + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); // Find a valid index where next part is non-empty size_t idx = 0; @@ -105,6 +106,130 @@ TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partiti return true; }) +TEST_PROPERTY("`shrink` decreases the size of a part by 1", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t(2)); + + // Find a valid index where current part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx)); + + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + vp.partitioning_.shrink(idx); + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before - 1); + RC_ASSERT(next_after == next_before + 1); + return true; +}) + +TEST_PROPERTY("`shrink_by` decreases the size of a part by `n`", [](vector_partitioning vp) { + const auto k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where current part is non-empty + size_t idx = 0; + size_t max_shrink = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t size = vp.partitioning_.part_size(i); + if (size > max_shrink) { + idx = i; + max_shrink = size; + } + } + RC_PRE(max_shrink > size_t(0)); + + const size_t n = *rc::gen::inRange(1, max_shrink + 1); + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + + vp.partitioning_.shrink_by(idx, n); + + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before - n); + RC_ASSERT(next_after == next_before + n); + return true; +}) + +TEST_PROPERTY( + "`grow` followed by `shrink` returns to original state", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where next part is non-empty + size_t idx = 0; + for (size_t i = 0; i + 1 < k; ++i) { + if (!vp.partitioning_.is_part_empty(i + 1)) { + idx = i; + break; + } + } + RC_PRE(!vp.partitioning_.is_part_empty(idx + 1)); + + const size_t before = vp.partitioning_.part_size(idx); + const size_t next_before = vp.partitioning_.part_size(idx + 1); + + vp.partitioning_.grow(idx); + vp.partitioning_.shrink(idx); + + const size_t after = vp.partitioning_.part_size(idx); + const size_t next_after = vp.partitioning_.part_size(idx + 1); + + RC_ASSERT(after == before); + RC_ASSERT(next_after == next_before); + return true; + } +) + +TEST_PROPERTY( + "`shrink_by` is equivalent to calling `shrink` `n` times", + [](vector_partitioning vp) { + auto copy1 = vp; + auto copy2 = vp; + const size_t k = copy1.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + + // Find a valid index where current part is non-empty + size_t idx = 0; + size_t max_shrink = 0; + for (size_t i = 0; i + 1 < k; ++i) { + const size_t size = copy1.partitioning_.part_size(i); + if (size > max_shrink) { + idx = i; + max_shrink = size; + } + } + RC_PRE(max_shrink > size_t(0)); + + const size_t n = *rc::gen::inRange(1, max_shrink + 1); + + // Use shrink_by + copy1.partitioning_.shrink_by(idx, n); + + // Use shrink n times + for (size_t i = 0; i < n; ++i) { + copy2.partitioning_.shrink(idx); + } + + RC_ASSERT(copy1.partitioning_.parts_count() == copy2.partitioning_.parts_count()); + for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { + RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); + } + return true; + } +) + TEST_PROPERTY( "`add_part_end` adds a new empty part at the end `part_index`", [](vector_partitioning vp) { @@ -177,7 +302,7 @@ TEST_PROPERTY( TEST_PROPERTY("`remove_part` decreases the number of parts by 1", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); - RC_PRE(k >= 2); + RC_PRE(k >= size_t{2}); const size_t idx = *rc::gen::inRange(1, k); vp.partitioning_.remove_part(idx); @@ -190,7 +315,7 @@ TEST_PROPERTY( "`remove_part` transfer all the elements of `part_index` to part `part_index-1`", [](vector_partitioning vp) { const auto k = vp.partitioning_.parts_count(); - RC_PRE(k >= 2); + RC_PRE(k >= size_t{2}); const size_t idx = *rc::gen::inRange(1, k); const size_t expected_size = @@ -253,96 +378,122 @@ TEST_PROPERTY( } ) +TEST_PROPERTY("forward_list partitioning covers entire data", [](std::forward_list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const auto k = p.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += p.part_size(i); + } + RC_ASSERT(static_cast(sum) == std::distance(data.begin(), data.end())); +}) + TEST_PROPERTY( - "forward_list partitioning covers entire data", - [](forward_list_partitioning fp) { - const auto k = fp.partitioning_.parts_count(); - size_t sum = 0; - for (size_t i = 0; i < k; ++i) - sum += std::distance(fp.partitioning_.part(i).first, fp.partitioning_.part(i).second); - RC_ASSERT( - static_cast(sum) == std::distance(fp.data_.begin(), fp.data_.end()) - ); - return true; + "can use all operations on a forward list partitioning", + [](std::forward_list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + + // We just want to check that all operations are correctly instantiated. + + (void)p.part(i); + (void)p.is_part_empty(i); + (void)p.part_size(i); // O(N) complexity + + if (!p.is_part_empty(i + 1)) { + p.grow(i); + } + if (!p.is_part_empty(i + 1)) { + p.grow_by(i, 1); + } + p.add_part_end(i); + p.add_part_begin(i); + p.add_parts_end(i, 2); + p.add_parts_begin(i, 2); + p.remove_part(i); } ) +TEST_PROPERTY("bidirectional list partitioning covers entire data", [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + size_t sum = 0; + for (size_t i = 0; i < k; ++i) { + sum += p.part_size(i); + } + RC_ASSERT(sum == data.size()); +}) + TEST_PROPERTY( - "forward_list basic ops: grow/grow_by/add/remove", - [](forward_list_partitioning fp) { - const auto k0 = fp.partitioning_.parts_count(); - RC_PRE(k0 >= 1); - - // add_part_begin at a random index - const size_t idx_add = *rc::gen::inRange(0, k0); - fp.partitioning_.add_part_begin(idx_add); - RC_ASSERT(fp.partitioning_.is_part_empty(idx_add)); - - // ensure we have at least two parts to operate grow/grow_by - const auto k1 = fp.partitioning_.parts_count(); - RC_PRE(k1 >= 2); - - // pick an index with non-empty next part for grow - size_t idx_grow = 0; - bool found = false; - for (size_t i = 0; i + 1 < k1; ++i) { - auto next = fp.partitioning_.part(i + 1); - if (next.first != next.second) { - idx_grow = i; - found = true; - break; - } + "can use all operations on a bidirectional list partitioning", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + + // We just want to check that all operations are correctly instantiated. + + (void)p.part(i); + (void)p.is_part_empty(i); + (void)p.part_size(i); // O(N) complexity + + if (!p.is_part_empty(i + 1)) { + p.grow(i); } - RC_PRE(found); - - // measure sizes - auto cur = fp.partitioning_.part(idx_grow); - auto nxt = fp.partitioning_.part(idx_grow + 1); - const size_t cur_before = std::distance(cur.first, cur.second); - const size_t nxt_before = std::distance(nxt.first, nxt.second); - - fp.partitioning_.grow(idx_grow); - - cur = fp.partitioning_.part(idx_grow); - nxt = fp.partitioning_.part(idx_grow + 1); - RC_ASSERT(std::distance(cur.first, cur.second) == cur_before + 1); - RC_ASSERT(std::distance(nxt.first, nxt.second) == nxt_before - 1); - - // grow_by on possibly different index - size_t idx_grow_by = idx_grow; - size_t max_grow = 0; - for (size_t i = 0; i + 1 < k1; ++i) { - auto next2 = fp.partitioning_.part(i + 1); - const size_t ns = std::distance(next2.first, next2.second); - if (ns > max_grow) { - max_grow = ns; - idx_grow_by = i; - } + if (!p.is_part_empty(i + 1)) { + p.grow_by(i, 1); } - RC_PRE(max_grow > size_t(0)); - const size_t n = *rc::gen::inRange(1, max_grow + 1); - auto cur2 = fp.partitioning_.part(idx_grow_by); - auto nxt2 = fp.partitioning_.part(idx_grow_by + 1); - const size_t cur2_before = std::distance(cur2.first, cur2.second); - const size_t nxt2_before = std::distance(nxt2.first, nxt2.second); - fp.partitioning_.grow_by(idx_grow_by, n); - cur2 = fp.partitioning_.part(idx_grow_by); - nxt2 = fp.partitioning_.part(idx_grow_by + 1); - RC_ASSERT(std::distance(cur2.first, cur2.second) == cur2_before + n); - RC_ASSERT(std::distance(nxt2.first, nxt2.second) == nxt2_before - n); - - // remove_part at valid index (>0) - const auto k2 = fp.partitioning_.parts_count(); - RC_PRE(k2 >= 2); - const size_t idx_rem = *rc::gen::inRange(1, k2); - auto left = fp.partitioning_.part(idx_rem - 1); - auto rem = fp.partitioning_.part(idx_rem); - const size_t expected = - std::distance(left.first, left.second) + std::distance(rem.first, rem.second); - fp.partitioning_.remove_part(idx_rem); - auto after_left = fp.partitioning_.part(idx_rem - 1); - RC_ASSERT(std::distance(after_left.first, after_left.second) == expected); - RC_ASSERT(fp.partitioning_.parts_count() == k2 - 1); - return true; + if (!p.is_part_empty(i)) { + p.shrink(i); + } + if (!p.is_part_empty(i)) { + p.shrink_by(i, 1); + } + p.add_part_end(i); + p.add_part_begin(i); + p.add_parts_end(i, 2); + p.add_parts_begin(i, 2); + p.remove_part(i); + } +) + +TEST_PROPERTY( + "bidirectional list: growing a part increases its size by 1", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + RC_PRE(!p.is_part_empty(i + 1)); + const size_t old_size = p.part_size(i); + p.grow(i); + RC_ASSERT(p.part_size(i) == old_size + 1); + } +) + +TEST_PROPERTY( + "bidirectional list: shrinking a part decreases its size by 1", + [](std::list data) { + partitioning::iterator> p(data.begin(), data.end()); + testgen::generate_splits(p); + const size_t k = p.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t i = *rc::gen::inRange(0, k - 1); + RC_PRE(!p.is_part_empty(i)); + const size_t old_size = p.part_size(i); + p.shrink(i); + RC_ASSERT(p.part_size(i) == old_size - 1); } ) From abf94ccd29fb19b24987bbbbee8e6fa8cf45170b Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Sun, 30 Nov 2025 19:50:04 +0200 Subject: [PATCH 08/13] Update README.md --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2471dc0..b9292d7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,22 @@ We want to have an implementation of positionless algorithms and a translation f ## Positionless features -TODO +- Construction from a range (forward access, bidirectional and random access) +- `paritioning` accessors: + - `parts_count() -> size_t` + - `part(size_t part_index) -> std::pair` + - `is_part_empty(size_t part_index) -> bool` + - `part_size(size_t part_index) -> size_t` -- constant time for random access, otherwise linear +- growing parts (at the end): + - `grow` + - `grow_by` +- shrinking parts (for bidirectional collections): + - `shrink` + - `shrink_by` +- creation and destruction of parts: + - `add_part_end` / `add_part_begin` + - `add_parts_end` / `add_parts_begin` + - `remove_part` ## Translation from iterators TODO From 39c93cdfe8c3baefeebed205a96b7a5dab61fa55 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Mon, 1 Dec 2025 12:17:24 +0200 Subject: [PATCH 09/13] Minor fixes after code review. --- include/positionless/partitioning.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 766154f..12da678 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -45,8 +45,6 @@ template class partitioning { /// Returns the size of the part `part_index`. /// - /// Defined only for random access collections. - /// /// Complexity: O(1) for random access iterators, O(n) otherwise. [[nodiscard]] size_t part_size(size_t part_index) const; @@ -62,7 +60,7 @@ template class partitioning { /// boundary forward by `n` elements, and decreasing the size of the next part. /// /// - Precondition: `part_index + 1 < parts_count()` - /// - Precondition: size of part `part_index + 1` >= `n` + /// - Precondition: `part_size(part_index + 1) >= n` /// - Complexity: O(n) for forward iterators, O(1) for random access iterators void grow_by(size_t part_index, size_t n); From dd4707718d9784b8d0d231d2d62c6224afc88824 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Mon, 1 Dec 2025 12:28:18 +0200 Subject: [PATCH 10/13] Implement `transfer_to_prev` and `transfer_to_next` --- README.md | 3 + include/positionless/partitioning.hpp | 25 ++++++++ test/partitioning_tests.cpp | 86 ++++++++++++++++----------- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b9292d7..df1cc65 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ We want to have an implementation of positionless algorithms and a translation f - shrinking parts (for bidirectional collections): - `shrink` - `shrink_by` +- transferring elements between parts: + - `transfer_to_prev` + - `transfer_to_next` - creation and destruction of parts: - `add_part_end` / `add_part_begin` - `add_parts_end` / `add_parts_begin` diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 12da678..6b4c845 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -64,6 +64,16 @@ template class partitioning { /// - Complexity: O(n) for forward iterators, O(1) for random access iterators void grow_by(size_t part_index, size_t n); + /// Transfers all the elements of `part_index` to part `part_index-1`, making the former empty. + /// + /// - Precondition: `0 < part_index < parts_count()` + void transfer_to_prev(size_t part_index); + + /// Transfers all the elements of `part_index` to part `part_index+1`, making the former empty. + /// + /// - Precondition: `part_index < parts_count() - 1` + void transfer_to_next(size_t part_index); + /// Adds a new empty part at the end of part `part_index`. /// /// - Precondition: `part_index < parts_count()` @@ -174,6 +184,21 @@ inline void partitioning::grow_by(size_t part_index, size_t n) { } } +template +inline void partitioning::transfer_to_prev(size_t part_index) { + PRECONDITION(0 < part_index); + PRECONDITION(part_index < parts_count()); + // Transfer all elements to previous part by moving the boundary between them + boundaries_[part_index] = boundaries_[part_index + 1]; +} + +template +inline void partitioning::transfer_to_next(size_t part_index) { + PRECONDITION(part_index < parts_count() - 1); + // Transfer all elements to next part by moving the boundary between them + boundaries_[part_index + 1] = boundaries_[part_index]; +} + template inline void partitioning::add_part_end(size_t part_index) { PRECONDITION(part_index < parts_count()); diff --git a/test/partitioning_tests.cpp b/test/partitioning_tests.cpp index 4ef9986..2c234f0 100644 --- a/test/partitioning_tests.cpp +++ b/test/partitioning_tests.cpp @@ -10,49 +10,43 @@ using positionless::partitioning; TEST_PROPERTY("parts of a partitioning cover the entire data", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); size_t sum = 0; for (size_t i = 0; i < k; ++i) { sum += vp.partitioning_.part_size(i); } RC_ASSERT(sum == vp.data_.size()); - return true; }) TEST_PROPERTY("partitioning allows accessing all the elements", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); std::vector reconstructed; for (size_t i = 0; i < k; ++i) { const auto part = vp.partitioning_.part(i); reconstructed.insert(reconstructed.end(), part.first, part.second); } RC_ASSERT(reconstructed == vp.data_); - return true; }) TEST_PROPERTY( "partitioning part count matches vector_partitioning", - [](vector_partitioning vp) { - RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); - return true; - } + [](vector_partitioning vp) { RC_ASSERT(vp.partitioning_.parts_count() >= size_t(1)); } ) TEST_PROPERTY( "`is_part_empty` returns true for empty parts, and false otherwise", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); for (size_t i = 0; i < k; ++i) { const bool empty = vp.partitioning_.is_part_empty(i); const size_t size = vp.partitioning_.part_size(i); RC_ASSERT(empty == (size == 0)); } - return true; } ) TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t(2)); // Find a valid index where next part is non-empty @@ -73,7 +67,6 @@ TEST_PROPERTY("`grow` increases the size of a part by 1", [](vector_partitioning RC_ASSERT(after == before + 1); RC_ASSERT(next_after == next_before - 1); - return true; }) TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partitioning vp) { @@ -103,11 +96,10 @@ TEST_PROPERTY("`grow_by` increases the size of a part by `n`", [](vector_partiti RC_ASSERT(after == before + n); RC_ASSERT(next_after == next_before - n); - return true; }) TEST_PROPERTY("`shrink` decreases the size of a part by 1", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t(2)); // Find a valid index where current part is non-empty @@ -128,11 +120,10 @@ TEST_PROPERTY("`shrink` decreases the size of a part by 1", [](vector_partitioni RC_ASSERT(after == before - 1); RC_ASSERT(next_after == next_before + 1); - return true; }) TEST_PROPERTY("`shrink_by` decreases the size of a part by `n`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t{2}); // Find a valid index where current part is non-empty @@ -158,7 +149,6 @@ TEST_PROPERTY("`shrink_by` decreases the size of a part by `n`", [](vector_parti RC_ASSERT(after == before - n); RC_ASSERT(next_after == next_before + n); - return true; }) TEST_PROPERTY( @@ -188,7 +178,6 @@ TEST_PROPERTY( RC_ASSERT(after == before); RC_ASSERT(next_after == next_before); - return true; } ) @@ -226,14 +215,47 @@ TEST_PROPERTY( for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); } - return true; + } +) + +TEST_PROPERTY( + "`transfer_to_prev` transfers all elements of a part to the previous part", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t idx = *rc::gen::inRange(1, k); + + const size_t size_before = vp.partitioning_.part_size(idx); + const size_t prev_size_before = vp.partitioning_.part_size(idx - 1); + vp.partitioning_.transfer_to_prev(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx - 1) == prev_size_before + size_before); + } +) + +TEST_PROPERTY( + "`transfer_to_next` transfers all elements of a part to the next part", + [](vector_partitioning vp) { + const size_t k = vp.partitioning_.parts_count(); + RC_PRE(k >= size_t{2}); + const size_t idx = *rc::gen::inRange(0, k - 1); + + const size_t size_before = vp.partitioning_.part_size(idx); + const size_t next_size_before = vp.partitioning_.part_size(idx + 1); + vp.partitioning_.transfer_to_next(idx); + + RC_ASSERT(vp.partitioning_.parts_count() == k); + RC_ASSERT(vp.partitioning_.is_part_empty(idx)); + RC_ASSERT(vp.partitioning_.part_size(idx + 1) == next_size_before + size_before); } ) TEST_PROPERTY( "`add_part_end` adds a new empty part at the end `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t size_before = vp.partitioning_.part_size(idx); @@ -242,14 +264,13 @@ TEST_PROPERTY( RC_ASSERT(vp.partitioning_.parts_count() == k + 1); RC_ASSERT(vp.partitioning_.part_size(idx) == size_before); RC_ASSERT(vp.partitioning_.is_part_empty(idx + 1)); - return true; } ) TEST_PROPERTY( "`add_part_begin` adds a new empty part at the begin `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t size_before = vp.partitioning_.part_size(idx); @@ -258,14 +279,13 @@ TEST_PROPERTY( RC_ASSERT(vp.partitioning_.parts_count() == k + 1); RC_ASSERT(vp.partitioning_.is_part_empty(idx)); RC_ASSERT(vp.partitioning_.part_size(idx + 1) == size_before); - return true; } ) TEST_PROPERTY( "`add_parts_end` adds `n` empty parts at the end `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); @@ -277,14 +297,13 @@ TEST_PROPERTY( for (size_t i = 1; i <= n; ++i) { RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); } - return true; } ) TEST_PROPERTY( "`add_parts_begin` adds `n` empty parts at the begin `part_index`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); @@ -296,25 +315,23 @@ TEST_PROPERTY( RC_ASSERT(vp.partitioning_.is_part_empty(idx + i)); } RC_ASSERT(vp.partitioning_.part_size(idx + n) == size_before); - return true; } ) TEST_PROPERTY("`remove_part` decreases the number of parts by 1", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t{2}); const size_t idx = *rc::gen::inRange(1, k); vp.partitioning_.remove_part(idx); RC_ASSERT(vp.partitioning_.parts_count() == k - 1); - return true; }) TEST_PROPERTY( "`remove_part` transfer all the elements of `part_index` to part `part_index-1`", [](vector_partitioning vp) { - const auto k = vp.partitioning_.parts_count(); + const size_t k = vp.partitioning_.parts_count(); RC_PRE(k >= size_t{2}); const size_t idx = *rc::gen::inRange(1, k); @@ -324,7 +341,6 @@ TEST_PROPERTY( vp.partitioning_.remove_part(idx); RC_ASSERT(vp.partitioning_.part_size(idx - 1) == expected_size); - return true; } ) @@ -333,7 +349,7 @@ TEST_PROPERTY( [](vector_partitioning vp) { auto copy1 = vp; auto copy2 = vp; - const auto k = copy1.partitioning_.parts_count(); + const size_t k = copy1.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); @@ -349,7 +365,6 @@ TEST_PROPERTY( for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); } - return true; } ) @@ -358,7 +373,7 @@ TEST_PROPERTY( [](vector_partitioning vp) { auto copy1 = vp; auto copy2 = vp; - const auto k = copy1.partitioning_.parts_count(); + const size_t k = copy1.partitioning_.parts_count(); const size_t idx = *rc::gen::inRange(0, k); const size_t n = *rc::gen::inRange(1, 6); @@ -374,7 +389,6 @@ TEST_PROPERTY( for (size_t i = 0; i < copy1.partitioning_.parts_count(); ++i) { RC_ASSERT(copy1.partitioning_.part_size(i) == copy2.partitioning_.part_size(i)); } - return true; } ) @@ -382,7 +396,7 @@ TEST_PROPERTY("forward_list partitioning covers entire data", [](std::forward_li partitioning::iterator> p(data.begin(), data.end()); testgen::generate_splits(p); - const auto k = p.parts_count(); + const size_t k = p.parts_count(); size_t sum = 0; for (size_t i = 0; i < k; ++i) { sum += p.part_size(i); From 7814f752295c1ad0170cfb3b58bec6413276b860 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Tue, 2 Dec 2025 21:09:29 +0200 Subject: [PATCH 11/13] Apply suggestions from code review Co-authored-by: Naveen Seth Hanig Co-authored-by: Dave Abrahams --- include/positionless/detail/precondition.hpp | 2 ++ include/positionless/partitioning.hpp | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/positionless/detail/precondition.hpp b/include/positionless/detail/precondition.hpp index 045228f..220a9cd 100644 --- a/include/positionless/detail/precondition.hpp +++ b/include/positionless/detail/precondition.hpp @@ -2,6 +2,8 @@ #if defined(NDEBUG) +#include + /// Assert-like precondition check that calls `std::terminate` on failure. #define PRECONDITION(expr) \ do { \ diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 6b4c845..be7a7ae 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -37,11 +37,11 @@ template class partitioning { [[nodiscard]] std::pair part(size_t part_index) const noexcept; - /// Returns `true` if the part `part_index` is empty. + /// Returns `true` if the part `i` is empty. /// - /// - Precondition: `part_index < parts_count()` + /// - Precondition: `i < parts_count()` [[nodiscard]] - bool is_part_empty(size_t part_index) const noexcept; + bool is_part_empty(size_t i) const noexcept; /// Returns the size of the part `part_index`. /// From 68df0e724d685397dc10fb39cf459960fbbf0da4 Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Tue, 2 Dec 2025 21:15:42 +0200 Subject: [PATCH 12/13] Rename `part_index` parameter to `i`. --- include/positionless/algorithms.hpp | 4 +- include/positionless/partitioning.hpp | 192 +++++++++++++------------- 2 files changed, 97 insertions(+), 99 deletions(-) diff --git a/include/positionless/algorithms.hpp b/include/positionless/algorithms.hpp index 0480819..3e546b8 100644 --- a/include/positionless/algorithms.hpp +++ b/include/positionless/algorithms.hpp @@ -10,8 +10,8 @@ namespace positionless { /// Swaps the first element from part `i` with the first element from part `j`. /// -/// - Precondition: `i < parts_count()` -/// - Precondition: `j < parts_count()` +/// - Precondition: `i < p.parts_count()` +/// - Precondition: `j < p.parts_count()` /// - Precondition: parts `i` and `j` are not empty template inline void swap_first(partitioning& p, size_t i, size_t j) { diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index be7a7ae..50cd786 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -31,11 +31,11 @@ template class partitioning { [[nodiscard]] size_t parts_count() const noexcept; - /// Returns the iterators delimiting the part `part_index`. + /// Returns the iterators delimiting the part `i`. /// - /// - Precondition: `part_index < parts_count()` + /// - Precondition: `i < parts_count()` [[nodiscard]] - std::pair part(size_t part_index) const noexcept; + std::pair part(size_t i) const noexcept; /// Returns `true` if the part `i` is empty. /// @@ -43,78 +43,78 @@ template class partitioning { [[nodiscard]] bool is_part_empty(size_t i) const noexcept; - /// Returns the size of the part `part_index`. + /// Returns the size of the part `i`. /// /// Complexity: O(1) for random access iterators, O(n) otherwise. [[nodiscard]] - size_t part_size(size_t part_index) const; + size_t part_size(size_t i) const; - /// Increases the size of the part `part_index` by moving its end + /// Increases the size of the part `i` by moving its end /// boundary forward by one element, and decreasing the size of the next part. /// - /// - Precondition: `part_index + 1 < parts_count()` - /// - Precondition: !is_part_empty(part_index + 1) - void grow(size_t part_index); + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: !is_part_empty(i + 1) + void grow(size_t i); - /// Increases the size of the part `part_index` by moving its end + /// Increases the size of the part `i` by moving its end /// boundary forward by `n` elements, and decreasing the size of the next part. /// - /// - Precondition: `part_index + 1 < parts_count()` - /// - Precondition: `part_size(part_index + 1) >= n` + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: `part_size(i + 1) >= n` /// - Complexity: O(n) for forward iterators, O(1) for random access iterators - void grow_by(size_t part_index, size_t n); + void grow_by(size_t i, size_t n); - /// Transfers all the elements of `part_index` to part `part_index-1`, making the former empty. + /// Transfers all the elements of `i` to part `i-1`, making the former empty. /// - /// - Precondition: `0 < part_index < parts_count()` - void transfer_to_prev(size_t part_index); + /// - Precondition: `0 < i < parts_count()` + void transfer_to_prev(size_t i); - /// Transfers all the elements of `part_index` to part `part_index+1`, making the former empty. + /// Transfers all the elements of `i` to part `i+1`, making the former empty. /// - /// - Precondition: `part_index < parts_count() - 1` - void transfer_to_next(size_t part_index); + /// - Precondition: `i < parts_count() - 1` + void transfer_to_next(size_t i); - /// Adds a new empty part at the end of part `part_index`. + /// Adds a new empty part at the end of part `i`. /// - /// - Precondition: `part_index < parts_count()` - void add_part_end(size_t part_index); + /// - Precondition: `i < parts_count()` + void add_part_end(size_t i); - /// Adds a new empty part at the begin of part `part_index`. + /// Adds a new empty part at the begin of part `i`. /// - /// - Precondition: `part_index < parts_count()` - void add_part_begin(size_t part_index); + /// - Precondition: `i < parts_count()` + void add_part_begin(size_t i); - /// Adds `count` new empty parts at the end of part `part_index`. + /// Adds `count` new empty parts at the end of part `i`. /// - /// - Precondition: `part_index < parts_count()` - void add_parts_end(size_t part_index, size_t count); + /// - Precondition: `i < parts_count()` + void add_parts_end(size_t i, size_t count); - /// Adds `count` new empty parts at the begin of part `part_index`. + /// Adds `count` new empty parts at the begin of part `i`. /// - /// - Precondition: `part_index < parts_count()` - void add_parts_begin(size_t part_index, size_t count); + /// - Precondition: `i < parts_count()` + void add_parts_begin(size_t i, size_t count); - /// Removes the part `part_index`, growing the previous part to + /// Removes the part `i`, growing the previous part to /// cover its range. /// - /// - Precondition: `0 < part_index < parts_count()` - void remove_part(size_t part_index); + /// - Precondition: `0 < i < parts_count()` + void remove_part(size_t i); - /// Decreases the size of the part `part_index` by moving its end + /// Decreases the size of the part `i` by moving its end /// boundary back by one element, and increasing the size of the next part. /// - /// - Precondition: `part_index + 1 < parts_count()` - /// - Precondition: !is_part_empty(part_index) - void shrink(size_t part_index) + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: !is_part_empty(i) + void shrink(size_t i) requires std::bidirectional_iterator; - /// Decreases the size of the part `part_index` by moving its end + /// Decreases the size of the part `i` by moving its end /// boundary back by `n` elements, and increasing the size of the next part. /// - /// - Precondition: `part_index + 1 < parts_count()` - /// - Precondition: size of part `part_index` >= `n` + /// - Precondition: `i + 1 < parts_count()` + /// - Precondition: size of part `i` >= `n` /// - Complexity: O(n) for bidirectional iterators, O(1) for random access iterators - void shrink_by(size_t part_index, size_t n) + void shrink_by(size_t i, size_t n) requires std::bidirectional_iterator; private: @@ -140,120 +140,118 @@ inline size_t partitioning::parts_count() const noexcept { } template -inline std::pair partitioning::part(size_t part_index -) const noexcept { - PRECONDITION(part_index < parts_count()); - return {boundaries_[part_index], boundaries_[part_index + 1]}; +inline std::pair partitioning::part(size_t i) const noexcept { + PRECONDITION(i < parts_count()); + return {boundaries_[i], boundaries_[i + 1]}; } template -inline bool partitioning::is_part_empty(size_t part_index) const noexcept { - PRECONDITION(part_index < parts_count()); - auto [begin, end] = part(part_index); +inline bool partitioning::is_part_empty(size_t i) const noexcept { + PRECONDITION(i < parts_count()); + auto [begin, end] = part(i); return begin == end; } template -inline size_t partitioning::part_size(size_t part_index) const { - PRECONDITION(part_index < parts_count()); - return std::distance(boundaries_[part_index], boundaries_[part_index + 1]); +inline size_t partitioning::part_size(size_t i) const { + PRECONDITION(i < parts_count()); + return std::distance(boundaries_[i], boundaries_[i + 1]); } -template -inline void partitioning::grow(size_t part_index) { - PRECONDITION(part_index + 1 < parts_count()); - PRECONDITION(!is_part_empty(part_index + 1)); - boundaries_[part_index + 1]++; +template inline void partitioning::grow(size_t i) { + PRECONDITION(i + 1 < parts_count()); + PRECONDITION(!is_part_empty(i + 1)); + boundaries_[i + 1]++; } template -inline void partitioning::grow_by(size_t part_index, size_t n) { - PRECONDITION(part_index + 1 < parts_count()); +inline void partitioning::grow_by(size_t i, size_t n) { + PRECONDITION(i + 1 < parts_count()); if constexpr (std::random_access_iterator) { // For random access iterators, we can check size and advance in O(1) - auto [begin, end] = part(part_index + 1); + auto [begin, end] = part(i + 1); PRECONDITION(static_cast(std::distance(begin, end)) >= n); - boundaries_[part_index + 1] += n; + boundaries_[i + 1] += n; } else { // For forward iterators, we need to check and advance step by step - for (size_t i = 0; i < n; ++i) { - PRECONDITION(!is_part_empty(part_index + 1)); - boundaries_[part_index + 1]++; + for (size_t k = 0; k < n; ++k) { + PRECONDITION(!is_part_empty(i + 1)); + boundaries_[i + 1]++; } } } template -inline void partitioning::transfer_to_prev(size_t part_index) { - PRECONDITION(0 < part_index); - PRECONDITION(part_index < parts_count()); +inline void partitioning::transfer_to_prev(size_t i) { + PRECONDITION(0 < i); + PRECONDITION(i < parts_count()); // Transfer all elements to previous part by moving the boundary between them - boundaries_[part_index] = boundaries_[part_index + 1]; + boundaries_[i] = boundaries_[i + 1]; } template -inline void partitioning::transfer_to_next(size_t part_index) { - PRECONDITION(part_index < parts_count() - 1); +inline void partitioning::transfer_to_next(size_t i) { + PRECONDITION(i < parts_count() - 1); // Transfer all elements to next part by moving the boundary between them - boundaries_[part_index + 1] = boundaries_[part_index]; + boundaries_[i + 1] = boundaries_[i]; } template -inline void partitioning::add_part_end(size_t part_index) { - PRECONDITION(part_index < parts_count()); - boundaries_.insert(boundaries_.begin() + part_index + 1, boundaries_[part_index + 1]); +inline void partitioning::add_part_end(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i + 1, boundaries_[i + 1]); } template -inline void partitioning::add_part_begin(size_t part_index) { - PRECONDITION(part_index < parts_count()); - boundaries_.insert(boundaries_.begin() + part_index, boundaries_[part_index]); +inline void partitioning::add_part_begin(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i, boundaries_[i]); } template -inline void partitioning::add_parts_end(size_t part_index, size_t count) { - PRECONDITION(part_index < parts_count()); - boundaries_.insert(boundaries_.begin() + part_index + 1, count, boundaries_[part_index + 1]); +inline void partitioning::add_parts_end(size_t i, size_t count) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i + 1, count, boundaries_[i + 1]); } template -inline void partitioning::add_parts_begin(size_t part_index, size_t count) { - PRECONDITION(part_index < parts_count()); - boundaries_.insert(boundaries_.begin() + part_index, count, boundaries_[part_index]); +inline void partitioning::add_parts_begin(size_t i, size_t count) { + PRECONDITION(i < parts_count()); + boundaries_.insert(boundaries_.begin() + i, count, boundaries_[i]); } template -inline void partitioning::remove_part(size_t part_index) { - PRECONDITION(part_index < parts_count()); - boundaries_.erase(boundaries_.begin() + part_index); +inline void partitioning::remove_part(size_t i) { + PRECONDITION(i < parts_count()); + boundaries_.erase(boundaries_.begin() + i); } template -inline void partitioning::shrink(size_t part_index) +inline void partitioning::shrink(size_t i) requires std::bidirectional_iterator { - PRECONDITION(part_index + 1 < parts_count()); - PRECONDITION(!is_part_empty(part_index)); - boundaries_[part_index + 1]--; + PRECONDITION(i + 1 < parts_count()); + PRECONDITION(!is_part_empty(i)); + boundaries_[i + 1]--; } template -inline void partitioning::shrink_by(size_t part_index, size_t n) +inline void partitioning::shrink_by(size_t i, size_t n) requires std::bidirectional_iterator { - PRECONDITION(part_index + 1 < parts_count()); + PRECONDITION(i + 1 < parts_count()); if constexpr (std::random_access_iterator) { // For random access iterators, we can check size and advance in O(1) - auto [begin, end] = part(part_index); + auto [begin, end] = part(i); PRECONDITION(static_cast(std::distance(begin, end)) >= n); - boundaries_[part_index + 1] -= n; + boundaries_[i + 1] -= n; } else { // For bidirectional iterators, we need to check and advance step by step - for (size_t i = 0; i < n; ++i) { - PRECONDITION(!is_part_empty(part_index)); - boundaries_[part_index + 1]--; + for (size_t k = 0; k < n; ++k) { + PRECONDITION(!is_part_empty(i)); + boundaries_[i + 1]--; } } } From 738d1dfcffa0edb6130c4d8f0a46b0322a7c822e Mon Sep 17 00:00:00 2001 From: Lucian Radu Teodorescu Date: Tue, 2 Dec 2025 21:22:37 +0200 Subject: [PATCH 13/13] Improve documentation. --- include/positionless/partitioning.hpp | 45 ++++++++++----------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/include/positionless/partitioning.hpp b/include/positionless/partitioning.hpp index 50cd786..25e9a73 100644 --- a/include/positionless/partitioning.hpp +++ b/include/positionless/partitioning.hpp @@ -26,37 +26,33 @@ template class partitioning { constexpr partitioning(Iterator begin, Iterator end); /// Returns the number of parts in the partitioning. - /// - /// - Invariant: `parts_count() >= 1` [[nodiscard]] size_t parts_count() const noexcept; - /// Returns the iterators delimiting the part `i`. + /// Returns the iterators delimiting the `i`th part. /// /// - Precondition: `i < parts_count()` [[nodiscard]] std::pair part(size_t i) const noexcept; - /// Returns `true` if the part `i` is empty. - /// - /// - Precondition: `i < parts_count()` + /// Returns `true` if the `i`th part is empty. [[nodiscard]] bool is_part_empty(size_t i) const noexcept; - /// Returns the size of the part `i`. + /// Returns the size of the `i`th part. /// /// Complexity: O(1) for random access iterators, O(n) otherwise. [[nodiscard]] size_t part_size(size_t i) const; - /// Increases the size of the part `i` by moving its end + /// Increases the size of the `i`th part by moving its end /// boundary forward by one element, and decreasing the size of the next part. /// /// - Precondition: `i + 1 < parts_count()` /// - Precondition: !is_part_empty(i + 1) void grow(size_t i); - /// Increases the size of the part `i` by moving its end + /// Increases the size of the `i`th part by moving its end /// boundary forward by `n` elements, and decreasing the size of the next part. /// /// - Precondition: `i + 1 < parts_count()` @@ -64,55 +60,46 @@ template class partitioning { /// - Complexity: O(n) for forward iterators, O(1) for random access iterators void grow_by(size_t i, size_t n); - /// Transfers all the elements of `i` to part `i-1`, making the former empty. + /// Transfers all the elements of `i`th part to `i-1`th part, making the former empty. /// /// - Precondition: `0 < i < parts_count()` void transfer_to_prev(size_t i); - /// Transfers all the elements of `i` to part `i+1`, making the former empty. + /// Transfers all the elements of `i`th part to `i+1`th part, making the former empty. /// /// - Precondition: `i < parts_count() - 1` void transfer_to_next(size_t i); - /// Adds a new empty part at the end of part `i`. - /// - /// - Precondition: `i < parts_count()` + /// Adds a new empty part at the end of the `i`th part. void add_part_end(size_t i); - /// Adds a new empty part at the begin of part `i`. - /// - /// - Precondition: `i < parts_count()` + /// Adds a new empty part at the beginning of the `i`th part. void add_part_begin(size_t i); - /// Adds `count` new empty parts at the end of part `i`. - /// - /// - Precondition: `i < parts_count()` + /// Adds `count` new empty parts at the end of the `i`th part. void add_parts_end(size_t i, size_t count); - /// Adds `count` new empty parts at the begin of part `i`. - /// - /// - Precondition: `i < parts_count()` + /// Adds `count` new empty parts at the beginning of the `i`th part. void add_parts_begin(size_t i, size_t count); - /// Removes the part `i`, growing the previous part to - /// cover its range. + /// Removes the `i`th part, growing the previous part to cover its range. /// /// - Precondition: `0 < i < parts_count()` void remove_part(size_t i); - /// Decreases the size of the part `i` by moving its end - /// boundary back by one element, and increasing the size of the next part. + /// Decreases the size of the `i`th part by moving its end boundary back by one element, and + /// increasing the size of the next part. /// /// - Precondition: `i + 1 < parts_count()` /// - Precondition: !is_part_empty(i) void shrink(size_t i) requires std::bidirectional_iterator; - /// Decreases the size of the part `i` by moving its end + /// Decreases the size of the `i`th part by moving its end /// boundary back by `n` elements, and increasing the size of the next part. /// /// - Precondition: `i + 1 < parts_count()` - /// - Precondition: size of part `i` >= `n` + /// - Precondition: `part_size(i) >= n` /// - Complexity: O(n) for bidirectional iterators, O(1) for random access iterators void shrink_by(size_t i, size_t n) requires std::bidirectional_iterator;