diff --git a/README.md b/README.md index 8ec973d..2bdf9c9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,36 @@ The primary focus of this library is * encapsulation of the iterator madness * removal of manual for-loops +## Contents +* [Compilation (Cmake)](#compilation-cmake) + * [Dependencies](#dependencies) + * [Minimum C++ version](#minimum-c-version) + * [macOS (Xcode)](#macos-xcode) + * [macOS (Makefiles/clang)](#macos-makefilesclang) + * [macOS (Makefiles/g++)](#macos-makefilesg) + * [Linux (Makefiles)](#linux-makefiles) + * [Windows (Visual Studio)](#windows-visual-studio) +* [Functional vector usage (fcpp::vector)](#functional-vector-usage-fcppvector) + * [extract unique (distinct) elements in a set](#extract-unique-distinct-elements-in-a-set) + * [zip, map, filter, sort, reduce](#zip-map-filter-sort-reduce) + * [lazy operations](#lazy-operations) + * [index search](#index-search) + * [remove, insert](#remove-insert) + * [size, capacity, reserve, resize](#size-capacity-reserve-resize) + * [all_of, any_of, none_of](#all_of-any_of-none_of) + * [Parallel algorithms](#parallel-algorithms) +* [Functional set usage (fcpp::set)](#functional-set-usage-fcppset) + * [difference, union, intersection](#difference-union-intersection-works-with-fcppset-and-stdset) + * [zip, map, filter, reduce](#zip-map-filter-reduce) + * [lazy operations](#lazy-operations-1) + * [all_of, any_of, none_of](#all_of-any_of-none_of-1) + * [remove, insert, contains, size, clear](#remove-insert-contains-size-clear) +* [Functional map usage (fcpp::map)](#functional-map-usage-fcppmap) + * [map_to, filter, reduce, for_each](#map_to-filter-reduce-for_each) + * [lazy operations](#lazy-operations-2) + * [all_of, any_of, none_of](#all_of-any_of-none_of-2) + * [keys, values, remove, insert](#keys-values-remove-insert) + ## Compilation (Cmake) ### Dependencies * CMake >= 3.14 @@ -135,6 +165,93 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c return partial_sum + p.age; }); ``` + +### lazy operations +Lazy vectors are useful when chaining multiple operations over a large vector. A regular `map().filter().reduce()` style chain creates intermediate vectors and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process elements in one pass. Sorting is an important exception: it cannot be streamed element by element, so lazy `sort`, `sort_ascending`, and `sort_descending` first collect the current lazy pipeline's values, sort that collected vector, and then continue feeding the rest of the lazy chain. + +```c++ +#include "vector.h" // instead of + +const fcpp::vector numbers({5, 1, 4, 2, 3}); + +const auto processed_numbers = numbers + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 2; + }) + + // sorting is also deferred, but it needs to materialize the filtered + // values internally when the terminal operation is called + .sort_ascending() + + // this transform is not evaluated yet + .map([](const int& number) { + return std::to_string(number); + }) + + // terminal operation: all stored operations are executed here + .get(); + +// processed_numbers -> fcpp::vector({ "3", "4", "5" }) +// numbers -> fcpp::vector({ 5, 1, 4, 2, 3 }) +``` + +Here is another example without sorting, thus all operations are materialized in the end. + +```c++ +const auto total = numbers + // start a lazy pipeline from this point on + .lazy() + + // this transform is not evaluated yet + .map([](const int& number) { + return number * 3; + }) + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 5; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const int& number) { + return partial_sum + number; + }); + +// total -> 42 +``` + +Lazy zip can combine a lazy vector with an `fcpp::vector`, a `std::vector`, or another `fcpp::lazy_vector` and also waits until a terminal operation is called, and only then checks that both sides have equal sizes. When zipping with another lazy vector, the right-hand lazy vector is materialized internally at that point, so its values can be paired by index. + +```c++ +const fcpp::vector ages({32, 45, 37}); +const fcpp::vector names({"Jake", "Anna", "Kate"}); + +const auto employees = ages + // start a lazy pipeline from this point on + .lazy() + + // zip is not evaluated yet + .zip(names) + + // this transform is not evaluated yet + .map([](const std::pair& pair) { + return person(pair.first, pair.second); + }) + + // terminal operation: zip size validation and all stored operations run here + .get(); + +// employees -> fcpp::vector({ +// person(32, "Jake"), +// person(45, "Anna"), +// person(37, "Kate"), +// }) +``` + ### index search ```c++ #include "vector.h" // instead of @@ -367,6 +484,93 @@ const auto total_age = employees_below_40.reduce(0, [](const int& partial_sum, c }); ``` +### lazy operations +Lazy sets are useful when chaining operations over a large set and only needing the final materialized set or a reduced value. A regular `map().filter().reduce()` chain creates intermediate sets and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map/filter/reduce-style pipelines process keys in one pass. Unlike vectors, sets are already ordered by their comparator, so lazy sets focus on the operations that make sense for set data: `map`, `filter`, `difference_with`, `union_with`, `intersect_with`, `zip`, and `reduce`. + +```c++ +#include "set.h" // instead of + +const fcpp::set numbers({1, 2, 3, 4, 5}); + +const auto total = numbers + // start a lazy pipeline from this point on + .lazy() + + // this transform is not evaluated yet + .map([](const int& number) { + return number * 3; + }) + + // this predicate is not evaluated yet + .filter([](const int& number) { + return number > 5; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const int& number) { + return partial_sum + number; + }); + +// total -> 42 +``` + +Lazy set algebra can combine a lazy set with an `fcpp::set`, a `std::set`, or another `fcpp::lazy_set`. The operation is still deferred, but set algebra needs set membership and sorted set semantics, so the current lazy pipeline is materialized internally when the terminal operation is called. When the right-hand side is also lazy, it is materialized internally at the same point. + +```c++ +const fcpp::set colleague_ages({15, 18, 25, 41, 51}); +const fcpp::set friend_ages({41, 42, 51}); +const fcpp::set family_ages({51, 81}); + +const auto guests = colleague_ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const int& age) { + return age >= 18; + }) + + // set difference is not evaluated yet + .difference_with(friend_ages) + + // set union is not evaluated yet + .union_with(family_ages) + + // terminal operation: the lazy filter and set algebra run here + .get(); + +// guests -> fcpp::set({18, 25, 51, 81}) +``` + +Lazy set zip can combine a lazy set with an `fcpp::set`, a `std::set`, an `fcpp::vector`, a `std::vector`, an `fcpp::lazy_vector`, or another `fcpp::lazy_set`. Size validation is deferred until a terminal operation is called. When zipping with a vector, duplicate vector values are removed before zipping, just like the eager set zip operation. When zipping with a lazy vector, the right-hand lazy vector is materialized internally at that point and then deduplicated. When zipping with another lazy set, the right-hand lazy set is materialized internally at that point, so its keys can be paired in set order. + +```c++ +const fcpp::set ages({25, 45, 30, 63}); +const fcpp::set names({"Jake", "Bob", "Michael", "Philipp"}); + +const auto employees = ages + // start a lazy pipeline from this point on + .lazy() + + // zip is not evaluated yet + .zip(names) + + // this transform is not evaluated yet + .map([](const std::pair& pair) { + return person(pair.first, pair.second); + }) + + // terminal operation: zip size validation and all stored operations run here + .get(); + +// employees -> fcpp::set({ +// person(25, "Bob"), +// person(30, "Jake"), +// person(45, "Michael"), +// person(63, "Philipp"), +// }) +``` + ### all_of, any_of, none_of ```c++ #include "set.h" // instead of @@ -466,6 +670,57 @@ adults.for_each([](const std::pair& element) { }); ``` +### lazy operations +Lazy maps are useful when chaining `map_to`, `filter`, and `reduce` over a large map. A regular `filtered().map_to().reduce()` style chain creates intermediate maps and iterates once per algorithm. Calling `.lazy()` stores the following operations and executes them only when a terminal operation is called, such as `get()` or `reduce()`. This can avoid unnecessary intermediate allocations and lets map_to/filter/reduce-style pipelines process key/value pairs in one pass. When a lazy `map_to` creates equivalent output keys, the first key/value pair encountered in sorted map order is kept, following `std::map::insert` semantics. + +```c++ +#include "map.h" // instead of + +const fcpp::map ages({ + {"jake", 32}, + {"mary", 16}, + {"david", 40} +}); + +const auto ages_by_initial = ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const std::pair& element) { + return element.second >= 18; + }) + + // this transform is not evaluated yet + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + }) + + // terminal operation: all stored operations are executed here + .get(); + +// ages_by_initial -> fcpp::map({{'d', "40 years"}, {'j', "32 years"}}) +// ages -> fcpp::map({{"david", 40}, {"jake", 32}, {"mary", 16}}) +``` + +```c++ +const auto total_age = ages + // start a lazy pipeline from this point on + .lazy() + + // this predicate is not evaluated yet + .filter([](const std::pair& element) { + return element.second >= 18; + }) + + // terminal operation: all stored operations are executed here + .reduce(0, [](const int& partial_sum, const std::pair& element) { + return partial_sum + element.second; + }); + +// total_age -> 72 +``` + ### all_of, any_of, none_of ```c++ #include "map.h" // instead of diff --git a/include/export_def.h b/include/export_def.h index dcca3a3..2f306f1 100644 --- a/include/export_def.h +++ b/include/export_def.h @@ -31,5 +31,3 @@ #else #define FunctionalCppExport __attribute__ ((__visibility__("default"))) #endif - -#include "compatibility.h" diff --git a/include/map.h b/include/map.h index 775955d..c0511cc 100644 --- a/include/map.h +++ b/include/map.h @@ -21,15 +21,199 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include #include +#include +#include +#include #include +#include #include #include #include "vector.h" namespace fcpp { +template +class map; + +// A lightweight wrapper representing a deferred map pipeline, enabling fluent and functional +// programming while avoiding intermediate map materialization. +// +// Member functions are non-mutating and keep extending the pipeline. Terminal functions such as +// `get` and `reduce` execute the stored operations. +template > +class lazy_map +{ +public: + using value_type = std::pair; + + lazy_map() + : m_compare() + , m_operation([](const std::function&) {}) + { + } + + // Creates a lazy map by copying the provided std::map as an owned source. + explicit lazy_map(const std::map& map) + : m_compare(map.key_comp()) + { + auto source = std::make_shared>(map); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy map by moving the provided std::map as an owned source. + explicit lazy_map(std::map&& map) + : m_compare(map.key_comp()) + { + auto source = std::make_shared>(std::move(map)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy map by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_map instances. + explicit lazy_map(std::function&)> operation) + : m_compare() + , m_operation(std::move(operation)) + { + } + + // Creates a lazy map by directly providing the deferred operation and comparator. + // This constructor is mostly useful for preserving comparator state while composing + // lazy_map instances. + lazy_map(std::function&)> operation, + const TCompare& compare) + : m_compare(compare) + , m_operation(std::move(operation)) + { + } + + // Performs the functional `map_to` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto labels_by_initial = ages + // .lazy() + // .map_to([](const auto& element) { + // return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + // }) + // .get(); + // + // outcome: + // labels_by_initial -> fcpp::map({ + // {'d', "40 years"}, {'j', "32 years"}, {'m', "26 years"} + // }) +#ifdef CPP17_AVAILABLE + template , Transform, value_type>>> +#else + template +#endif + [[nodiscard]] lazy_map map_to(Transform&& transform) const + { + const auto previous = m_operation; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_map( + [previous, transform_copy](const std::function::value_type&)>& consumer) mutable { + previous([&consumer, &transform_copy](const value_type& element) { + const auto transformed = transform_copy(element); + const typename lazy_map::value_type transformed_element(transformed.first, transformed.second); + consumer(transformed_element); + }); + }); + } + + // Performs the functional `filter` algorithm lazily, in which all key/value pairs which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto adults = ages + // .lazy() + // .filter([](const auto& element) { + // return element.second >= 32; + // }) + // .get(); + // + // outcome: + // adults -> fcpp::map({{"david", 40}, {"jake", 32}}) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_map filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_map( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const value_type& element) { + if (predicate_copy(element)) { + consumer(element); + } + }); + }, + m_compare); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_map filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all key/value pairs in this lazy map to an initial value. + // + // example: + // const fcpp::map ages({{"jake", 32}, {"mary", 26}, {"david", 40}}); + // const auto total_age = ages + // .lazy() + // .filter([](const auto& element) { + // return element.second >= 32; + // }) + // .reduce(0, [](const int& partial_sum, const auto& element) { + // return partial_sum + element.second; + // }); + // + // outcome: + // total_age -> 72 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const value_type& element) { + result = reduction(result, element); + }); + return result; + } + + // Materializes this lazy map to a functional map, executing all stored operations. + [[nodiscard]] map get() const; + +private: + TCompare m_compare; + std::function&)> m_operation; +}; + // A lightweight wrapper around std::map, enabling fluent and functional // programming on the map itself, rather than using the more procedural style // of the standard library algorithms. @@ -436,6 +620,13 @@ class map return m_map.size(); } + // Starts a lazy pipeline. The returned lazy map defers following map_to/filter transformations + // until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_map lazy() const + { + return lazy_map(m_map); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::map::iterator begin() { @@ -511,4 +702,14 @@ class map std::map m_map; }; +template +[[nodiscard]] map lazy_map::get() const +{ + std::map materialized(m_compare); + m_operation([&materialized](const value_type& element) { + materialized.insert(element); + }); + return map(std::move(materialized)); +} + } diff --git a/include/optional.h b/include/optional.h index 8fd62f7..81c5946 100644 --- a/include/optional.h +++ b/include/optional.h @@ -24,15 +24,19 @@ #pragma once #include "compatibility.h" -namespace fcpp { #ifdef CPP17_AVAILABLE #include -template -using optional_t = std::optional; #else +#include #include #include +#endif +namespace fcpp { +#ifdef CPP17_AVAILABLE +template +using optional_t = std::optional; +#else // A replacement for std::optional when C++17 is not available template class optional diff --git a/include/set.h b/include/set.h index 37d8025..58f6ac7 100644 --- a/include/set.h +++ b/include/set.h @@ -21,14 +21,461 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include "optional.h" namespace fcpp { template class vector; + template + class lazy_vector; + + template + class set; + + // A lightweight wrapper representing a deferred set pipeline, enabling fluent and functional + // programming while avoiding intermediate set materialization. + // + // Member functions are non-mutating and keep extending the pipeline. Terminal functions such as + // `get` and `reduce` execute the stored operations. + template > + class lazy_set + { + public: + lazy_set() + : m_compare() + , m_operation([](const std::function&) {}) + { + } + + // Creates a lazy set by copying the provided std::set as an owned source. + explicit lazy_set(const std::set& set) + : m_compare(set.key_comp()) + { + auto source = std::make_shared>(set); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy set by moving the provided std::set as an owned source. + explicit lazy_set(std::set&& set) + : m_compare(set.key_comp()) + { + auto source = std::make_shared>(std::move(set)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy set by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_set instances. + explicit lazy_set(std::function&)> operation) + : m_compare() + , m_operation(std::move(operation)) + { + } + + // Creates a lazy set by directly providing the deferred operation and comparator. + // This constructor is mostly useful for preserving comparator state while composing + // lazy_set instances. + lazy_set(std::function&)> operation, + const TCompare& compare) + : m_compare(compare) + , m_operation(std::move(operation)) + { + } + + // Performs the functional `map` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set input_set({ 1, 3, -5 }); + // const auto output_set = input_set + // .lazy() + // .map([](const int& element) { + // return std::to_string(element); + // }) + // .get(); + // + // outcome: + // output_set -> fcpp::set({ "-5", "1", "3" }) +#ifdef CPP17_AVAILABLE + template , typename Transform, typename = std::enable_if_t< + std::is_invocable_r_v>> +#else + template , typename Transform> +#endif + [[nodiscard]] lazy_set map(Transform&& transform) const + { + const auto previous = m_operation; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_set( + [previous, transform_copy](const std::function& consumer) mutable { + previous([&consumer, &transform_copy](const TKey& key) { + consumer(transform_copy(key)); + }); + }); + } + + // Performs the functional `filter` algorithm lazily, in which all keys which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto filtered_numbers = numbers + // .lazy() + // .filter([](const int& element) { + // return element >= 1.5; + // }) + // .get(); + // + // outcome: + // filtered_numbers -> fcpp::set({ 2, 3, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_set filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_set( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const TKey& key) { + if (predicate_copy(key)) { + consumer(key); + } + }); + }, + m_compare); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_set filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Returns the lazy set of elements which belong to this lazy set but not in the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto diff = set1 + // .lazy() + // .difference_with(set2) + // .get(); + // + // outcome: + // diff -> fcpp::set({1, 3, 8}) + [[nodiscard]] lazy_set difference_with(const set& other) const + { + return difference_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); + } + + // Returns the lazy set of elements which belong to this lazy set but not in the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set difference_with(const std::set& other) const + { + return difference_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong to this lazy set but not in the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set difference_with(const lazy_set& other) const + { + const auto previous = m_operation; + const auto compare = m_compare; + return lazy_set( + [previous, other, compare](const std::function& consumer) { + std::set current(compare); + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = materialize_with_compare(other, compare); + std::set diff(compare); + std::set_difference(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(diff, diff.begin()), + compare); + std::for_each(diff.begin(), diff.end(), consumer); + }, + compare); + } + + // Returns the lazy set of elements which belong either to this lazy set or the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto combined = set1 + // .lazy() + // .union_with(set2) + // .get(); + // + // outcome: + // combined -> fcpp::set({1, 2, 3, 5, 7, 8, 10, 15, 17}) + [[nodiscard]] lazy_set union_with(const set& other) const + { + return union_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); + } + + // Returns the lazy set of elements which belong either to this lazy set or the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set union_with(const std::set& other) const + { + return union_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong either to this lazy set or the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set union_with(const lazy_set& other) const + { + const auto previous = m_operation; + const auto compare = m_compare; + return lazy_set( + [previous, other, compare](const std::function& consumer) { + std::set current(compare); + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = materialize_with_compare(other, compare); + std::set combined(compare); + std::set_union(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(combined, combined.begin()), + compare); + std::for_each(combined.begin(), combined.end(), consumer); + }, + compare); + } + + // Returns the lazy set of elements which belong to both this lazy set and the other set. + // The operation is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::set set1({1, 2, 3, 5, 7, 8, 10}); + // const fcpp::set set2({2, 5, 7, 10, 15, 17}); + // const auto combined = set1 + // .lazy() + // .intersect_with(set2) + // .get(); + // + // outcome: + // combined -> fcpp::set({2, 5, 7, 10}) + [[nodiscard]] lazy_set intersect_with(const set& other) const + { + return intersect_with(lazy_set(std::set(other.begin(), + other.end(), + other.key_comp()))); + } + + // Returns the lazy set of elements which belong to both this lazy set and the std::set. + // The operation is deferred until a terminal operation is called. + [[nodiscard]] lazy_set intersect_with(const std::set& other) const + { + return intersect_with(lazy_set(other)); + } + + // Returns the lazy set of elements which belong to both this lazy set and the other lazy set. + // Both lazy sets are materialized internally when a terminal operation is called. + [[nodiscard]] lazy_set intersect_with(const lazy_set& other) const + { + const auto previous = m_operation; + const auto compare = m_compare; + return lazy_set( + [previous, other, compare](const std::function& consumer) { + std::set current(compare); + previous([¤t](const TKey& key) { + current.insert(key); + }); + + const auto materialized_other = materialize_with_compare(other, compare); + std::set intersection(compare); + std::set_intersection(current.begin(), + current.end(), + materialized_other.begin(), + materialized_other.end(), + std::inserter(intersection, intersection.begin()), + compare); + std::for_each(intersection.begin(), intersection.end(), consumer); + }, + compare); + } + + // Performs the functional `zip` algorithm lazily, in which every key of the resulting + // lazy set is a tuple of this instance's key (first) and the second set's key (second). + // The sizes of the two sets must be equal. + template + [[nodiscard]] lazy_set> zip(const set& set) const + { + return zip(lazy_set(std::set(set.begin(), + set.end(), + set.key_comp()))); + } + + // Performs the functional `zip` algorithm lazily. + // The sizes of the two sets must be equal. + template + [[nodiscard]] lazy_set> zip(const std::set& set) const + { + return zip(lazy_set(set)); + } + + // Performs the functional `zip` algorithm lazily where duplicates are removed before zipping. + // The input vector must contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const vector& vector) const + { + std::set distinct_values(vector.begin(), vector.end()); + return zip(lazy_set(std::move(distinct_values))); + } + + // Performs the functional `zip` algorithm lazily where duplicates are removed before zipping. + // The input vector must contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const std::vector& vector) const + { + std::set distinct_values(vector.begin(), vector.end()); + return zip(lazy_set(std::move(distinct_values))); + } + + // Performs the functional `zip` algorithm lazily where the lazy vector is materialized + // and duplicates are removed when a terminal operation is called. The lazy vector must + // contain the same number of distinct values as the set size. + template + [[nodiscard]] lazy_set> zip(const lazy_vector& vector) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, vector](const std::function&)>& consumer) { + const auto materialized_vector = vector.get(); + std::set distinct_values(materialized_vector.begin(), materialized_vector.end()); + auto it = distinct_values.begin(); + previous([&distinct_values, &it, &consumer](const TKey& key) { + if (it == distinct_values.end()) { + assert(false); + std::abort(); + } + consumer({key, *it}); + ++it; + }); + if (it != distinct_values.end()) { + assert(false); + std::abort(); + } + }); + } + + // Performs the functional `zip` algorithm lazily, in which every key of the resulting + // lazy set is a tuple of this instance's key (first) and the second lazy set's key (second). + // The sizes of the two lazy sets must be equal. + // The right-hand lazy set is materialized internally when a terminal operation is called. + template + [[nodiscard]] lazy_set> zip(const lazy_set& set) const + { + const auto previous = m_operation; + return lazy_set>( + [previous, set](const std::function&)>& consumer) { + const auto materialized_set = set.get(); + size_t index = 0; + previous([&materialized_set, &consumer, &index](const TKey& key) { + if (index >= materialized_set.size()) { + assert(false); + std::abort(); + } + consumer({key, materialized_set[index]}); + ++index; + }); + if (index != materialized_set.size()) { + assert(false); + std::abort(); + } + }); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all the values in this lazy set to an initial value. + // + // example: + // const fcpp::set numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto sum = numbers + // .lazy() + // .filter([](const int& element) { + // return element > 0; + // }) + // .reduce(0, [](const int& partial_sum, const int& number) { + // return partial_sum + number; + // }); + // + // outcome: + // sum -> 15 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const TKey& key) { + result = reduction(result, key); + }); + return result; + } + + // Materializes this lazy set to a functional set, executing all stored operations. + [[nodiscard]] set get() const; + + private: + [[nodiscard]] static std::set materialize_with_compare(const lazy_set& other, + const TCompare& compare) + { + const auto materialized_other = other.get(); + return std::set(materialized_other.begin(), + materialized_other.end(), + compare); + } + + TCompare m_compare; + std::function&)> m_operation; + }; + // A lightweight wrapper around std::set, enabling fluent and functional // programming on the set itself, rather than using the more procedural style // of the standard library algorithms. @@ -612,6 +1059,19 @@ namespace fcpp { return m_set.size(); } + // Returns a copy of the comparator used to order this set's keys. + [[nodiscard]] TCompare key_comp() const + { + return m_set.key_comp(); + } + + // Starts a lazy pipeline. The returned lazy set defers following map/filter/zip + // transformations until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_set lazy() const + { + return lazy_set(m_set); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::set::iterator begin() { @@ -739,4 +1199,14 @@ namespace fcpp { return set>(combined_set); } }; + + template + [[nodiscard]] set lazy_set::get() const + { + std::set materialized(m_compare); + m_operation([&materialized](const TKey& key) { + materialized.insert(key); + }); + return set(std::move(materialized)); + } } diff --git a/include/vector.h b/include/vector.h index be3da65..42b5735 100644 --- a/include/vector.h +++ b/include/vector.h @@ -21,21 +21,384 @@ // SOFTWARE. #pragma once +#include "compatibility.h" + #include #include +#include +#include +#ifdef PARALLEL_ALGORITHM_AVAILABLE +#include +#endif +#include +#include +#include +#include #include +#include #include -#include + #include "index_range.h" #include "optional.h" -#ifdef PARALLEL_ALGORITHM_AVAILABLE -#include -#endif namespace fcpp { template class set; + template + class vector; + + // A lightweight wrapper representing a deferred vector pipeline, enabling fluent and functional + // programming while avoiding intermediate vector materialization. + // + // Member functions are non-mutating and keep extending the pipeline. Terminal functions such as + // `get` and `reduce` execute the stored operations. + template + class lazy_vector + { + public: + lazy_vector() + : m_operation([](const std::function&) {}) + , m_capacity_hint(0) + { + } + + // Creates a lazy vector by copying the provided std::vector as an owned source. + explicit lazy_vector(const std::vector& vector) + : m_capacity_hint(vector.size()) + { + auto source = std::make_shared>(vector); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy vector by moving the provided std::vector as an owned source. + explicit lazy_vector(std::vector&& vector) + : m_capacity_hint(vector.size()) + { + auto source = std::make_shared>(std::move(vector)); + m_operation = [source](const std::function& consumer) { + std::for_each(source->begin(), source->end(), consumer); + }; + } + + // Creates a lazy vector by directly providing the deferred operation. + // This constructor is mostly useful for composing lazy_vector instances. + lazy_vector(std::function&)> operation, size_t capacity_hint) + : m_operation(std::move(operation)) + , m_capacity_hint(capacity_hint) + { + } + + // Performs the functional `map` algorithm lazily. The transform is not applied until + // a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector input_vector({ 1, 3, -5 }); + // const auto output_vector = input_vector + // .lazy() + // .map([](const auto& element) { + // return std::to_string(element); + // }) + // .get(); + // + // outcome: + // output_vector -> fcpp::vector({ "1", "3", "-5" }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector map(Transform&& transform) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type transform_copy(std::forward(transform)); + return lazy_vector( + [previous, transform_copy](const std::function& consumer) mutable { + previous([&consumer, &transform_copy](const T& element) { + consumer(transform_copy(element)); + }); + }, + capacity_hint); + } + + // Performs the functional `map` algorithm lazily. + // See also `map` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector mapped(Transform&& transform) const + { + return map(std::forward(transform)); + } + + // Performs the functional `filter` algorithm lazily, in which all elements which match + // the given predicate are kept. The predicate is not applied until a terminal operation, + // such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto filtered_numbers = numbers + // .lazy() + // .filter([](const auto& element) { + // return element >= 1.5; + // }) + // .get(); + // + // outcome: + // filtered_numbers -> fcpp::vector({ 3, 2, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector filter(Filter&& predicate_to_keep) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type predicate_copy(std::forward(predicate_to_keep)); + return lazy_vector( + [previous, predicate_copy](const std::function& consumer) mutable { + previous([&consumer, &predicate_copy](const T& element) { + if (predicate_copy(element)) { + consumer(element); + } + }); + }, + capacity_hint); + } + + // Performs the functional `filter` algorithm lazily. + // See also `filter` for more documentation. +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector filtered(Filter&& predicate_to_keep) const + { + return filter(std::forward(predicate_to_keep)); + } + + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the second vector's element + // (second) at the same index. The sizes of the two vectors must be equal. + // + // example: + // const fcpp::vector ages_vector({ 32, 25, 53 }); + // const fcpp::vector names_vector({ "Jake", "Mary", "John" }); + // const auto zipped_vector = ages_vector + // .lazy() + // .zip(names_vector) + // .get(); + // + // outcome: + // zipped_vector -> fcpp::vector>({ + // (32, "Jake"), + // (25, "Mary"), + // (53, "John"), + // }) + template + [[nodiscard]] lazy_vector> zip(const vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + const auto vector_copy = vector; + return lazy_vector>( + [previous, vector_copy](const std::function&)>& consumer) { + size_t index = 0; + previous([&vector_copy, &consumer, &index](const T& element) { + if (index >= vector_copy.size()) { + assert(false); + std::abort(); + } + consumer({element, vector_copy[index]}); + ++index; + }); + if (index != vector_copy.size()) { + assert(false); + std::abort(); + } + }, + capacity_hint); + } + + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the std::vector's element + // (second) at the same index. The sizes of the two vectors must be equal. + template + [[nodiscard]] lazy_vector> zip(const std::vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + const auto vector_copy = vector; + return lazy_vector>( + [previous, vector_copy](const std::function&)>& consumer) { + size_t index = 0; + previous([&vector_copy, &consumer, &index](const T& element) { + if (index >= vector_copy.size()) { + assert(false); + std::abort(); + } + consumer({element, vector_copy[index]}); + ++index; + }); + if (index != vector_copy.size()) { + assert(false); + std::abort(); + } + }, + capacity_hint); + } + + // Performs the functional `zip` algorithm lazily, in which every element of the resulting + // lazy vector is a tuple of this instance's element (first) and the second lazy vector's + // element (second) at the same index. The sizes of the two lazy vectors must be equal. + // The right-hand lazy vector is materialized internally when a terminal operation is called. + template + [[nodiscard]] lazy_vector> zip(const lazy_vector& vector) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + return lazy_vector>( + [previous, vector](const std::function&)>& consumer) { + const auto materialized_vector = vector.get(); + size_t index = 0; + previous([&materialized_vector, &consumer, &index](const T& element) { + if (index >= materialized_vector.size()) { + assert(false); + std::abort(); + } + consumer({element, materialized_vector[index]}); + ++index; + }); + if (index != materialized_vector.size()) { + assert(false); + std::abort(); + } + }, + capacity_hint); + } + + // Sorts the lazy vector. The comparison predicate takes two elements + // `v1` and `v2` and returns true if the first element `v1` should appear before `v2`. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort([](const auto& number1, const auto& number2) { + // return number1 < number2; + // }) + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ -4, 1, 3, 9 }) +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + [[nodiscard]] lazy_vector sort(Sortable&& comparison_predicate) const + { + const auto previous = m_operation; + const auto capacity_hint = m_capacity_hint; + typename std::decay::type comparison_copy(std::forward(comparison_predicate)); + return lazy_vector( + [previous, capacity_hint, comparison_copy](const std::function& consumer) mutable { + std::vector sorted_vector; + sorted_vector.reserve(capacity_hint); + previous([&sorted_vector](const T& element) { + sorted_vector.push_back(element); + }); + + std::sort(sorted_vector.begin(), + sorted_vector.end(), + comparison_copy); + std::for_each(sorted_vector.begin(), + sorted_vector.end(), + consumer); + }, + capacity_hint); + } + + // Sorts the lazy vector in ascending order, when its elements support comparison by std::less [<]. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort_ascending() + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ -4, 1, 3, 9 }) + [[nodiscard]] lazy_vector sort_ascending() const + { + return sort(std::less()); + } + + // Sorts the lazy vector in descending order, when its elements support comparison by std::greater [>]. + // Sorting is deferred until a terminal operation, such as `get` or `reduce`, is called. + // + // example: + // const fcpp::vector numbers({ 3, 1, 9, -4 }); + // const auto sorted_numbers = numbers + // .lazy() + // .sort_descending() + // .get(); + // + // outcome: + // sorted_numbers -> fcpp::vector({ 9, 3, 1, -4 }) + [[nodiscard]] lazy_vector sort_descending() const + { + return sort(std::greater()); + } + + // Performs the functional `reduce` (fold/accumulate) algorithm, by returning the result of + // accumulating all the values in this lazy vector to an initial value. + // + // example: + // const fcpp::vector numbers({ 1, 3, -5, 2, -1, 9, -4 }); + // const auto sum = numbers + // .lazy() + // .filter([](const auto& element) { + // return element > 0; + // }) + // .reduce(0, [](const int& partial_sum, const int& number) { + // return partial_sum + number; + // }); + // + // outcome: + // sum -> 15 +#ifdef CPP17_AVAILABLE + template >> +#else + template +#endif + U reduce(const U& initial, Reduce&& reduction) const + { + auto result = initial; + m_operation([&result, &reduction](const T& element) { + result = reduction(result, element); + }); + return result; + } + + // Materializes this lazy vector to a functional vector, executing all stored operations. + [[nodiscard]] vector get() const; + + private: + std::function&)> m_operation; + size_t m_capacity_hint; + }; + // A lightweight wrapper around std::vector, enabling fluent and functional // programming on the vector itself, rather than using the more procedural style // of the standard library algorithms. @@ -1429,6 +1792,13 @@ namespace fcpp { return *this; } + // Starts a lazy pipeline. The returned lazy vector defers following map/filter + // transformations until a terminal operation, such as get() or reduce(), is called. + [[nodiscard]] lazy_vector lazy() const + { + return lazy_vector(m_vector); + } + // Returns the begin iterator, useful for other standard library algorithms [[nodiscard]] typename std::vector::iterator begin() { @@ -1685,4 +2055,15 @@ namespace fcpp { assert(index <= size()); } }; + + template + [[nodiscard]] vector lazy_vector::get() const + { + std::vector materialized; + materialized.reserve(m_capacity_hint); + m_operation([&materialized](const T& element) { + materialized.push_back(element); + }); + return vector(std::move(materialized)); + } } diff --git a/tests/map_test.cc b/tests/map_test.cc index 43df882..ca55941 100644 --- a/tests/map_test.cc +++ b/tests/map_test.cc @@ -20,10 +20,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include + #include -#include "warnings.h" + #include "map.h" -#include +#include "warnings.h" using namespace fcpp; @@ -34,6 +38,33 @@ void test_contents(const map& map_under_test) { EXPECT_EQ("three", map_under_test[3]); } +struct stateful_descending_int_compare +{ + stateful_descending_int_compare() = delete; + + explicit stateful_descending_int_compare(bool descending) + : descending(descending) + { + } + + bool operator()(const int& lhs, const int& rhs) const + { + return descending + ? lhs > rhs + : lhs < rhs; + } + + bool descending; +}; + +std::map make_stateful_descending_map( + const std::initializer_list>& values) +{ + std::map result(stateful_descending_int_compare(true)); + result.insert(values.begin(), values.end()); + return result; +} + TEST(MapTest, EmptyConstructor) { const map map_under_test; @@ -247,3 +278,122 @@ TEST(MapTest, InequalityOperator) EXPECT_FALSE(map1 == map2); EXPECT_TRUE(map1 != map2); } + +TEST(MapTest, LazyMapToFilterGet) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map_to([&map_call_count](const std::pair& element) { + ++map_call_count; + return std::make_pair(element.first[0], std::to_string(element.second) + " years"); + }) + .filter([&filter_call_count](const std::pair& element) { + ++filter_call_count; + return element.first != 'm'; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto mapped = lazy_persons.get(); + + EXPECT_EQ((map({{'d', "40 years"}, {'j', "32 years"}})), mapped); + EXPECT_EQ((map({{"david", 40}, {"jake", 32}, {"mary", 26}})), persons); + EXPECT_EQ(3, map_call_count); + EXPECT_EQ(3, filter_call_count); +} + +TEST(MapTest, LazyFiltered) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + + const auto filtered_persons = persons + .lazy() + .filtered([](const std::pair& element) { + return element.second >= 32; + }) + .get(); + + EXPECT_EQ((map({{"david", 40}, {"jake", 32}})), filtered_persons); + EXPECT_EQ((map({{"david", 40}, {"jake", 32}, {"mary", 26}})), persons); +} + +TEST(MapTest, LazyFilteredPreservesComparatorState) +{ + const map numbers( + make_stateful_descending_map({{1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}})); + + const auto filtered_numbers = numbers + .lazy() + .filtered([](const std::pair& element) { + return element.first % 2 == 0; + }) + .get(); + + EXPECT_EQ((vector({4, 2})), filtered_numbers.keys()); + EXPECT_EQ("four", filtered_numbers[4]); + EXPECT_EQ("two", filtered_numbers[2]); +} + +TEST(MapTest, LazyReduce) +{ + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + int filter_call_count = 0; + + const auto total_age = persons + .lazy() + .filter([&filter_call_count](const std::pair& element) { + ++filter_call_count; + return element.second >= 32; + }) + .reduce(0, [](const int& partial_sum, const std::pair& element) { + return partial_sum + element.second; + }); + + EXPECT_EQ(72, total_age); + EXPECT_EQ(3, filter_call_count); +} + +TEST(MapTest, LazySourceCanOutliveFunctionalMap) +{ + lazy_map lazy_persons; + { + const map persons({{"jake", 32}, {"mary", 26}, {"david", 40}}); + lazy_persons = persons + .lazy() + .filter([](const std::pair& element) { + return element.second >= 32; + }); + } + + EXPECT_EQ((map({{"david", 40}, {"jake", 32}})), lazy_persons.get()); +} + +TEST(MapTest, LazySourceCanStartFromTemporaryFunctionalMap) +{ + const auto lazy_persons = map({{"jake", 32}, {"mary", 26}}) + .lazy() + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], element.second); + }); + + EXPECT_EQ((map({{'j', 32}, {'m', 26}})), lazy_persons.get()); +} + +TEST(MapTest, LazyMapToDuplicateKeysKeepsFirst) +{ + const map persons({{"anna", 28}, {"alex", 30}, {"david", 40}}); + + const auto mapped = persons + .lazy() + .map_to([](const std::pair& element) { + return std::make_pair(element.first[0], element.second); + }) + .get(); + + EXPECT_EQ((map({{'a', 30}, {'d', 40}})), mapped); +} diff --git a/tests/set_test.cc b/tests/set_test.cc index 48fe52e..6751d9d 100644 --- a/tests/set_test.cc +++ b/tests/set_test.cc @@ -20,22 +20,21 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include +#include +#include + #include -#include "warnings.h" + #include "set.h" -#include "vector.h" #include "test_types.h" +#include "vector.h" +#include "warnings.h" using namespace fcpp; -template -void debug(set& set) -{ - set.for_each([](const T& element) { - std::cout << element << std::endl; - }); -} - void test_contents(const set& set) { EXPECT_EQ(3, set.size()); EXPECT_EQ(1, set[0]); @@ -43,6 +42,40 @@ void test_contents(const set& set) { EXPECT_EQ(5, set[2]); } +struct stateful_descending_int_compare +{ + stateful_descending_int_compare() = delete; + + explicit stateful_descending_int_compare(bool descending) + : descending(descending) + { + } + + bool operator()(const int& lhs, const int& rhs) const + { + return descending + ? lhs > rhs + : lhs < rhs; + } + + bool descending; +}; + +std::set make_stateful_ordered_set( + const std::initializer_list& values, + bool descending) +{ + std::set result{stateful_descending_int_compare(descending)}; + result.insert(values.begin(), values.end()); + return result; +} + +std::set make_stateful_descending_set( + const std::initializer_list& values) +{ + return make_stateful_ordered_set(values, true); +} + TEST(SetTest, EmptyConstructor) { const set set_under_test; @@ -285,7 +318,6 @@ TEST(SetTest, MaxCustomType) person(62, "Bob") }); const auto maximum = persons.max(); - std::cout << maximum.value().name << std::endl; #if __linux__ // NOLINT(clang-diagnostic-undef) EXPECT_EQ(person(18, "Jannet"), maximum.value()); #else @@ -568,3 +600,634 @@ TEST(SetTest, EqualityOperatorCustomType) EXPECT_TRUE(set1 == set2); EXPECT_FALSE(set1 != set2); } + +TEST(SetTest, LazyMapFilterGet) +{ + const set set_under_test({1, 2, 3, 4}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_set = set_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 2; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 4; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto materialized_set = lazy_set.get(); + EXPECT_EQ(set({6, 8}), materialized_set); + EXPECT_EQ(set({1, 2, 3, 4}), set_under_test); + EXPECT_EQ(4, map_call_count); + EXPECT_EQ(4, filter_call_count); +} + +TEST(SetTest, LazyFiltered) +{ + const set set_under_test({1, 2, 3, 4}); + const auto filtered_set = set_under_test + .lazy() + .filtered([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(set({2, 4}), filtered_set); + EXPECT_EQ(set({1, 2, 3, 4}), set_under_test); +} + +TEST(SetTest, LazyReduce) +{ + const set set_under_test({1, 2, 3, 4, 5}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto result = set_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 3; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 5; + }) + .reduce(0, [](const int& partial_sum, const int& value) { + return partial_sum + value; + }); + + EXPECT_EQ(42, result); + EXPECT_EQ(5, map_call_count); + EXPECT_EQ(5, filter_call_count); +} + +TEST(SetTest, LazyZipWithFunctionalSet) +{ + const set ages({25, 45, 30, 63}); + const set persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithStdSet) +{ + const set ages({25, 45, 30, 63}); + const std::set persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithFunctionalVector) +{ + const set ages({25, 45, 30, 63}); + const vector persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithStdVector) +{ + const set ages({25, 45, 30, 63}); + const std::vector persons({"Jake", "Bob", "Michael", "Philipp"}); + + const auto zipped = ages + .lazy() + .zip(persons) + .get(); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazySourceCanOutliveFunctionalSet) +{ + lazy_set lazy_numbers; + { + const set numbers({1, 2, 3, 4}); + lazy_numbers = numbers + .lazy() + .filter([](const int& value) { + return value > 2; + }); + } + + EXPECT_EQ(set({3, 4}), lazy_numbers.get()); +} + +TEST(SetTest, LazySourceCanStartFromTemporaryFunctionalSet) +{ + const auto lazy_numbers = set({1, 2, 3, 4}) + .lazy() + .map([](const int& value) { + return value * 2; + }); + + EXPECT_EQ(set({2, 4, 6, 8}), lazy_numbers.get()); +} + +TEST(SetTest, LazyFilterPreservesComparatorState) +{ + const set numbers(make_stateful_descending_set({1, 2, 3, 4})); + + const auto filtered = numbers + .lazy() + .filter([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(2, filtered.size()); + EXPECT_EQ(4, filtered[0]); + EXPECT_EQ(2, filtered[1]); +} + +TEST(SetTest, LazyZipWithTemporaryFunctionalSet) +{ + const set ages({25, 45, 30, 63}); + + const auto lazy_zipped = ages + .lazy() + .zip(set({"Jake", "Bob", "Michael", "Philipp"})); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, lazy_zipped.get()); +} + +TEST(SetTest, LazyZipWithFunctionalSetPreservesComparatorState) +{ + const set left({1, 2, 3}); + const set right(make_stateful_descending_set({10, 20, 30})); + + const auto zipped = left + .lazy() + .zip(right) + .get(); + + const auto expected = set>({ + std::pair(1, 30), + std::pair(2, 20), + std::pair(3, 10), + }); + EXPECT_EQ(expected, zipped); +} + +TEST(SetTest, LazyZipWithTemporaryStdSet) +{ + const set ages({25, 45, 30, 63}); + + const auto lazy_zipped = ages + .lazy() + .zip(std::set({"Jake", "Bob", "Michael", "Philipp"})); + + const auto expected = set>({ + std::pair(25, "Bob"), + std::pair(30, "Jake"), + std::pair(45, "Michael"), + std::pair(63, "Philipp"), + }); + EXPECT_EQ(expected, lazy_zipped.get()); +} + +TEST(SetTest, LazyZipWithLazyVector) +{ + const set ages({25, 45, 30, 63}); + const vector persons({"Jake", "Bob", "Michael", "Philipp"}); + int map_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_zipped = ages + .lazy() + .zip(lazy_persons); + + EXPECT_EQ(0, map_call_count); + + const auto zipped = lazy_zipped.get(); + + const auto expected = set>({ + std::pair(25, "Bob!"), + std::pair(30, "Jake!"), + std::pair(45, "Michael!"), + std::pair(63, "Philipp!"), + }); + EXPECT_EQ(expected, zipped); + EXPECT_EQ(4, map_call_count); +} + +TEST(SetTest, LazyZipWithLazyVectorFewerDistinctValuesThrows) +{ + const set ages({25, 45}); + const vector persons({"Jake"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons.lazy()).get(); }, ""); +} + +TEST(SetTest, LazyZipWithLazyVectorMoreDistinctValuesThrows) +{ + const set ages({25}); + const vector persons({"Jake", "Bob"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons.lazy()).get(); }, ""); +} + +TEST(SetTest, LazyZipWithLazySet) +{ + const set ages({25, 45, 30, 63}); + const set persons({"Jake", "Bob", "Michael", "Philipp"}); + int map_call_count = 0; + + const auto lazy_persons = persons + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_zipped = ages + .lazy() + .zip(lazy_persons); + + EXPECT_EQ(0, map_call_count); + + const auto zipped = lazy_zipped.get(); + + const auto expected = set>({ + std::pair(25, "Bob!"), + std::pair(30, "Jake!"), + std::pair(45, "Michael!"), + std::pair(63, "Philipp!"), + }); + EXPECT_EQ(expected, zipped); + EXPECT_EQ(4, map_call_count); +} + +TEST(SetTest, LazyZipWithDifferentSizesThrows) +{ + const set ages({25, 45}); + const std::set persons({"Jake"}); + + EXPECT_DEATH({ const auto zipped = ages.lazy().zip(persons).get(); }, ""); +} + +TEST(SetTest, LazyDifferenceWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_diff = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .difference_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto diff = lazy_diff.get(); + + EXPECT_EQ(set({1, 3, 8}), diff); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyDifferenceWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto diff = set1 + .lazy() + .difference_with(set2) + .get(); + + EXPECT_EQ(set({1, 3, 8}), diff); +} + +TEST(SetTest, LazyDifferenceWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_diff = set1 + .lazy() + .difference_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto diff = lazy_diff.get(); + + EXPECT_EQ(set({1, 2, 8}), diff); + EXPECT_EQ(6, map_call_count); +} + +TEST(SetTest, LazyDifferenceWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto diff = set1 + .lazy() + .difference_with(set2.lazy()) + .get(); + + EXPECT_EQ(2, diff.size()); + EXPECT_EQ(3, diff[0]); + EXPECT_EQ(1, diff[1]); +} + +TEST(SetTest, LazyDifferenceWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_diff = set1 + .lazy() + .difference_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({1, 3, 8}), lazy_diff.get()); +} + +TEST(SetTest, LazyDifferenceWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto diff = set1 + .lazy() + .difference_with(set2) + .get(); + + EXPECT_EQ(2, diff.size()); + EXPECT_EQ(3, diff[0]); + EXPECT_EQ(1, diff[1]); +} + +TEST(SetTest, LazyUnionWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_combined = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .union_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto combined = lazy_combined.get(); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), combined); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyUnionWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto combined = set1 + .lazy() + .union_with(set2) + .get(); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), combined); +} + +TEST(SetTest, LazyUnionWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_combined = set1 + .lazy() + .union_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto combined = lazy_combined.get(); + + EXPECT_EQ(set({1, 2, 3, 4, 5, 7, 8, 9, 10}), combined); + EXPECT_EQ(6, map_call_count); +} + +TEST(SetTest, LazyUnionWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto combined = set1 + .lazy() + .union_with(set2.lazy()) + .get(); + + EXPECT_EQ(5, combined.size()); + EXPECT_EQ(6, combined[0]); + EXPECT_EQ(4, combined[1]); + EXPECT_EQ(3, combined[2]); + EXPECT_EQ(2, combined[3]); + EXPECT_EQ(1, combined[4]); +} + +TEST(SetTest, LazyUnionWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_combined = set1 + .lazy() + .union_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({1, 2, 3, 5, 7, 8, 10, 15, 17}), lazy_combined.get()); +} + +TEST(SetTest, LazyUnionWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto combined = set1 + .lazy() + .union_with(set2) + .get(); + + EXPECT_EQ(5, combined.size()); + EXPECT_EQ(6, combined[0]); + EXPECT_EQ(4, combined[1]); + EXPECT_EQ(3, combined[2]); + EXPECT_EQ(2, combined[3]); + EXPECT_EQ(1, combined[4]); +} + +TEST(SetTest, LazyIntersectionWithFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({2, 5, 7, 10, 15, 17}); + int filter_call_count = 0; + + const auto lazy_intersection = set1 + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value < 10; + }) + .intersect_with(set2); + + EXPECT_EQ(0, filter_call_count); + + const auto intersection = lazy_intersection.get(); + + EXPECT_EQ(set({2, 5, 7}), intersection); + EXPECT_EQ(7, filter_call_count); +} + +TEST(SetTest, LazyIntersectionWithStdSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const std::set set2({2, 5, 7, 10, 15, 17}); + + const auto intersection = set1 + .lazy() + .intersect_with(set2) + .get(); + + EXPECT_EQ(set({2, 5, 7, 10}), intersection); +} + +TEST(SetTest, LazyIntersectionWithLazySet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + const set set2({1, 2, 3, 5, 7, 8}); + int map_call_count = 0; + + const auto lazy_set2 = set2 + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value + 2; + }); + + const auto lazy_intersection = set1 + .lazy() + .intersect_with(lazy_set2); + + EXPECT_EQ(0, map_call_count); + + const auto intersection = lazy_intersection.get(); + + EXPECT_EQ(set({3, 5, 7, 10}), intersection); + EXPECT_EQ(6, map_call_count); +} + +TEST(SetTest, LazyIntersectionWithLazySetNormalizesRightHandComparatorState) +{ + const set set1(make_stateful_ordered_set({1, 2, 3, 4}, true)); + const set set2(make_stateful_ordered_set({2, 4, 6}, false)); + + const auto intersection = set1 + .lazy() + .intersect_with(set2.lazy()) + .get(); + + EXPECT_EQ(2, intersection.size()); + EXPECT_EQ(4, intersection[0]); + EXPECT_EQ(2, intersection[1]); +} + +TEST(SetTest, LazyIntersectionWithTemporaryFunctionalSet) +{ + const set set1({1, 2, 3, 5, 7, 8, 10}); + + const auto lazy_intersection = set1 + .lazy() + .intersect_with(set({2, 5, 7, 10, 15, 17})); + + EXPECT_EQ(set({2, 5, 7, 10}), lazy_intersection.get()); +} + +TEST(SetTest, LazyIntersectionWithFunctionalSetPreservesComparatorState) +{ + const set set1(make_stateful_descending_set({1, 2, 3, 4})); + const set set2(make_stateful_descending_set({2, 4, 6})); + + const auto intersection = set1 + .lazy() + .intersect_with(set2) + .get(); + + EXPECT_EQ(2, intersection.size()); + EXPECT_EQ(4, intersection[0]); + EXPECT_EQ(2, intersection[1]); +} diff --git a/tests/test_types.h b/tests/test_types.h index 3205945..70b66c2 100644 --- a/tests/test_types.h +++ b/tests/test_types.h @@ -21,7 +21,10 @@ // SOFTWARE. #pragma once +#include +#include #include +#include struct child { @@ -42,12 +45,6 @@ struct child } }; -struct child_comparator { - bool operator() (const child& a, const child& b) const { - return a < b; - } -}; - struct person { person() diff --git a/tests/vector_test.cc b/tests/vector_test.cc index 7e44f94..f42f3ce 100644 --- a/tests/vector_test.cc +++ b/tests/vector_test.cc @@ -20,11 +20,17 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +#include +#include +#include +#include + #include -#include "vector.h" -#include "set.h" + #include "index_range.h" +#include "set.h" #include "test_types.h" +#include "vector.h" #include "warnings.h" #pragma warning( push ) @@ -32,14 +38,6 @@ using namespace fcpp; -template -void debug(const vector& vec) -{ - vec.for_each([](const T& element){ - std::cout << element << std::endl; - }); -} - TEST(VectorTest, InsertBack) { vector vector_under_test; @@ -1375,4 +1373,318 @@ TEST(VectorTest, DistinctCustomType) EXPECT_EQ(expected, unique_persons); } +TEST(VectorTest, LazyMapFilterGet) +{ + const vector vector_under_test({1, 2, 3, 4}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 2; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 4; + }); + + EXPECT_EQ(0, map_call_count); + EXPECT_EQ(0, filter_call_count); + + const auto materialized_vector = lazy_vector.get(); + EXPECT_EQ(vector({6, 8}), materialized_vector); + EXPECT_EQ(vector({1, 2, 3, 4}), vector_under_test); + EXPECT_EQ(4, map_call_count); + EXPECT_EQ(4, filter_call_count); +} + +TEST(VectorTest, LazyFiltered) +{ + const vector vector_under_test({1, 2, 3, 4}); + const auto filtered_vector = vector_under_test + .lazy() + .filtered([](const int& value) { + return value % 2 == 0; + }) + .get(); + + EXPECT_EQ(vector({2, 4}), filtered_vector); + EXPECT_EQ(vector({1, 2, 3, 4}), vector_under_test); +} + +TEST(VectorTest, LazyReduce) +{ + const vector vector_under_test({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); + int map_call_count = 0; + int filter_call_count = 0; + + const auto result = vector_under_test + .lazy() + .map([&map_call_count](const int& value) { + ++map_call_count; + return value * 3; + }) + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 5; + }) + .reduce(0, [](const int& partial_sum, const int& value) { + return partial_sum + value; + }); + + EXPECT_EQ(162, result); + EXPECT_EQ(10, map_call_count); + EXPECT_EQ(10, filter_call_count); +} + +TEST(VectorTest, LazySourceCanOutliveFunctionalVector) +{ + lazy_vector lazy_numbers; + { + const vector vector_under_test({1, 2, 3, 4}); + lazy_numbers = vector_under_test + .lazy() + .filter([](const int& value) { + return value > 2; + }); + } + + EXPECT_EQ(vector({3, 4}), lazy_numbers.get()); +} + +TEST(VectorTest, LazySourceCanStartFromTemporaryFunctionalVector) +{ + const auto lazy_numbers = vector({1, 2, 3, 4}) + .lazy() + .map([](const int& value) { + return value * 2; + }); + + EXPECT_EQ(vector({2, 4, 6, 8}), lazy_numbers.get()); +} + +TEST(VectorTest, LazySort) +{ + const vector vector_under_test({ + person(45, "Jake"), person(34, "Bob"), person(52, "Manfred"), person(8, "Alice") + }); + + const auto sorted_vector = vector_under_test + .lazy() + .sort([](const person& person1, const person& person2) { + return person1.name < person2.name; + }) + .get(); + + EXPECT_EQ(4, vector_under_test.size()); + EXPECT_EQ("Jake", vector_under_test[0].name); + EXPECT_EQ("Bob", vector_under_test[1].name); + EXPECT_EQ("Manfred", vector_under_test[2].name); + EXPECT_EQ("Alice", vector_under_test[3].name); + + EXPECT_EQ(4, sorted_vector.size()); + EXPECT_EQ("Alice", sorted_vector[0].name); + EXPECT_EQ(8, sorted_vector[0].age); + EXPECT_EQ("Bob", sorted_vector[1].name); + EXPECT_EQ(34, sorted_vector[1].age); + EXPECT_EQ("Jake", sorted_vector[2].name); + EXPECT_EQ(45, sorted_vector[2].age); + EXPECT_EQ("Manfred", sorted_vector[3].name); + EXPECT_EQ(52, sorted_vector[3].age); +} + +TEST(VectorTest, LazySortAscending) +{ + const vector vector_under_test({3, 1, 9, -4}); + + const auto sorted_vector = vector_under_test + .lazy() + .sort_ascending() + .get(); + + EXPECT_EQ(vector({3, 1, 9, -4}), vector_under_test); + EXPECT_EQ(vector({-4, 1, 3, 9}), sorted_vector); +} + +TEST(VectorTest, LazySortDescending) +{ + const vector vector_under_test({3, 1, 9, -4}); + + const auto sorted_vector = vector_under_test + .lazy() + .sort_descending() + .get(); + + EXPECT_EQ(vector({3, 1, 9, -4}), vector_under_test); + EXPECT_EQ(vector({9, 3, 1, -4}), sorted_vector); +} + +TEST(VectorTest, LazyFilterSortMap) +{ + const vector vector_under_test({5, 1, 4, 2, 3}); + int filter_call_count = 0; + int map_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 2; + }) + .sort_ascending() + .map([&map_call_count](const int& value) { + ++map_call_count; + return std::to_string(value); + }); + + EXPECT_EQ(0, filter_call_count); + EXPECT_EQ(0, map_call_count); + + const auto materialized_vector = lazy_vector.get(); + + EXPECT_EQ(vector({"3", "4", "5"}), materialized_vector); + EXPECT_EQ(5, filter_call_count); + EXPECT_EQ(3, map_call_count); +} + +TEST(VectorTest, LazyZipWithFunctionalVector) +{ + const vector vector_under_test({1, 2, 3, 4}); + const vector names({"three", "four"}); + int filter_call_count = 0; + + const auto lazy_vector = vector_under_test + .lazy() + .filter([&filter_call_count](const int& value) { + ++filter_call_count; + return value > 2; + }) + .zip(names); + + EXPECT_EQ(0, filter_call_count); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(2, zipped_vector.size()); + EXPECT_EQ(3, zipped_vector[0].first); + EXPECT_EQ("three", zipped_vector[0].second); + EXPECT_EQ(4, zipped_vector[1].first); + EXPECT_EQ("four", zipped_vector[1].second); + EXPECT_EQ(4, filter_call_count); +} + +TEST(VectorTest, LazyZipWithStdVector) +{ + const vector vector_under_test({1, 2, 3}); + const std::vector names({"one", "two", "three"}); + + const auto zipped_vector = vector_under_test + .lazy() + .zip(names) + .get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithTemporaryFunctionalVector) +{ + const vector vector_under_test({1, 2, 3}); + + const auto lazy_vector = vector_under_test + .lazy() + .zip(vector({"one", "two", "three"})); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithTemporaryStdVector) +{ + const vector vector_under_test({1, 2, 3}); + + const auto lazy_vector = vector_under_test + .lazy() + .zip(std::vector({"one", "two", "three"})); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(1, zipped_vector[0].first); + EXPECT_EQ("one", zipped_vector[0].second); + EXPECT_EQ(2, zipped_vector[1].first); + EXPECT_EQ("two", zipped_vector[1].second); + EXPECT_EQ(3, zipped_vector[2].first); + EXPECT_EQ("three", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithLazyVector) +{ + const vector ages({32, 45, 37}); + const vector names({"Jake", "Anna", "Kate"}); + int map_call_count = 0; + + const auto lazy_names = names + .lazy() + .map([&map_call_count](const std::string& name) { + ++map_call_count; + return name + "!"; + }); + + const auto lazy_vector = ages + .lazy() + .zip(lazy_names); + + EXPECT_EQ(0, map_call_count); + + const auto zipped_vector = lazy_vector.get(); + + EXPECT_EQ(3, map_call_count); + EXPECT_EQ(3, zipped_vector.size()); + EXPECT_EQ(32, zipped_vector[0].first); + EXPECT_EQ("Jake!", zipped_vector[0].second); + EXPECT_EQ(45, zipped_vector[1].first); + EXPECT_EQ("Anna!", zipped_vector[1].second); + EXPECT_EQ(37, zipped_vector[2].first); + EXPECT_EQ("Kate!", zipped_vector[2].second); +} + +TEST(VectorTest, LazyZipWithDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const std::vector names({"one"}); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + +TEST(VectorTest, LazyZipWithFunctionalVectorDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const vector names({"one"}); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + +TEST(VectorTest, LazyZipWithLazyVectorDifferentSizesThrows) +{ + const vector vector_under_test({1, 2}); + const auto names = vector({"one"}).lazy(); + + EXPECT_DEATH({ const auto zipped_vector = vector_under_test.lazy().zip(names).get(); }, ""); +} + #pragma warning( pop )