From 796c8db3609967fa480ceac061fcf7ef06956500 Mon Sep 17 00:00:00 2001 From: Tristan Brindle Date: Fri, 4 Aug 2023 18:53:04 +0100 Subject: [PATCH 1/2] Add find_min/max/minmax algorithms These do the same as `min()`/`max()`/`minmax()`, but take multipass sequences and return a cursor to the requisite elements rather than an optional value, returning a past-the-end cursor if the input sequence is empty. Fixes #112 --- include/flux.hpp | 1 + include/flux/core/inline_sequence_base.hpp | 15 +++ include/flux/op/find_min_max.hpp | 112 ++++++++++++++++ include/flux/op/minmax.hpp | 1 + test/CMakeLists.txt | 1 + test/test_find_min_max.cpp | 149 +++++++++++++++++++++ test/test_utils.hpp | 2 +- 7 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 include/flux/op/find_min_max.hpp create mode 100644 test/test_find_min_max.cpp diff --git a/include/flux.hpp b/include/flux.hpp index bc56d1af..e989a961 100644 --- a/include/flux.hpp +++ b/include/flux.hpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/include/flux/core/inline_sequence_base.hpp b/include/flux/core/inline_sequence_base.hpp index 1205e3b4..635d94ba 100644 --- a/include/flux/core/inline_sequence_base.hpp +++ b/include/flux/core/inline_sequence_base.hpp @@ -388,6 +388,21 @@ struct inline_sequence_base { [[nodiscard]] constexpr auto find_if_not(Pred pred); + template + requires strict_weak_order_for + [[nodiscard]] + constexpr auto find_max(Cmp cmp = Cmp{}); + + template + requires strict_weak_order_for + [[nodiscard]] + constexpr auto find_min(Cmp cmp = Cmp{}); + + template + requires strict_weak_order_for + [[nodiscard]] + constexpr auto find_minmax(Cmp cmp = Cmp{}); + template requires foldable [[nodiscard]] diff --git a/include/flux/op/find_min_max.hpp b/include/flux/op/find_min_max.hpp new file mode 100644 index 00000000..fc91e500 --- /dev/null +++ b/include/flux/op/find_min_max.hpp @@ -0,0 +1,112 @@ + +// Copyright (c) 2023 Tristan Brindle (tcbrindle at gmail dot com) +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef FLUX_OP_FIND_MIN_MAX_HPP_INCLUDED +#define FLUX_OP_FIND_MIN_MAX_HPP_INCLUDED + +#include +#include + +namespace flux { + +namespace detail { + +struct find_min_fn { + template Cmp = std::ranges::less> + [[nodiscard]] + constexpr auto operator()(Seq&& seq, Cmp cmp = {}) const -> cursor_t + { + auto min = first(seq); + if (!is_last(seq, min)) { + for (auto cur = next(seq, min); !is_last(seq, cur); inc(seq, cur)) { + if (std::invoke(cmp, read_at(seq, cur), read_at(seq, min))) { + min = cur; + } + } + } + + return min; + } +}; + +struct find_max_fn { + template Cmp = std::ranges::less> + [[nodiscard]] + constexpr auto operator()(Seq&& seq, Cmp cmp = {}) const -> cursor_t + { + auto max = first(seq); + if (!is_last(seq, max)) { + for (auto cur = next(seq, max); !is_last(seq, cur); inc(seq, cur)) { + if (!std::invoke(cmp, read_at(seq, cur), read_at(seq, max))) { + max = cur; + } + } + } + + return max; + } +}; + +struct find_minmax_fn { + template Cmp = std::ranges::less> + [[nodiscard]] + constexpr auto operator()(Seq&& seq, Cmp cmp = {}) const + -> minmax_result> + { + auto min = first(seq); + auto max = min; + if (!is_last(seq, min)) { + for (auto cur = next(seq, min); !is_last(seq, cur); inc(seq, cur)) { + auto&& elem = read_at(seq, cur); + + if (std::invoke(cmp, elem, read_at(seq, min))) { + min = cur; + } + if (!std::invoke(cmp, elem, read_at(seq, max))) { + max = cur; + } + } + } + + return {std::move(min), std::move(max)}; + } +}; + +} // namespace detail + +FLUX_EXPORT inline constexpr auto find_min = detail::find_min_fn{}; +FLUX_EXPORT inline constexpr auto find_max = detail::find_max_fn{}; +FLUX_EXPORT inline constexpr auto find_minmax = detail::find_minmax_fn{}; + +template +template + requires strict_weak_order_for +constexpr auto inline_sequence_base::find_min(Cmp cmp) +{ + return flux::find_min(derived(), std::move(cmp)); +} + +template +template + requires strict_weak_order_for +constexpr auto inline_sequence_base::find_max(Cmp cmp) +{ + return flux::find_max(derived(), std::move(cmp)); +} + +template +template + requires strict_weak_order_for +constexpr auto inline_sequence_base::find_minmax(Cmp cmp) +{ + return flux::find_minmax(derived(), std::move(cmp)); +} + +} // namespace flux + +#endif // FLUX_OP_FIND_MIN_MAX_HPP_INCLUDED diff --git a/include/flux/op/minmax.hpp b/include/flux/op/minmax.hpp index df7fd9a3..027c8c2b 100644 --- a/include/flux/op/minmax.hpp +++ b/include/flux/op/minmax.hpp @@ -8,6 +8,7 @@ #include +#include #include namespace flux { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5a8ce7da..12eabfea 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ add_executable(test-libflux test_fill.cpp test_filter.cpp test_find.cpp + test_find_min_max.cpp test_flatten.cpp test_for_each.cpp test_fold.cpp diff --git a/test/test_find_min_max.cpp b/test/test_find_min_max.cpp new file mode 100644 index 00000000..158097a0 --- /dev/null +++ b/test/test_find_min_max.cpp @@ -0,0 +1,149 @@ + +// Copyright (c) 2023 Tristan Brindle (tcbrindle at gmail dot com) +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include "catch.hpp" + +#include + +#include + +#include "test_utils.hpp" + +namespace { + +struct IntPair { + int a, b; + friend bool operator==(IntPair const&, IntPair const&) = default; +}; + + +constexpr bool test_find_min() +{ + // Empty range -> no min value + { + auto seq = flux::empty; + auto cur = flux::find_min(seq); + STATIC_CHECK(seq.is_last(cur)); + } + + // Basic min works as expected + { + auto arr = std::array{5, 4, 3, 2, 1}; + + STATIC_CHECK(flux::read_at(arr, flux::find_min(arr)) == 1); + + auto ref = flux::ref(arr); + STATIC_CHECK(ref[ref.find_min()] == 1); // much better! + } + + // Can use custom comparator and projection + { + IntPair arr[] = { {1, 2}, {3, 4}, {5, 6}}; + + auto cur = flux::find_min(arr, flux::proj(std::greater{}, &IntPair::a)); + + STATIC_CHECK(not flux::is_last(arr, cur)); + STATIC_CHECK(flux::read_at(arr, cur) == IntPair{5, 6}); + + } + + // If several elements are equally minimal, returns the first + { + IntPair arr[] = { {1, 2}, {1, 3}, {1, 4}}; + + auto cur = flux::find_min(arr, flux::proj(std::less{}, &IntPair::a)); + + STATIC_CHECK(not flux::is_last(arr, cur)); + STATIC_CHECK(flux::read_at(arr, cur).b == 2); + } + + return true; +} +static_assert(test_find_min()); + +constexpr bool test_find_max() +{ + // Empty range -> no max value + { + auto seq = flux::filter(std::array{2, 4, 6, 8, 10}, flux::pred::odd); + auto max = seq.find_max(); + STATIC_CHECK(seq.is_last(max)); + } + + // Basic max works as expected + { + auto seq = flux::from(std::array{5, 4, 3, 2, 1}); + auto cur = seq.find_max(); + STATIC_CHECK(cur == 0); + STATIC_CHECK(seq[cur] == 5); + } + + // Can use custom comparator and projection + { + IntPair arr[] = { {1, 2}, {3, 4}, {5, 6}}; + + auto cur = flux::find_max(arr, flux::proj(std::greater{}, &IntPair::a)); + + STATIC_CHECK(flux::read_at(arr, cur) == IntPair{1, 2}); + } + + // If several elements are equally maximal, returns the last + { + IntPair arr[] = { {1, 2}, {1, 3}, {1, 4}}; + + auto cur = flux::find_max(arr, flux::proj(std::less{}, &IntPair::b)); + + STATIC_CHECK(flux::read_at(arr, cur).b == 4); + } + + return true; +} +static_assert(test_find_max()); + +constexpr bool test_find_minmax() +{ + // Empty range -> no minmax + { + auto seq = flux::filter(std::array{2, 4, 6, 8, 10}, flux::pred::odd); + auto [min, max] = seq.find_minmax(); + STATIC_CHECK(seq.is_last(min)); + STATIC_CHECK(seq.is_last(max)); + } + + // Basic minmax works as expected + { + auto seq = flux::from(std::array{5, 4, 3, 2, 1}); + + auto result = seq.find_minmax(); + + STATIC_CHECK(seq[result.min] == 1); + STATIC_CHECK(seq[result.max] == 5); + } + + // Can use custom comparator and projection + { + IntPair arr[] = { {1, 2}, {3, 4}, {5, 6}}; + + auto result = flux::find_minmax(arr, flux::proj(std::greater<>{}, &IntPair::a)); + + + STATIC_CHECK(flux::read_at(arr, result.min) == IntPair{5, 6}); + STATIC_CHECK(flux::read_at(arr, result.max) == IntPair{1, 2}); + } + + // If several elements are equally minimal/maximal, returns the first/last resp. + { + IntPair arr[] = { {1, 2}, {1, 3}, {1, 4}}; + auto [min, max] = flux::find_minmax(arr, flux::proj(std::ranges::less{}, &IntPair::a)); + + STATIC_CHECK(flux::read_at(arr, min) == IntPair{1, 2}); + STATIC_CHECK(flux::read_at(arr, max) == IntPair{1, 4}); + } + + return true; +} +static_assert(test_find_minmax()); + +} diff --git a/test/test_utils.hpp b/test/test_utils.hpp index cc69b79c..4ff0840f 100644 --- a/test/test_utils.hpp +++ b/test/test_utils.hpp @@ -11,7 +11,7 @@ #include #include -#define STATIC_CHECK(...) if (!(__VA_ARGS__)) throw false +#define STATIC_CHECK(...) if (!(__VA_ARGS__)) throw std::runtime_error("Test assertion failed") inline namespace test_utils { From e83bdcb8da0763ce46feaaf22206855c6ea70775 Mon Sep 17 00:00:00 2001 From: Tristan Brindle Date: Mon, 7 Aug 2023 14:49:08 +0100 Subject: [PATCH 2/2] Add find_min/max/minmax documentation --- docs/reference/algorithms.rst | 98 ++++++++++++++++++++++++++++++++++- example/CMakeLists.txt | 3 ++ example/docs/find_max.cpp | 32 ++++++++++++ example/docs/find_min.cpp | 32 ++++++++++++ example/docs/find_minmax.cpp | 34 ++++++++++++ 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 example/docs/find_max.cpp create mode 100644 example/docs/find_min.cpp create mode 100644 example/docs/find_minmax.cpp diff --git a/docs/reference/algorithms.rst b/docs/reference/algorithms.rst index 8be149e8..58f8e8d0 100644 --- a/docs/reference/algorithms.rst +++ b/docs/reference/algorithms.rst @@ -345,6 +345,102 @@ Algorithms requires std::predicate> \ auto find_if_not(Seq&& seq, Pred pred) -> cursor_t; +``find_max`` +------------ + +.. function:: + template Cmp = std::ranges::less> \ + auto find_max(Seq&& seq, Cmp cmp = {}) -> cursor_t; + + Returns a cursor to the maximum element of :var:`seq`, compared using :var:`cmp`. + + If several elements are equally maximal, :func:`find_max` returns a cursor to the **last** such element. + + .. note:: This behaviour differs from :func:`std::max_element()`, which returns an iterator to the *first* maximal element. + + :param seq: A multipass sequence + :param cmp: A comparator to use to find the maximum element, defaulting to :type:`std::ranges::less` + + :returns: A cursor pointing to the maximum element of :var:`seq`. + + :example: + + .. literalinclude:: ../../example/docs/find_max.cpp + :language: cpp + :linenos: + :dedent: + :lines: 10-31 + + :see also: + * `std::ranges::max_element() `_ + * :func:`flux::max` + * :func:`flux::find_minmax` + +``find_min`` +------------ + +.. function:: + template Cmp = std::ranges::less> \ + auto find_min(Seq&& seq, Cmp cmp = {}) -> cursor_t; + + Returns a cursor to the minimum element of :var:`seq`, compared using :var:`cmp`. + + If several elements are equally minimal, :func:`find_min` returns a cursor to the **first** such element. + + :param seq: A multipass sequence + :param cmp: A comparator to use to find the minimum element, defaulting to :type:`std::ranges::less` + + :returns: A cursor pointing to the minimum element of :var:`seq`. + + :example: + + .. literalinclude:: ../../example/docs/find_min.cpp + :language: cpp + :linenos: + :dedent: + :lines: 10-31 + + :see also: + * `std::ranges::min_element() `_ + * :func:`flux::min` + * :func:`flux::minmax` + +``find_minmax`` +--------------- + +.. function:: + template Cmp = std::ranges::less> \ + auto find_minmax(Seq&& seq, Cmp cmp = {}) -> minmax_result>; + + Returns a pair of cursors to the minimum and maximum elements of :var:`seq`, compared using :var:`cmp`. + + If several elements are equally minimal, :func:`find_minmax` returns a cursor to the first. If several elements are equally maximal, :func:`find_minmax` returns a cursor to the last. + + Equivalent to:: + + minmax_element>{.min = find_min(seq, cmp), + .max = find_max(seq, cmp)}; + + but only does a single pass over :var:`seq`. + + :param seq: A multipass sequence + :param cmp: A comparator to use to find the maximum element, defaulting to :type:`std::ranges::less` + + :returns: A cursor pointing to the maximum element of :var:`seq`. + + :example: + + .. literalinclude:: ../../example/docs/find_minmax.cpp + :language: cpp + :linenos: + :dedent: + :lines: 10-33 + + :see also: + * `std::ranges::minmax_element() `_ + * :func:`flux::minmax` + + ``fold`` -------- @@ -408,7 +504,7 @@ Algorithms ``minmax`` ---------- -.. struct:: template minmax_result; +.. struct:: template minmax_result; .. function:: template \ diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 546c58e2..7c429ef2 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -27,6 +27,9 @@ add_example(example-docs-cursors docs/cursors.cpp) add_example(example-docs-cycle docs/cycle.cpp) add_example(example-docs-drop docs/drop.cpp) add_example(example-docs-ends-with docs/ends_with.cpp) +add_example(example-docs-find-max docs/find_max.cpp) +add_example(example-docs-find-min docs/find_min.cpp) +add_example(example-docs-find-minmax docs/find_minmax.cpp) add_example(example-docs-mask docs/mask.cpp) add_example(example-docs-prescan docs/prescan.cpp) add_example(example-docs-read-only docs/read_only.cpp) diff --git a/example/docs/find_max.cpp b/example/docs/find_max.cpp new file mode 100644 index 00000000..5fd78ed2 --- /dev/null +++ b/example/docs/find_max.cpp @@ -0,0 +1,32 @@ + +#include + +#include +#include +#include + +int main() +{ + struct Person { + std::string name; + int age; + }; + + std::vector people{ + {"Alice", 44}, + {"Bob", 63}, + {"Chris", 29}, + {"Dani", 29}, + {"Eddy", 63} + }; + + // Get a cursor to the maximum of the people vector, according to age + auto max_cur = flux::find_max(people, flux::proj(std::less{}, &Person::age)); + + // The oldest person is 63 + assert(flux::read_at(people, max_cur).age == 63); + + // Note that (unlike std::max_element) find_max() return a cursor to the + // *last* of several equally-maximum elements + assert(flux::read_at(people, max_cur).name == "Eddy"); +} \ No newline at end of file diff --git a/example/docs/find_min.cpp b/example/docs/find_min.cpp new file mode 100644 index 00000000..bd15f876 --- /dev/null +++ b/example/docs/find_min.cpp @@ -0,0 +1,32 @@ + +#include + +#include +#include +#include + +int main() +{ + struct Person { + std::string name; + int age; + }; + + std::vector people{ + {"Alice", 44}, + {"Bob", 63}, + {"Chris", 29}, + {"Dani", 29}, + {"Eddy", 63} + }; + + // Get a cursor to the maximum of the people vector, according to age + auto min_cur = flux::find_min(people, flux::proj(std::less{}, &Person::age)); + + // The youngest person is 29 + assert(flux::read_at(people, min_cur).age == 29); + + // Note that find_min() return a cursor to the first of several + // equally-minimum elements + assert(flux::read_at(people, min_cur).name == "Chris"); +} \ No newline at end of file diff --git a/example/docs/find_minmax.cpp b/example/docs/find_minmax.cpp new file mode 100644 index 00000000..9ba185c1 --- /dev/null +++ b/example/docs/find_minmax.cpp @@ -0,0 +1,34 @@ + +#include + +#include +#include +#include + +int main() +{ + struct Person { + std::string name; + int age; + }; + + std::vector people{ + {"Alice", 44}, + {"Bob", 63}, + {"Chris", 29}, + {"Dani", 29}, + {"Eddy", 63} + }; + + // find_minmax() returns a pair of cursors which we can destructure + // Here we'll get the min and max of the people vector, according to age + auto [min, max] = flux::find_minmax(people, flux::proj(std::less{}, &Person::age)); + + // The "minimum" is Chris. Dani is the same age, but Chris appears earlier + // in the sequence + assert(flux::read_at(people, min).name == "Chris"); + + // The "maximum" is Eddy. Bob is the same age, but Eddy appears later in the + // sequence + assert(flux::read_at(people, max).name == "Eddy"); +} \ No newline at end of file