From 4fa7a37750e2005af3e8a6d3eee37cfea3647ad0 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Thu, 17 May 2018 21:04:37 +0200 Subject: [PATCH] Implement QPromise>::filter(filterer) Add a new method that iterates over all the promise values (i.e. `Sequence`) and filters the sequence to another using the given `filterer` function. If `filterer` returns `true`, a copy of the item is put in the `output` sequence, otherwise, the item will not appear in `output`. Also provide a static helper to directly filter values (`QtPromise::filter(values, filterer)`). --- docs/SUMMARY.md | 2 + docs/qtpromise/api-reference.md | 2 + docs/qtpromise/helpers/filter.md | 44 +++++ docs/qtpromise/qpromise/filter.md | 45 ++++++ src/qtpromise/qpromise.h | 3 + src/qtpromise/qpromise.inl | 9 ++ src/qtpromise/qpromisehelpers.h | 20 +++ .../auto/qtpromise/helpers/filter/filter.pro | 4 + .../qtpromise/helpers/filter/tst_filter.cpp | 147 +++++++++++++++++ tests/auto/qtpromise/helpers/helpers.pro | 1 + .../auto/qtpromise/qpromise/filter/filter.pro | 4 + .../qtpromise/qpromise/filter/tst_filter.cpp | 152 ++++++++++++++++++ tests/auto/qtpromise/qpromise/qpromise.pro | 1 + 13 files changed, 434 insertions(+) create mode 100644 docs/qtpromise/helpers/filter.md create mode 100644 docs/qtpromise/qpromise/filter.md create mode 100644 tests/auto/qtpromise/helpers/filter/filter.pro create mode 100644 tests/auto/qtpromise/helpers/filter/tst_filter.cpp create mode 100644 tests/auto/qtpromise/qpromise/filter/filter.pro create mode 100644 tests/auto/qtpromise/qpromise/filter/tst_filter.cpp diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b472a19..3086aef 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -6,6 +6,7 @@ * [QPromise](qtpromise/qpromise/constructor.md) * [.delay](qtpromise/qpromise/delay.md) * [.fail](qtpromise/qpromise/fail.md) + * [.filter](qtpromise/qpromise/filter.md) * [.finally](qtpromise/qpromise/finally.md) * [.isFulfilled](qtpromise/qpromise/isfulfilled.md) * [.isPending](qtpromise/qpromise/ispending.md) @@ -21,4 +22,5 @@ * [::resolve (static)](qtpromise/qpromise/resolve.md) * [qPromise](qtpromise/helpers/qpromise.md) * [qPromiseAll](qtpromise/helpers/qpromiseall.md) + * [QtPromise::filter](qtpromise/helpers/filter.md) * [QtPromise::map](qtpromise/helpers/map.md) diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md index 483b305..e32d3db 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -5,6 +5,7 @@ * [`QPromise::QPromise`](qpromise/constructor.md) * [`QPromise::delay`](qpromise/delay.md) * [`QPromise::fail`](qpromise/fail.md) +* [`QPromise::filter`](qpromise/filter.md) * [`QPromise::finally`](qpromise/finally.md) * [`QPromise::isFulfilled`](qpromise/isfulfilled.md) * [`QPromise::isPending`](qpromise/ispending.md) @@ -26,4 +27,5 @@ * [`qPromise`](helpers/qpromise.md) * [`qPromiseAll`](helpers/qpromiseall.md) +* [`QtPromise::filter`](helpers/filter.md) * [`QtPromise::map`](helpers/map.md) diff --git a/docs/qtpromise/helpers/filter.md b/docs/qtpromise/helpers/filter.md new file mode 100644 index 0000000..257d280 --- /dev/null +++ b/docs/qtpromise/helpers/filter.md @@ -0,0 +1,44 @@ +## `QtPromise::filter` + +```cpp +QtPromise::filter(Sequence values, Filterer filterer) -> QPromise> + +// With: +// - Sequence: STL compatible container (e.g. QVector, etc.) +// - Filterer: Function(T value, int index) -> bool +``` + +Iterates over `values` and [filters the sequence](https://en.wikipedia.org/wiki/Filter_%28higher-order_function%29) +to another using the given `filterer` function. If `filterer` returns `true`, a copy of the item +is put in the `output` sequence, otherwise, the item will not appear in `output`. If `filterer` +throws, `output` is rejected with the new exception. + +If `filterer` returns a promise (or `QFuture`), the `output` promise is delayed until all the +promises are resolved. If any of the promises fail, `output` immediately rejects with the error +of the promise that rejected, whether or not the other promises are resolved. + +```cpp +auto output = QtPromise::filter(QVector{ + QUrl("http://a..."), + QUrl("http://b..."), + QUrl("http://c...") +}, [](const QUrl& url, ...) { + return QPromise([&](auto resolve, auto reject) { + // resolve(true) if 'url' is reachable, else resolve(false) + // {...} + }); +}); + +// 'output' resolves as soon as all promises returned by +// 'filterer' are fulfilled or at least one is rejected. + +// 'output' type: QPromise> +output.then([](const QVector& res) { + // 'res' contains only reachable URLs +}); +``` + +> **Note:** the order of the output sequence values is guarantee to be the same as the original +sequence, regardless of completion order of the promises returned by `filterer`. + +See also: [`QPromise::filter`](../qpromise/filter.md) diff --git a/docs/qtpromise/qpromise/filter.md b/docs/qtpromise/qpromise/filter.md new file mode 100644 index 0000000..f350b28 --- /dev/null +++ b/docs/qtpromise/qpromise/filter.md @@ -0,0 +1,45 @@ +## `QPromise>::filter` + +> **Important:** applies only to promise with sequence value. + +```cpp +QPromise>::filter(Filter filterer) -> QPromise> + +// With: +// - Sequence: STL compatible container (e.g. QVector, etc.) +// - Filterer: Function(T value, int index) -> bool +``` + +Iterates over all the promise values (i.e. `Sequence`) and [filters the sequence](https://en.wikipedia.org/wiki/Filter_%28higher-order_function%29) +to another using the given `filterer` function. If `filterer` returns `true`, a copy of the item +is put in the `output` sequence, otherwise, the item will not appear in `output`. If `filterer` +throws, `output` is rejected with the new exception. + +If `filterer` returns a promise (or `QFuture`), the `output` promise is delayed until all the +promises are resolved. If any of the promises fail, `output` immediately rejects with the error +of the promise that rejected, whether or not the other promises are resolved. + +```cpp +QPromise> input = {...} + +auto output = input.filter([](const QUrl& url, ...) { + return url.isValid(); // Keep only valid URLs +}).filter([](const QUrl& url, ...) { + return QPromise([&](auto resolve, auto reject) { + // resolve(true) if `url` is reachable, else resolve(false) + }); +}); + +// 'output' resolves as soon as all promises returned by +// 'filterer' are fulfilled or at least one is rejected. + +// 'output' type: QPromise> +output.then([](const QList& res) { + // 'res' contains only reachable URLs +}); +``` + +> **Note:** the order of the output sequence values is guarantee to be the same as the original +sequence, regardless of completion order of the promises returned by `filterer`. + +See also: [`QtPromise::filter`](../helpers/filter.md) diff --git a/src/qtpromise/qpromise.h b/src/qtpromise/qpromise.h index 5f9272d..1c4740e 100644 --- a/src/qtpromise/qpromise.h +++ b/src/qtpromise/qpromise.h @@ -88,6 +88,9 @@ public: template QPromise(F&& resolver): QPromiseBase(std::forward(resolver)) { } + template + inline QPromise filter(Functor fn); + template inline typename QtPromisePrivate::PromiseMapper::PromiseType map(Functor fn); diff --git a/src/qtpromise/qpromise.inl b/src/qtpromise/qpromise.inl index 1579213..b1afa7d 100644 --- a/src/qtpromise/qpromise.inl +++ b/src/qtpromise/qpromise.inl @@ -155,6 +155,15 @@ inline QPromise QPromiseBase::reject(E&& error) }); } +template +template +inline QPromise QPromise::filter(Functor fn) +{ + return this->then([=](const T& values) { + return QtPromise::filter(values, fn); + }); +} + template template inline typename QtPromisePrivate::PromiseMapper::PromiseType diff --git a/src/qtpromise/qpromisehelpers.h b/src/qtpromise/qpromisehelpers.h index 92bb128..463b2f1 100644 --- a/src/qtpromise/qpromisehelpers.h +++ b/src/qtpromise/qpromisehelpers.h @@ -63,6 +63,26 @@ map(const Sequence& values, Functor fn) return QPromise::all(promises); } +template +static inline QPromise filter(const Sequence& values, Functor fn) +{ + return QtPromise::map(values, fn) + .then([=](const QVector& filters) { + Sequence filtered; + + auto filter = filters.begin(); + for (auto& value : values) { + if (*filter) { + filtered.push_back(std::move(value)); + } + + filter++; + } + + return filtered; + }); +} + } // namespace QtPromise #endif // QTPROMISE_QPROMISEHELPERS_H diff --git a/tests/auto/qtpromise/helpers/filter/filter.pro b/tests/auto/qtpromise/helpers/filter/filter.pro new file mode 100644 index 0000000..e006799 --- /dev/null +++ b/tests/auto/qtpromise/helpers/filter/filter.pro @@ -0,0 +1,4 @@ +TARGET = tst_helpers_filter +SOURCES += $$PWD/tst_filter.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/helpers/filter/tst_filter.cpp b/tests/auto/qtpromise/helpers/filter/tst_filter.cpp new file mode 100644 index 0000000..53e2ea3 --- /dev/null +++ b/tests/auto/qtpromise/helpers/filter/tst_filter.cpp @@ -0,0 +1,147 @@ +// Tests +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_helpers_filter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void emptySequence(); + void filterValues(); + void delayedFulfilled(); + void delayedRejected(); + void functorThrows(); + void functorArguments(); + void preserveOrder(); + void sequenceTypes(); +}; + +QTEST_MAIN(tst_helpers_filter) +#include "tst_filter.moc" + +namespace { + +template +struct SequenceTester +{ + static void exec() + { + auto inputs = Sequence{42, 43, 44, 45, 46, 47, 48, 49, 50, 51}; + auto p = QtPromise::filter(inputs, [](int v, ...) { + return v % 3 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(waitForValue(p, Sequence()), Sequence({42, 45, 48, 51})); + } +}; + +} // anonymous namespace + +#include + +void tst_helpers_filter::emptySequence() +{ + auto p = QtPromise::filter(QVector{}, [](int v, ...) { + return v % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector{}); +} + +void tst_helpers_filter::filterValues() +{ + auto p = QtPromise::filter(QVector{42, 43, 44}, [](int v, ...) { + return v % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); +} + +void tst_helpers_filter::delayedFulfilled() +{ + auto p = QtPromise::filter(QVector{42, 43, 44}, [](int v, ...) { + return QPromise([&](const QPromiseResolve& resolve) { + QtPromisePrivate::qtpromise_defer([=]() { + resolve(v % 2 == 0); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); +} + +void tst_helpers_filter::delayedRejected() +{ + auto p = QtPromise::filter(QVector{42, 43, 44}, [](int v, ...) { + return QPromise([&]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) { + QtPromisePrivate::qtpromise_defer([=]() { + if (v == 44) { + reject(QString("foo")); + } + resolve(true); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_helpers_filter::functorThrows() +{ + auto p = QtPromise::filter(QVector{42, 43, 44}, [](int v, ...) { + if (v == 44) { + throw QString("foo"); + } + return true; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_helpers_filter::functorArguments() +{ + QMap args; + auto p = QtPromise::filter(QVector{42, 43, 44}, [&](int v, int i) { + args[v] = i; + return i % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); + QMap expected{{42, 0}, {43, 1}, {44, 2}}; + QCOMPARE(args, expected); +} + +void tst_helpers_filter::preserveOrder() +{ + auto p = QtPromise::filter(QVector{500, 100, 300, 250, 400}, [](int v, ...) { + return QPromise::resolve(v > 200).delay(v); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({500, 300, 250, 400})); +} + +void tst_helpers_filter::sequenceTypes() +{ + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); +} diff --git a/tests/auto/qtpromise/helpers/helpers.pro b/tests/auto/qtpromise/helpers/helpers.pro index bc86a22..5e340f1 100644 --- a/tests/auto/qtpromise/helpers/helpers.pro +++ b/tests/auto/qtpromise/helpers/helpers.pro @@ -1,6 +1,7 @@ TEMPLATE = subdirs SUBDIRS += \ all \ + filter \ map \ reject \ resolve diff --git a/tests/auto/qtpromise/qpromise/filter/filter.pro b/tests/auto/qtpromise/qpromise/filter/filter.pro new file mode 100644 index 0000000..6d55b0f --- /dev/null +++ b/tests/auto/qtpromise/qpromise/filter/filter.pro @@ -0,0 +1,4 @@ +TARGET = tst_qpromise_filter +SOURCES += $$PWD/tst_filter.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/qpromise/filter/tst_filter.cpp b/tests/auto/qtpromise/qpromise/filter/tst_filter.cpp new file mode 100644 index 0000000..e033fea --- /dev/null +++ b/tests/auto/qtpromise/qpromise/filter/tst_filter.cpp @@ -0,0 +1,152 @@ +// Tests +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_qpromise_filter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void emptySequence(); + void filterValues(); + void delayedFulfilled(); + void delayedRejected(); + void functorThrows(); + void functorArguments(); + void preserveOrder(); + void sequenceTypes(); +}; + +QTEST_MAIN(tst_qpromise_filter) +#include "tst_filter.moc" + +namespace { + +template +struct SequenceTester +{ + static void exec() + { + auto p = QtPromise::qPromise(Sequence{ + 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 + }).filter([](int v, ...) { + return v > 42 && v < 51; + }).filter([](int, int i) { + return QPromise::resolve(i % 2 == 0); + }).filter([](int v, ...) { + return v != 45; + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(waitForValue(p, Sequence()), Sequence({43, 47, 49})); + } +}; + +} // anonymous namespace + +#include + +void tst_qpromise_filter::emptySequence() +{ + auto p = QPromise>::resolve({}).filter([](int v, ...) { + return v % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector{}); +} + +void tst_qpromise_filter::filterValues() +{ + auto p = QPromise>::resolve({42, 43, 44}).filter([](int v, ...) { + return v % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); +} + +void tst_qpromise_filter::delayedFulfilled() +{ + auto p = QPromise>::resolve({42, 43, 44}).filter([](int v, ...) { + return QPromise([&](const QPromiseResolve& resolve) { + QtPromisePrivate::qtpromise_defer([=]() { + resolve(v % 2 == 0); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); +} + +void tst_qpromise_filter::delayedRejected() +{ + auto p = QPromise>::resolve({42, 43, 44}).filter([](int v, ...) { + return QPromise([&]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) { + QtPromisePrivate::qtpromise_defer([=]() { + if (v == 43) { + reject(QString("foo")); + } + resolve(true); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_qpromise_filter::functorThrows() +{ + auto p = QPromise>::resolve({42, 43, 44}).filter([](int v, ...) { + if (v == 43) { + throw QString("foo"); + } + return true; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_qpromise_filter::functorArguments() +{ + QMap args; + auto p = QPromise>::resolve({42, 43, 44}).filter([&](int v, int i) { + args[v] = i; + return i % 2 == 0; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 44})); + QMap expected{{42, 0}, {43, 1}, {44, 2}}; + QCOMPARE(args, expected); +} + +void tst_qpromise_filter::preserveOrder() +{ + auto p = QPromise>::resolve({250, 50, 100, 400, 300}).filter([](int v, ...) { + return QPromise::resolve(v > 200).delay(v); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({250, 400, 300})); +} + +void tst_qpromise_filter::sequenceTypes() +{ + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); +} diff --git a/tests/auto/qtpromise/qpromise/qpromise.pro b/tests/auto/qtpromise/qpromise/qpromise.pro index 9c84a2b..e555dd0 100644 --- a/tests/auto/qtpromise/qpromise/qpromise.pro +++ b/tests/auto/qtpromise/qpromise/qpromise.pro @@ -3,6 +3,7 @@ SUBDIRS += \ construct \ delay \ fail \ + filter \ finally \ map \ operators \