From 83acaf24242d195ae497460d9b40c50d8d642921 Mon Sep 17 00:00:00 2001
From: Vitaly Sender <vitaly.sender@gmail.com>
Date: Tue, 3 Aug 2021 14:01:34 -0400
Subject: [PATCH] AllSatisfy operator

- Added an `AllSatisfy` operator.
- Added tests.
- Updated `README`.
---
 CombineExt.xcodeproj/project.pbxproj |  8 ++++
 README.md                            | 33 ++++++++++++++
 Sources/Operators/AllSatisfy.swift   | 54 +++++++++++++++++++++++
 Tests/AllSatisfyTests.swift          | 66 ++++++++++++++++++++++++++++
 4 files changed, 161 insertions(+)
 create mode 100644 Sources/Operators/AllSatisfy.swift
 create mode 100644 Tests/AllSatisfyTests.swift

diff --git a/CombineExt.xcodeproj/project.pbxproj b/CombineExt.xcodeproj/project.pbxproj
index fe9cb70..a96bf8a 100644
--- a/CombineExt.xcodeproj/project.pbxproj
+++ b/CombineExt.xcodeproj/project.pbxproj
@@ -37,6 +37,8 @@
 		C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C387777D24E6BF6C00FAD2D8 /* NwiseTests.swift */; };
 		D836234824EA9446002353AC /* MergeMany.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836234724EA9446002353AC /* MergeMany.swift */; };
 		D836234A24EA9888002353AC /* MergeManyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D836234924EA9888002353AC /* MergeManyTests.swift */; };
+		ECF0134D26B86DC000F1D286 /* AllSatisfy.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF0134C26B86DC000F1D286 /* AllSatisfy.swift */; };
+		ECF0135426B9B79900F1D286 /* AllSatisfyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF0135226B9B50000F1D286 /* AllSatisfyTests.swift */; };
 		OBJ_100 /* ZipMany.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* ZipMany.swift */; };
 		OBJ_101 /* CurrentValueRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* CurrentValueRelay.swift */; };
 		OBJ_102 /* PassthroughRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* PassthroughRelay.swift */; };
@@ -122,6 +124,8 @@
 		"CombineExt::CombineExtTests::Product" /* CombineExtTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = CombineExtTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		D836234724EA9446002353AC /* MergeMany.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeMany.swift; sourceTree = "<group>"; };
 		D836234924EA9888002353AC /* MergeManyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeManyTests.swift; sourceTree = "<group>"; };
+		ECF0134C26B86DC000F1D286 /* AllSatisfy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllSatisfy.swift; sourceTree = "<group>"; };
+		ECF0135226B9B50000F1D286 /* AllSatisfyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllSatisfyTests.swift; sourceTree = "<group>"; };
 		OBJ_10 /* Sink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sink.swift; sourceTree = "<group>"; };
 		OBJ_12 /* Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Optional.swift; sourceTree = "<group>"; };
 		OBJ_14 /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = "<group>"; };
@@ -238,6 +242,7 @@
 			isa = PBXGroup;
 			children = (
 				BF330EF724F2001F001281FC /* Internal */,
+				ECF0134C26B86DC000F1D286 /* AllSatisfy.swift */,
 				OBJ_17 /* Amb.swift */,
 				OBJ_18 /* AssignOwnership.swift */,
 				OBJ_19 /* AssignToMany.swift */,
