diff --git a/README.md b/README.md index dd55604e..96878d35 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ evaluation wherever possible. [accumulate](#accumulate)
[compress](#compress)
[sorted](#sorted)
+[shuffled](#shuffled)
[chain](#chain)
[chain.from\_iterable](#chainfrom_iterable)
[reversed](#reversed)
@@ -570,6 +571,25 @@ The below outputs `0 1 2 3 4`. ```c++ unordered_set nums{4, 0, 2, 1, 3}; for (auto&& i : sorted(nums)) { + cout << i << ' '; +} +``` + +shuffled +-------- +*Additional Requirements*: Input must have a ForwardIterator. + +Allows iteration over a sequence in shuffled order. `shuffled` does +**not** produce a new sequence, copy elements, or modify the original +sequence. It only provides a way to iterate over existing elements. +`shuffled` also takes an optional second argument - randomization seed. +If not provided, defaults to 1. + +The below outputs `0 2 4 3 1`. + +```c++ +unordered_set nums{4, 0, 2, 1, 3}; +for (auto&& i : shuffled(nums)) { cout << i << '\n'; } ``` diff --git a/shuffled.hpp b/shuffled.hpp new file mode 100644 index 00000000..b643041b --- /dev/null +++ b/shuffled.hpp @@ -0,0 +1,175 @@ +#ifndef ITER_SHUFFLED_HPP_ +#define ITER_SHUFFLED_HPP_ + +#include "internal/iterbase.hpp" +#include +#include +#include + +/* Suppose the size of the container is N. We can find the power of two, + * that is greater or equal to N - K. Numbers from 1 to K can be easy + * shuffled with help of Linear Feedback Shift Register (LFSR) using + * special prime poly. Access to every element of the shuffled is + * implemented through advance of the first (begin) iterator of the + * container, so random access iterator is desirable. In the constructor + * we have to calculate the total size of the container, so + * std::distance will be used. User can define more effective + * std::distance for the container.*/ + +namespace iter { + namespace impl { + template + class ShuffledView; + // linear feedback shift register + namespace lfsr { + constexpr std::array PRIME_POLY = { + {1, 3, 3, 3, 5, 3, 3, 29, 17, 9, 5, 83, 27, 43, 3, 45, 9, 129, 39, 9, + 5, 3, 33, 27, 9, 71, 39, 9, 5, 83, 9, 197, 8193, 231, 5, 119, 83, + 99, 17, 57, 9, 153, 89, 101, 27, 449, 33, 183, 113, 29, 75, 9, 71, + 125, 71, 149, 129, 99, 123, 3, 39, 105, 3, + 27}}; // prime poly for full-cycle shift registers + constexpr std::uint16_t get_approx(std::uint64_t val); + constexpr std::uint64_t shift(std::uint64_t reg, std::uint8_t reg_size); + } + } + + template + impl::ShuffledView shuffled(Container&&, int seed = 1); +} + +// power of 2 approximation (val < pow(2, get_approx(val)+1)) +constexpr inline std::uint16_t iter::impl::lfsr::get_approx(std::uint64_t val) { + if (val == 0) { + return 0; + } + constexpr auto LIMIT = std::uint64_t(1UL) << 63; + if (val > LIMIT) { + return 63; + } + std::uint16_t pow2_approx = 0; + while (pow2_approx < 64 && (val >= (1UL << (pow2_approx + 1)))) { + ++pow2_approx; + } + return pow2_approx; +} + +constexpr inline std::uint64_t iter::impl::lfsr::shift( + std::uint64_t reg, std::uint8_t reg_size) { + if (reg & 1) { + reg = ((reg ^ PRIME_POLY[reg_size]) >> 1) | (1 << reg_size); + } else { + reg >>= 1; + } + return reg; +} + +template +class iter::impl::ShuffledView { + private: + Container container; + Distance size; + std::uint8_t size_approx; + std::uint64_t seed; + + friend ShuffledView iter::shuffled(Container&&, int); + + using IterDeref = iterator_traits_deref; + ShuffledView(Container&& in_container, int in_seed) + : container(std::forward(in_container)), + size(std::distance(std::begin(container), std::end(container))), + size_approx(lfsr::get_approx(size)), + seed(in_seed) { + if (size > 0) { + std::uint64_t mask = -1; + mask = (mask >> (64 - size_approx)); + this->seed = seed & mask; + this->seed = lfsr::shift(this->seed, size_approx); + while (this->seed >= size) { + this->seed = lfsr::shift(this->seed, size_approx); + } + } + } + + public: + ShuffledView(ShuffledView&&) = default; + + class Iterator : public std::iterator { + private: + friend class ShuffledView; + ShuffledView* owner{}; + std::uint64_t state{}; + + public: + Iterator() = default; + Iterator(ShuffledView* in_owner, std::uint64_t in_state) + : owner{in_owner}, state{in_state} {} + + Iterator& operator++() { + if (*this == owner->end()) { + return *this; + } + do { + state = lfsr::shift(state, owner->size_approx); + } while (state > owner->size); + if (state == owner->seed) { + *this = owner->end(); + } + return *this; + } + + Iterator operator++(int) { + auto ret = *this; + ++*this; + return ret; + } + + bool operator==(const Iterator& other) const { + return owner == other.owner && state == other.state; + } + + bool operator!=(const Iterator& other) const { + return !(*this == other); + } + + iterator_deref operator*() { + return *std::next(std::begin(owner->container), state - 1); + } + + ArrowProxy operator->() { + return {**this}; + } + }; + + Iterator begin() { + return {this, (size == 0 ? 0 : seed)}; + } + + Iterator end() { + return {this, 0}; + } + + /* @brief restore iteration through a vector in shuffled order from the + * index of the last seen element in non shuffled container + * + * @example: + * v = {"apple", "banana", "orange", "peach"}; // original vector + * s = shuffled(v); // {"orange", "peach", "banana", "apple"} - shuffled + * vector + * + * We iterated through s. Last seen element was "banana". In the + * original vector "banana" had index 1. So to restore shuffling wee + * need to do s.restore(1); Operation is zero cost - no iterators + * advance is needed.*/ + Iterator restore(std::uint64_t state) { + return {this, (state >= size ? seed : state + 1)}; + } +}; + + +template +iter::impl::ShuffledView iter::shuffled( + Container&& container, int seed) { + return {std::forward(container), seed}; +} + +#endif diff --git a/test/SConstruct b/test/SConstruct index cbf3cd3c..9809b263 100644 --- a/test/SConstruct +++ b/test/SConstruct @@ -37,6 +37,7 @@ progs = Split( sliding_window starmap sorted + shuffled takewhile unique_everseen unique_justseen diff --git a/test/test_shuffled.cpp b/test/test_shuffled.cpp new file mode 100644 index 00000000..e07c6a46 --- /dev/null +++ b/test/test_shuffled.cpp @@ -0,0 +1,224 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "helpers.hpp" +#include "catch.hpp" + +using iter::shuffled; + +using Vec = const std::vector; + +TEST_CASE("shuffled: iterates through a vector in shuffled order", "[shuffled]") { + Vec ns = {4, 0, 5, 1, 6, 7, 9, 3, 2, 8}; + auto s = shuffled(ns); + Vec v(std::begin(s), std::end(s)); + Vec vc = {2, 9, 8, 6, 7, 5, 3, 1, 0, 4}; + REQUIRE(v == vc); +} + +TEST_CASE("shuffled: iterates through a list of strings in shuffled order", + "[shuffled]") { + std::list ns = {"apple", "banana", "orange", "peach"}; + auto s = shuffled(ns); + std::list v(std::begin(s), std::end(s)); + std::list vc = {"orange", "peach", "banana", "apple"}; + REQUIRE(v == vc); +} + +TEST_CASE("shuffled: restore iteration through a vector in shuffled order " + "from the index of the last seen element in non shuffled container", + "[shuffled]") { + //index: 0 1 2 3 4 5 6 7 8 9 + Vec ns = {4, 0, 5, 1, 6, 7, 9, 3, 2, 8}; + auto s = shuffled(ns); + + // overflow - same as default + Vec v10(s.restore(10), std::end(s)); + Vec vc10 = {2, 9, 8, 6, 7, 5, 3, 1, 0, 4}; + REQUIRE(v10 == vc10); + + Vec v1(s.restore(6), std::end(s)); + Vec vc1 = {9, 8, 6, 7, 5, 3, 1, 0, 4}; + REQUIRE(v1 == vc1); + + Vec v2(s.restore(9), std::end(s)); + Vec vc2 = {8, 6, 7, 5, 3, 1, 0, 4}; + REQUIRE(v2 == vc2); + + Vec v3(s.restore(4), std::end(s)); + Vec vc3 = {6, 7, 5, 3, 1, 0, 4}; + REQUIRE(v3 == vc3); + + Vec v4(s.restore(5), std::end(s)); + Vec vc4 = {7, 5, 3, 1, 0, 4}; + REQUIRE(v4 == vc4); + + Vec v5(s.restore(2), std::end(s)); + Vec vc5 = {5, 3, 1, 0, 4}; + REQUIRE(v5 == vc5); + + Vec v6(s.restore(7), std::end(s)); + Vec vc6 = {3, 1, 0, 4}; + REQUIRE(v6 == vc6); + + Vec v7(s.restore(3), std::end(s)); + Vec vc7 = {1, 0, 4}; + REQUIRE(v7 == vc7); + + Vec v8(s.restore(1), std::end(s)); + Vec vc8 = {0, 4}; + REQUIRE(v8 == vc8); + + Vec v9(s.restore(0), std::end(s)); + Vec vc9 = {4}; + REQUIRE(v9 == vc9); +} + +TEST_CASE("shuffled: restore iteration through a vector in shuffled order " + "from the index of the last seen element in non shuffled " + "container of strings", "[shuffled]") { + std::list ns = {"apple", "banana", "orange", "peach"}; + auto s = shuffled(ns); + std::list v(s.restore(1), std::end(s)); + std::list vc = {"banana", "apple"}; + REQUIRE(v == vc); +} + +TEST_CASE("shuffled: iterates through a vector in shuffled order with non" + " standard seed", "[shuffled]") { + Vec ns = {4, 0, 5, 1, 6, 7, 9, 3, 2, 8}; + auto s = shuffled(ns, 1234); + Vec v(std::begin(s), std::end(s)); + Vec vc = {4, 2, 9, 8, 6, 7, 5, 3, 1, 0}; + REQUIRE(v == vc); +} + +TEST_CASE("shuffled: can modify elements through shuffled", "[shuffled]") { + std::vector ns(3, 9); + for (auto&& n : shuffled(ns)) { + n = -1; + } + Vec vc(3, -1); + REQUIRE(ns == vc); +} + +TEST_CASE("shuffled: can iterate over unordered container", "[shuffled]") { + std::unordered_set ns = {1, 3, 2, 0, 4}; + auto s = shuffled(ns); + + Vec v(std::begin(s), std::end(s)); + Vec vc = {3, 2, 1, 4, 0}; + REQUIRE(v == vc); +} + +TEST_CASE("shuffled: empty when iterable is empty", "[shuffled]") { + Vec ns{}; + auto s = shuffled(ns); + REQUIRE(std::begin(s) == std::end(s)); +} + +namespace { + template + class BasicIterableWithConstDeref { + private: + T* data; + std::size_t size; + bool was_moved_from_ = false; + + public: + BasicIterableWithConstDeref(std::initializer_list il) + : data{new T[il.size()]}, size{il.size()} { + // would like to use enumerate, can't because it's for unit + // testing enumerate + std::size_t i = 0; + for (auto&& e : il) { + data[i] = e; + ++i; + } + } + + BasicIterableWithConstDeref& operator=( + BasicIterableWithConstDeref&&) = delete; + BasicIterableWithConstDeref& operator=( + const BasicIterableWithConstDeref&) = delete; + BasicIterableWithConstDeref(const BasicIterableWithConstDeref&) = delete; + + BasicIterableWithConstDeref(BasicIterableWithConstDeref&& other) + : data{other.data}, size{other.size} { + other.data = nullptr; + other.was_moved_from_ = true; + } + + bool was_moved_from() const { + return this->was_moved_from_; + } + + ~BasicIterableWithConstDeref() { + delete[] this->data; + } + + class Iterator + : public std::iterator { + private: + T* p; + + public: + Iterator(T* b) : p{b} {} + bool operator!=(const Iterator& other) const { + return this->p != other.p; + } + + Iterator& operator++() { + ++this->p; + return *this; + } + + T& operator*() { + return *this->p; + } + + const T& operator*() const { + return *this->p; + } + }; + + Iterator begin() { + return {this->data}; + } + + Iterator end() { + return {this->data + this->size}; + } + }; +} + +TEST_CASE("shuffled: moves rvalues and binds to lvalues", "[shuffled]") { + BasicIterableWithConstDeref bi{1, 2}; + shuffled(bi); + REQUIRE_FALSE(bi.was_moved_from()); + + shuffled(std::move(bi)); + REQUIRE(bi.was_moved_from()); +} + +TEST_CASE("shuffled: doesn't move or copy elements of iterable", "[shuffled]") { + using itertest::SolidInt; + constexpr SolidInt arr[] = {{6}, {7}, {8}}; + for (auto &&i : shuffled(arr)) + (void)i; +} + +template +using ImpT = decltype(shuffled(std::declval())); +TEST_CASE("shuffled: has correct ctor and assign ops", "[shuffled]") { + REQUIRE(itertest::IsMoveConstructibleOnly>::value); + REQUIRE(itertest::IsMoveConstructibleOnly>::value); +} +