Skip to content

[stdlib] Implement sequence/collection methods for searching from the end #13337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions stdlib/private/StdlibCollectionUnittest/CheckCollectionType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ public struct SuffixFromTest {
}
}

public struct FindLastTest {
public let expected: Int?
public let comparisons: Int
public let element: MinimalEquatableValue
public let sequence: [MinimalEquatableValue]
public let loc: SourceLoc

public init(
expected: Int?, comparisons: Int, element: Int, sequence: [Int],
file: String = #file, line: UInt = #line
) {
self.expected = expected
self.comparisons = comparisons
self.element = MinimalEquatableValue(element)
self.sequence = sequence.enumerated().map {
return MinimalEquatableValue($1, identity: $0)
}
self.loc = SourceLoc(file, line, comment: "test data")
}
}

public let subscriptRangeTests = [
// Slice an empty collection.
SubscriptRangeTest(
Expand Down Expand Up @@ -305,6 +326,68 @@ let removeFirstTests: [RemoveFirstNTest] = [
),
]

let findLastTests = [
FindLastTest(
expected: nil,
comparisons: 0,
element: 42,
sequence: []),

FindLastTest(
expected: nil,
comparisons: 1,
element: 42,
sequence: [ 1010 ]),
FindLastTest(
expected: 0,
comparisons: 1,
element: 1010,
sequence: [ 1010 ]),

FindLastTest(
expected: nil,
comparisons: 2,
element: 42,
sequence: [ 1010, 1010 ]),
FindLastTest(
expected: 1,
comparisons: 1,
element: 1010,
sequence: [ 1010, 1010 ]),

FindLastTest(
expected: nil,
comparisons: 4,
element: 42,
sequence: [ 1010, 2020, 3030, 4040 ]),
FindLastTest(
expected: 0,
comparisons: 4,
element: 1010,
sequence: [ 1010, 2020, 3030, 4040 ]),
FindLastTest(
expected: 1,
comparisons: 3,
element: 2020,
sequence: [ 1010, 2020, 3030, 4040 ]),
FindLastTest(
expected: 2,
comparisons: 2,
element: 3030,
sequence: [ 1010, 2020, 3030, 4040 ]),
FindLastTest(
expected: 3,
comparisons: 1,
element: 4040,
sequence: [ 1010, 2020, 3030, 4040 ]),

FindLastTest(
expected: 3,
comparisons: 2,
element: 2020,
sequence: [ 1010, 2020, 3030, 2020, 4040 ]),
]

extension Collection {
public func nthIndex(_ offset: Int) -> Index {
return self.index(self.startIndex, offsetBy: numericCast(offset))
Expand Down Expand Up @@ -1222,6 +1305,12 @@ extension TestSuite {
return makeCollection(elements.map(wrapValue))
}

func makeWrappedCollectionWithEquatableElement(
_ elements: [MinimalEquatableValue]
) -> CollectionWithEquatableElement {
return makeCollectionOfEquatable(elements.map(wrapValueIntoEquatable))
}

testNamePrefix += String(describing: C.Type.self)

// FIXME: swift-3-indexing-model - add tests for the follow?
Expand Down Expand Up @@ -1254,6 +1343,92 @@ extension TestSuite {
}
}

//===------------------------------------------------------------------===//
// last(where:)
//===------------------------------------------------------------------===//

self.test("\(testNamePrefix).last(where:)/semantics") {
for test in findLastTests {
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
var closureCounter = 0
let closureLifetimeTracker = LifetimeTracked(0)
let found = c.last(where: {
_blackHole(closureLifetimeTracker)
closureCounter += 1
return $0 == wrapValueIntoEquatable(test.element)
})
expectEqual(
test.expected == nil ? nil : wrapValueIntoEquatable(test.element),
found,
stackTrace: SourceLocStack().with(test.loc))
expectEqual(
test.comparisons,
closureCounter,
stackTrace: SourceLocStack().with(test.loc))
if let expectedIdentity = test.expected {
expectEqual(
expectedIdentity, extractValueFromEquatable(found!).identity,
"last(where:) should find only the first element matching its predicate")
}
}
}

//===------------------------------------------------------------------===//
// lastIndex(of:)/lastIndex(where:)
//===------------------------------------------------------------------===//

self.test("\(testNamePrefix).lastIndex(of:)/semantics") {
for test in findLastTests {
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
MinimalEquatableValue.timesEqualEqualWasCalled = 0
let wrappedElement = wrapValueIntoEquatable(test.element)
var result = c.lastIndex(of: wrappedElement)
expectType(
Optional<CollectionWithEquatableElement.Index>.self,
&result)
let zeroBasedIndex = result.map {
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a long version of saying Int(c.distance(from: c.startIndex, to: $0)), isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and it should be Int anyway, since the deprecation of Collection.IndexDistance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pervasive in these files, from when IndexDistance was a thing. I'll fix 'em in a follow-up.

}
expectEqual(
test.expected,
zeroBasedIndex,
stackTrace: SourceLocStack().with(test.loc))
if wrappedElement is MinimalEquatableValue {
expectEqual(
test.comparisons,
MinimalEquatableValue.timesEqualEqualWasCalled,
stackTrace: SourceLocStack().with(test.loc))
}
}
}

self.test("\(testNamePrefix).lastIndex(where:)/semantics") {
for test in findLastTests {
let closureLifetimeTracker = LifetimeTracked(0)
expectEqual(1, LifetimeTracked.instances)
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
var closureCounter = 0
let result = c.lastIndex(where: {
(candidate) in
_blackHole(closureLifetimeTracker)
closureCounter += 1
return
extractValueFromEquatable(candidate).value == test.element.value
})
let zeroBasedIndex = result.map {
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
}
expectEqual(
test.expected,
zeroBasedIndex,
stackTrace: SourceLocStack().with(test.loc))
expectEqual(
test.comparisons,
closureCounter,
stackTrace: SourceLocStack().with(test.loc))
}
}

//===------------------------------------------------------------------===//
// removeLast()/slice
//===------------------------------------------------------------------===//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public struct FindTest {

public init(
expected: Int?, element: Int, sequence: [Int],
expectedLeftoverSequence: [Int],
expectedLeftoverSequence: [Int] = [],
file: String = #file, line: UInt = #line
) {
self.expected = expected
Expand Down
6 changes: 6 additions & 0 deletions stdlib/public/core/ClosedRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ where Bound : Strideable, Bound.Stride : SignedInteger
return lowerBound <= element && element <= upperBound
? .inRange(element) : nil
}

@inlinable
public func _customLastIndexOfEquatableElement(_ element: Bound) -> Index?? {
// The first and last elements are the same because each element is unique.
return _customIndexOfEquatableElement(element)
}
}

extension Comparable {
Expand Down
32 changes: 30 additions & 2 deletions stdlib/public/core/Collection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,8 @@ public protocol Collection: Sequence where SubSequence: Collection {
/// of the collection.
var count: Int { get }

// The following requirements enable dispatching for firstIndex(of:) when
// the element type is Equatable.
// The following requirements enable dispatching for firstIndex(of:) and
// lastIndex(of:) when the element type is Equatable.

/// Returns `Optional(Optional(index))` if an element was found
/// or `Optional(nil)` if an element was determined to be missing;
Expand All @@ -631,6 +631,18 @@ public protocol Collection: Sequence where SubSequence: Collection {
/// - Complexity: O(*n*)
func _customIndexOfEquatableElement(_ element: Element) -> Index??

/// Customization point for `Collection.lastIndex(of:)`.
///
/// Define this method if the collection can find an element in less than
/// O(*n*) by exploiting collection-specific knowledge.
///
/// - Returns: `nil` if a linear search should be attempted instead,
/// `Optional(nil)` if the element was not found, or
/// `Optional(Optional(index))` if an element was found.
///
/// - Complexity: Hopefully less than O(`count`).
func _customLastIndexOfEquatableElement(_ element: Element) -> Index??

/// The first element of the collection.
///
/// If the collection is empty, the value of this property is `nil`.
Expand Down Expand Up @@ -1196,6 +1208,22 @@ extension Collection {
func _customIndexOfEquatableElement(_: Iterator.Element) -> Index?? {
return nil
}

/// Customization point for `Collection.lastIndex(of:)`.
///
/// Define this method if the collection can find an element in less than
/// O(*n*) by exploiting collection-specific knowledge.
///
/// - Returns: `nil` if a linear search should be attempted instead,
/// `Optional(nil)` if the element was not found, or
/// `Optional(Optional(index))` if an element was found.
///
/// - Complexity: Hopefully less than O(`count`).
@inlinable
public // dispatching
func _customLastIndexOfEquatableElement(_ element: Element) -> Index?? {
return nil
}
}

//===----------------------------------------------------------------------===//
Expand Down
91 changes: 91 additions & 0 deletions stdlib/public/core/CollectionAlgorithms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,97 @@ extension Collection {
}
}

//===----------------------------------------------------------------------===//
// lastIndex(of:)/lastIndex(where:)
//===----------------------------------------------------------------------===//

extension BidirectionalCollection {
/// Returns the last element of the sequence that satisfies the given
/// predicate.
///
/// This example uses the `last(where:)` method to find the last
/// negative number in an array of integers:
///
/// let numbers = [3, 7, 4, -2, 9, -6, 10, 1]
/// if let lastNegative = numbers.last(where: { $0 < 0 }) {
/// print("The last negative number is \(firstNegative).")
/// }
/// // Prints "The last negative number is -6."
///
/// - Parameter predicate: A closure that takes an element of the sequence as
/// its argument and returns a Boolean value indicating whether the
/// element is a match.
/// - Returns: The last element of the sequence that satisfies `predicate`,
/// or `nil` if there is no element that satisfies `predicate`.
@inlinable
public func last(
where predicate: (Element) throws -> Bool
) rethrows -> Element? {
return try lastIndex(where: predicate).map { self[$0] }
}

/// Returns the index of the last element in the collection that matches the
/// given predicate.
///
/// You can use the predicate to find an element of a type that doesn't
/// conform to the `Equatable` protocol or to find an element that matches
/// particular criteria. This example finds the index of the last name that
/// begins with the letter "A":
///
/// let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
/// if let i = students.lastIndex(where: { $0.hasPrefix("A") }) {
/// print("\(students[i]) starts with 'A'!")
/// }
/// // Prints "Akosua starts with 'A'!"
///
/// - Parameter predicate: A closure that takes an element as its argument
/// and returns a Boolean value that indicates whether the passed element
/// represents a match.
/// - Returns: The index of the last element in the collection that matches
/// `predicate`, or `nil` if no elements match.
@inlinable
public func lastIndex(
where predicate: (Element) throws -> Bool
) rethrows -> Index? {
var i = endIndex
while i != startIndex {
formIndex(before: &i)
if try predicate(self[i]) {
return i
}
}
return nil
}
}

extension BidirectionalCollection where Element : Equatable {
/// Returns the last index where the specified value appears in the
/// collection.
///
/// After using `lastIndex(of:)` to find the position of the last instance of
/// a particular element in a collection, you can use it to access the
/// element by subscripting. This example shows how you can modify one of
/// the names in an array of students.
///
/// var students = ["Ben", "Ivy", "Jordell", "Ben", "Maxime"]
/// if let i = students.lastIndex(of: "Ben") {
/// students[i] = "Benjamin"
/// }
/// print(students)
/// // Prints "["Ben", "Ivy", "Jordell", "Benjamin", "Max"]"
///
/// - Parameter element: An element to search for in the collection.
/// - Returns: The last index where `element` is found. If `element` is not
/// found in the collection, returns `nil`.
@inlinable
public func lastIndex(of element: Element) -> Index? {
if let result = _customLastIndexOfEquatableElement(element) {
return result
}
return lastIndex(where: { $0 == element })
}
}

//===----------------------------------------------------------------------===//
// partition(by:)
//===----------------------------------------------------------------------===//
Expand Down
6 changes: 6 additions & 0 deletions stdlib/public/core/Dictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1262,6 +1262,12 @@ extension Dictionary {
return Optional(_variantBuffer.index(forKey: element))
}

@inlinable // FIXME(sil-serialize-all)
public func _customLastIndexOfEquatableElement(_ element: Element) -> Index?? {
// The first and last elements are the same because each element is unique.
return _customIndexOfEquatableElement(element)
}

@inlinable // FIXME(sil-serialize-all)
public static func ==(lhs: Keys, rhs: Keys) -> Bool {
// Equal if the two dictionaries share storage.
Expand Down
1 change: 1 addition & 0 deletions stdlib/public/core/ExistentialCollection.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ internal class _AnyRandomAccessCollectionBox<Element>
// TODO: swift-3-indexing-model: forward the following methods.
/*
func _customIndexOfEquatableElement(element: Element) -> Index??
func _customLastIndexOfEquatableElement(element: Element) -> Index??
*/

@inlinable // FIXME(sil-serialize-all)
Expand Down
Loading