@@ -286,6 +291,7 @@
 		OBJ_40 /* Tests */ = {
 			isa = PBXGroup;
 			children = (
+				ECF0135226B9B50000F1D286 /* AllSatisfyTests.swift */,
 				OBJ_41 /* AmbTests.swift */,
 				OBJ_42 /* AssignOwnershipTests.swift */,
 				OBJ_43 /* AssignToManyTests.swift */,
@@ -557,6 +563,7 @@
 				OBJ_127 /* CurrentValueRelayTests.swift in Sources */,
 				C387777F24E6BF8F00FAD2D8 /* NwiseTests.swift in Sources */,
 				OBJ_128 /* DematerializeTests.swift in Sources */,
+				ECF0135426B9B79900F1D286 /* AllSatisfyTests.swift in Sources */,
 				OBJ_129 /* FlatMapLatestTests.swift in Sources */,
 				OBJ_130 /* MapManyTests.swift in Sources */,
 				OBJ_131 /* MaterializeTests.swift in Sources */,
@@ -612,6 +619,7 @@
 				BF330EF924F20032001281FC /* Timer.swift in Sources */,
 				OBJ_101 /* CurrentValueRelay.swift in Sources */,
 				OBJ_102 /* PassthroughRelay.swift in Sources */,
+				ECF0134D26B86DC000F1D286 /* AllSatisfy.swift in Sources */,
 				OBJ_103 /* Relay.swift in Sources */,
 				OBJ_104 /* ReplaySubject.swift in Sources */,
 			);
diff --git a/README.md b/README.md
index 3f6b169..efaf80c 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ All operators, utilities and helpers respect Combine's publisher contract, inclu
 * [ignoreFailure](#ignoreFailure)
 * [mapToResult](#mapToResult)
 * [flatMapBatches(of:)](#flatMapBatchesof)
+* [allSatisfy](#allSatisfy)
 
 ### Publishers
 * [AnyPublisher.create](#AnypublisherCreate)
@@ -755,6 +756,38 @@ subscription = ints
 .finished
 ```
 
+------
+
+### allSatisfy
+
+Returns a Boolean value indicating whether every element of a publisher `Collection` satisfies a given predicate.
+
+```swift
+let intArrayPublisher = PassthroughSubject<[Int], Never>()
+
+intArrayPublisher
+  .allSatisfy { $0.isMultiple(of: 2) }
+  .sink { print("All multiples of 2? \($0)") }
+
+intArrayPublisher.send([2, 4, 6, 8, 10])
+intArrayPublisher.send([2, 4, 6, 9, 10])
+
+let names = [Just("John"), Just("Jane"), Just("Jim"), Just("Jill"), Just("Joan")]
+
+names
+  .combineLatest()
+  .allSatisfy { $0.count <= 4 }
+  .sink { print("All short names? \($0)") }
+```
+
+#### Output
+
+```none
+All multiples of 2? true
+All multiples of 2? false
+All short names? true
+```
+
 ## Publishers
 
 This section outlines some of the custom Combine publishers CombineExt provides
diff --git a/Sources/Operators/AllSatisfy.swift b/Sources/Operators/AllSatisfy.swift
new file mode 100644
index 0000000..d7cc94a
--- /dev/null
+++ b/Sources/Operators/AllSatisfy.swift
@@ -0,0 +1,54 @@
+//
+//  AllSatisfy.swift
+//  CombineExt
+//
+//  Created by Vitaly Sender on 2/8/21.
+//  Copyright © 2021 Combine Community. All rights reserved.
+//
+
+#if canImport(Combine)
+import Combine
+
+@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+public extension Publisher where Output: Collection {
+  /// Returns a Boolean value indicating whether every element of a publisher `Collection` satisfies a given predicate.
+  ///
+  /// Example usages:
+  ///
+  ///    ```
+  ///    let intArrayPublisher = PassthroughSubject<[Int], Never>()
+  ///
+  ///    intArrayPublisher
+  ///      .allSatisfy { $0.isMultiple(of: 2) }
+  ///      .sink { print("All multiples of 2? \($0)") }
+  ///
+  ///    intArrayPublisher.send([2, 4, 6, 8, 10])
+  ///    intArrayPublisher.send([2, 4, 6, 9, 10])
+  ///
+  ///    // Output
+  ///    All multiples of 2? true
+  ///    All multiples of 2? false
+  ///    ```
+  ///
+  ///    ```
+  ///    let names = [Just("John"), Just("Jane"), Just("Jim"), Just("Jill"), Just("Joan")]
+  ///
+  ///    names
+  ///      .combineLatest()
+  ///      .allSatisfy { $0.count <= 4 }
+  ///      .sink { print("All short names? \($0)") }
+  ///
+  ///    // Output
+  ///    All short names? true
+  ///    ```
+  ///
+  /// - parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition.
+  ///
+  /// - returns: A publisher that represents whether all elements in the original publisher `Collection` satisfy `predicate`.
+  ///
+  func allSatisfy(_ predicate: @escaping (Output.Element) -> Bool) -> AnyPublisher<Bool, Failure> {
+    map { $0.allSatisfy { predicate($0) } }
+      .eraseToAnyPublisher()
+  }
+}
+#endif
diff --git a/Tests/AllSatisfyTests.swift b/Tests/AllSatisfyTests.swift
new file mode 100644
index 0000000..5321add
--- /dev/null
+++ b/Tests/AllSatisfyTests.swift
@@ -0,0 +1,66 @@
+//
+//  AllSatisfyTests.swift
+//  CombineExt
+//
+//  Created by Vitaly Sender on 3/8/21.
+//
+
+#if !os(watchOS)
+import XCTest
+import Combine
+
+@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+final class AllSatisfyTests: XCTestCase {
+  var subscription: AnyCancellable!
+  
+  func testAllSatisfyWithEmptyArray() {
+    let source = PassthroughSubject<[Int], Never>()
+    var result = false
+    
+    subscription = source
+      .allSatisfy { $0.isMultiple(of: 2) }
+      .sink { result = $0 }
+    
+    XCTAssertFalse(result)
+  }
+  
+  func testAllSatisfyWithSingleElement() {
+    let source = PassthroughSubject<[Int], Never>()
+    var result = false
+    
+    subscription = source
+      .allSatisfy { $0.isMultiple(of: 2) }
+      .sink { result = $0 }
+    
+    source.send([2])
+    
+    XCTAssertTrue(result)
+  }
+  
+  func testAllSatisfyWithMultipleElementsFailing() {
+    let source = PassthroughSubject<[Int], Never>()
+    var result = false
+    
+    subscription = source
+      .allSatisfy { $0.isMultiple(of: 2) }
+      .sink { result = $0 }
+    
+    source.send([1, 4])
+    
+    XCTAssertFalse(result)
+  }
+  
+  func testAllSatisfyWithMultipleElementsSucceeding() {
+    let source = PassthroughSubject<[Int], Never>()
+    var result = false
+    
+    subscription = source
+      .allSatisfy { $0.isMultiple(of: 2) }
+      .sink { result = $0 }
+    
+    source.send([2, 4, 10])
+    
+    XCTAssertTrue(result)
+  }
+}
+#endif