Skip to content

Commit

Permalink
Improve Sequence extensions with cleaner code and performance impro…
Browse files Browse the repository at this point in the history
…vements (SwifterSwift#912)

* Improve `Sequence` extensions with cleaner code and performance improvements
Added `contains(_:)` for `Hashable` elements

* Fix memory allocation for `last(where:)`
Add LinkedList for testing ordered, unidirectional sequence

* PR improvements
  • Loading branch information
guykogus authored Dec 20, 2020
1 parent 157d8e6 commit b9e7401
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 71 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The changelog for **SwifterSwift**. Also see the [releases](https://github.com/S

## Upcoming Release
### Breaking Change
- **Sequence**
- Remove `last(where:)` and move `last(where:equals:)` to `BidirectionalCollection`, since it only makes semantic sense for ordered sequences. [#912](https://github.com/SwifterSwift/SwifterSwift/pull/912) by [guykogus](https://github.com/guykogus)
- **UIView**
- Rename `shadowColor`, `shadowOffset`, `shadowOpacity` and `shadowRadius` to `layerShadowColor`, `layerShadowOffset`, `layerShadowOpacity` and `layerShadowRadius` to avoid naming colisions with subclasses properties defined in other modules e.g. UIKit. [#897](https://github.com/SwifterSwift/SwifterSwift/pull/897) by [LucianoPAlmeida](https://github.com/LucianoPAlmeida)

Expand Down Expand Up @@ -34,6 +36,7 @@ The changelog for **SwifterSwift**. Also see the [releases](https://github.com/S
- **RangeReplaceableCollection**:
- `subscript(offset:)` and `subscript(range:)` to access and replace elements by the index offsets. [#826](https://github.com/SwifterSwift/SwifterSwift/pull/826) by [guykogus](https://github.com/guykogus)
- **Sequence**:
- Added `contains(_:)` for `Hashable` elements for performance improvement. [#912](https://github.com/SwifterSwift/SwifterSwift/pull/912) by [guykogus](https://github.com/guykogus)
- Added `first(where:equals:)` to find the first element of the sequence with having property by given key path equals to given value. [#836](https://github.com/SwifterSwift/SwifterSwift/pull/836) by [hamtiko](https://github.com/hamtiko)
- Added `last(where:equals:)` to find the last element of the sequence with having property by given key path equals to given value. [#838](https://github.com/SwifterSwift/SwifterSwift/pull/838) by [hamtiko](https://github.com/hamtiko)
- **SKNode**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ public extension BidirectionalCollection {
let index = distance >= 0 ? startIndex : endIndex
return self[indices.index(index, offsetBy: distance)]
}

/// SwifterSwift: Returns the last element of the sequence with having property by given key path equals to given `value`.
///
/// - Parameters:
/// - keyPath: The `KeyPath` of property for `Element` to compare.
/// - value: The value to compare with `Element` property
/// - Returns: The last element of the collection that has property by given key path equals to given `value` or `nil` if there is no such element.
func last<T: Equatable>(where keyPath: KeyPath<Element, T>, equals value: T) -> Element? {
return last { $0[keyPath: keyPath] == value }
}
}
14 changes: 14 additions & 0 deletions Sources/SwifterSwift/SwiftStdlib/Deprecated/StdlibDeprecated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,18 @@ public extension Sequence {
func filter(by keyPath: KeyPath<Element, Bool>) -> [Element] {
return filter { $0[keyPath: keyPath] }
}

/// SwifterSwift: Get last element that satisfies a conditon.
///
/// [2, 2, 4, 7].last(where: {$0 % 2 == 0}) -> 4
///
/// - Parameter condition: condition to evaluate each element against.
/// - Returns: the last element in the array matching the specified condition. (optional)
@available(*, deprecated, message: "For an unordered sequence using `last` instead of `first` is equal.")
func last(where condition: (Element) throws -> Bool) rethrows -> Element? {
for element in reversed() {
if try condition(element) { return element }
}
return nil
}
}
72 changes: 25 additions & 47 deletions Sources/SwifterSwift/SwiftStdlib/SequenceExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,6 @@ public extension Sequence {
return try contains { try condition($0) }
}

/// SwifterSwift: Get last element that satisfies a conditon.
///
/// [2, 2, 4, 7].last(where: {$0 % 2 == 0}) -> 4
///
/// - Parameter condition: condition to evaluate each element against.
/// - Returns: the last element in the array matching the specified condition. (optional)
func last(where condition: (Element) throws -> Bool) rethrows -> Element? {
for element in reversed() {
if try condition(element) { return element }
}
return nil
}

/// SwifterSwift: Filter elements based on a rejection condition.
///
/// [2, 2, 4, 7].reject(where: {$0 % 2 == 0}) -> [7]
Expand Down Expand Up @@ -88,9 +75,7 @@ public extension Sequence {
/// - condition: condition to evaluate each element against.
/// - body: a closure that takes an element of the array as a parameter.
func forEach(where condition: (Element) throws -> Bool, body: (Element) throws -> Void) rethrows {
for element in self where try condition(element) {
try body(element)
}
try lazy.filter(condition).forEach(body)
}

/// SwifterSwift: Reduces an array while returning each interim combination.
Expand Down Expand Up @@ -118,12 +103,7 @@ public extension Sequence {
/// - transform: transform element function to evaluate every element.
/// - Returns: Return an filtered and mapped array.
func filtered<T>(_ isIncluded: (Element) throws -> Bool, map transform: (Element) throws -> T) rethrows -> [T] {
return try compactMap {
if try isIncluded($0) {
return try transform($0)
}
return nil
}
return try lazy.filter(isIncluded).map(transform)
}

/// SwifterSwift: Get the only element based on a condition.
Expand Down Expand Up @@ -169,14 +149,13 @@ public extension Sequence {
/// - Returns: A tuple of matched and non-matched items
func divided(by condition: (Element) throws -> Bool) rethrows -> (matching: [Element], nonMatching: [Element]) {
// Inspired by: http://ruby-doc.org/core-2.5.0/Enumerable.html#method-i-partition
var matching = ContiguousArray<Element>()
var nonMatching = ContiguousArray<Element>()
var matching = [Element]()
var nonMatching = [Element]()

var iterator = makeIterator()
while let element = iterator.next() {
for element in self {
try condition(element) ? matching.append(element) : nonMatching.append(element)
}
return (Array(matching), Array(nonMatching))
return (matching, nonMatching)
}

/// SwifterSwift: Return a sorted array based on a key path and a compare function.
Expand Down Expand Up @@ -251,16 +230,6 @@ public extension Sequence {
func first<T: Equatable>(where keyPath: KeyPath<Element, T>, equals value: T) -> Element? {
return first { $0[keyPath: keyPath] == value }
}

/// SwifterSwift: Returns the last element of the sequence with having property by given key path equals to given `value`.
///
/// - Parameters:
/// - keyPath: The `KeyPath` of property for `Element` to compare.
/// - value: The value to compare with `Element` property
/// - Returns: The last element of the collection that has property by given key path equals to given `value` or `nil` if there is no such element.
func last<T: Equatable>(where keyPath: KeyPath<Element, T>, equals value: T) -> Element? {
return last { $0[keyPath: keyPath] == value }
}
}

public extension Sequence where Element: Equatable {
Expand All @@ -272,18 +241,27 @@ public extension Sequence where Element: Equatable {
///
/// - Parameter elements: array of elements to check.
/// - Returns: true if array contains all given items.
/// - Complexity: _O(m·n)_, where _m_ is the length of `elements` and _n_ is the length of this sequence.
func contains(_ elements: [Element]) -> Bool {
guard !elements.isEmpty else { return true }
for element in elements {
if !contains(element) {
return false
}
}
return true
return elements.allSatisfy { contains($0) }
}
}

public extension Sequence where Element: Hashable {
/// SwifterSwift: Check if array contains an array of elements.
///
/// [1, 2, 3, 4, 5].contains([1, 2]) -> true
/// [1.2, 2.3, 4.5, 3.4, 4.5].contains([2, 6]) -> false
/// ["h", "e", "l", "l", "o"].contains(["l", "o"]) -> true
///
/// - Parameter elements: array of elements to check.
/// - Returns: true if array contains all given items.
/// - Complexity: _O(m + n)_, where _m_ is the length of `elements` and _n_ is the length of this sequence.
func contains(_ elements: [Element]) -> Bool {
let set = Set(self)
return elements.allSatisfy { set.contains($0) }
}

/// SwifterSwift: Check whether a sequence contains duplicates.
///
/// - Returns: true if the receiver contains duplicates.
Expand Down Expand Up @@ -316,15 +294,15 @@ public extension Sequence where Element: Hashable {
}
}

// MARK: - Methods (Numeric)
// MARK: - Methods (AdditiveArithmetic)

public extension Sequence where Element: Numeric {
public extension Sequence where Element: AdditiveArithmetic {
/// SwifterSwift: Sum of all elements in array.
///
/// [1, 2, 3, 4, 5].sum() -> 15
///
/// - Returns: sum of the array's elements.
func sum() -> Element {
return reduce(into: 0, +=)
return reduce(.zero, +)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,20 @@ final class BidirectionalCollectionExtensionsTests: XCTestCase {
XCTAssertEqual(arr[offset: 4], 5)
XCTAssertEqual(arr[offset: -2], 4)
}

func testLastByKeyPath() {
let array1 = [
Person(name: "John", age: 30, location: Location(city: "Boston")),
Person(name: "Jan", age: 22, location: nil),
Person(name: "Roman", age: 30, location: Location(city: "Moscow"))
]

let last30Age = array1.last(where: \.age, equals: 30)

XCTAssertEqual(last30Age, array1.last)

let missingPerson = array1.last(where: \.name, equals: "Tom")

XCTAssertNil(missingPerson)
}
}
40 changes: 16 additions & 24 deletions Tests/SwiftStdlibTests/SequenceExtensionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ private enum SequenceTestError: Error {
case closureThrows
}

struct TestValue: Equatable, ExpressibleByIntegerLiteral {
let value: Int

init(integerLiteral value: Int) { self.value = value }
}

final class SequenceExtensionsTests: XCTestCase {
func testAllMatch() {
let collection = [2, 4, 6, 8, 10, 12]
Expand All @@ -23,13 +29,6 @@ final class SequenceExtensionsTests: XCTestCase {
XCTAssert(collection.none { $0 % 2 == 0 })
}

func testLastWhere() {
let array = [1, 1, 2, 1, 1, 1, 2, 1, 4, 1]
let element = array.last { $0 % 2 == 0 }
XCTAssertEqual(element, 4)
XCTAssertNil([Int]().last { $0 % 2 == 0 })
}

func testRejectWhere() {
let input = [1, 2, 3, 4, 5]
let output = input.reject { $0 % 2 == 0 }
Expand Down Expand Up @@ -97,7 +96,16 @@ final class SequenceExtensionsTests: XCTestCase {
XCTAssertEqual(tuple.1, [1, 3, 5])
}

func testContains() {
func testContainsEquatable() {
XCTAssert([TestValue]().contains([]))
XCTAssertFalse([TestValue]().contains([1, 2]))
XCTAssert(([1, 2, 3] as [TestValue]).contains([1, 2]))
XCTAssert(([1, 2, 3] as [TestValue]).contains([2, 3]))
XCTAssert(([1, 2, 3] as [TestValue]).contains([1, 3]))
XCTAssertFalse(([1, 2, 3] as [TestValue]).contains([4, 5]))
}

func testContainsHashable() {
XCTAssert([Int]().contains([]))
XCTAssertFalse([Int]().contains([1, 2]))
XCTAssert([1, 2, 3].contains([1, 2]))
Expand Down Expand Up @@ -190,20 +198,4 @@ final class SequenceExtensionsTests: XCTestCase {

XCTAssertNil(missingPerson)
}

func testLastByKeyPath() {
let array1 = [
Person(name: "John", age: 30, location: Location(city: "Boston")),
Person(name: "Jan", age: 22, location: nil),
Person(name: "Roman", age: 30, location: Location(city: "Moscow"))
]

let last30Age = array1.last(where: \.age, equals: 30)

XCTAssertEqual(last30Age, array1.last)

let missingPerson = array1.last(where: \.name, equals: "Tom")

XCTAssertNil(missingPerson)
}
}

0 comments on commit b9e7401

Please sign in to comment.