Skip to content

Commit b1ab7d8

Browse files
authored
[stdlib] Implement sequence/collection methods for searching from the end (#13337)
This implements the new last(where:), and lastIndex(of/where:) methods as extensions on `BidirectionalCollection`, which partially implements SE-204. The protocol requirements for `Sequence` and `Collection` as described in the proposal need to wait until there's a solution for picking up the specialized versions in types that conditionally conform to `BidirectionalCollection`.
1 parent 06c528d commit b1ab7d8

File tree

11 files changed

+347
-7
lines changed

11 files changed

+347
-7
lines changed

stdlib/private/StdlibCollectionUnittest/CheckCollectionType.swift

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,27 @@ public struct SuffixFromTest {
110110
}
111111
}
112112

113+
public struct FindLastTest {
114+
public let expected: Int?
115+
public let comparisons: Int
116+
public let element: MinimalEquatableValue
117+
public let sequence: [MinimalEquatableValue]
118+
public let loc: SourceLoc
119+
120+
public init(
121+
expected: Int?, comparisons: Int, element: Int, sequence: [Int],
122+
file: String = #file, line: UInt = #line
123+
) {
124+
self.expected = expected
125+
self.comparisons = comparisons
126+
self.element = MinimalEquatableValue(element)
127+
self.sequence = sequence.enumerated().map {
128+
return MinimalEquatableValue($1, identity: $0)
129+
}
130+
self.loc = SourceLoc(file, line, comment: "test data")
131+
}
132+
}
133+
113134
public let subscriptRangeTests = [
114135
// Slice an empty collection.
115136
SubscriptRangeTest(
@@ -305,6 +326,68 @@ let removeFirstTests: [RemoveFirstNTest] = [
305326
),
306327
]
307328

329+
let findLastTests = [
330+
FindLastTest(
331+
expected: nil,
332+
comparisons: 0,
333+
element: 42,
334+
sequence: []),
335+
336+
FindLastTest(
337+
expected: nil,
338+
comparisons: 1,
339+
element: 42,
340+
sequence: [ 1010 ]),
341+
FindLastTest(
342+
expected: 0,
343+
comparisons: 1,
344+
element: 1010,
345+
sequence: [ 1010 ]),
346+
347+
FindLastTest(
348+
expected: nil,
349+
comparisons: 2,
350+
element: 42,
351+
sequence: [ 1010, 1010 ]),
352+
FindLastTest(
353+
expected: 1,
354+
comparisons: 1,
355+
element: 1010,
356+
sequence: [ 1010, 1010 ]),
357+
358+
FindLastTest(
359+
expected: nil,
360+
comparisons: 4,
361+
element: 42,
362+
sequence: [ 1010, 2020, 3030, 4040 ]),
363+
FindLastTest(
364+
expected: 0,
365+
comparisons: 4,
366+
element: 1010,
367+
sequence: [ 1010, 2020, 3030, 4040 ]),
368+
FindLastTest(
369+
expected: 1,
370+
comparisons: 3,
371+
element: 2020,
372+
sequence: [ 1010, 2020, 3030, 4040 ]),
373+
FindLastTest(
374+
expected: 2,
375+
comparisons: 2,
376+
element: 3030,
377+
sequence: [ 1010, 2020, 3030, 4040 ]),
378+
FindLastTest(
379+
expected: 3,
380+
comparisons: 1,
381+
element: 4040,
382+
sequence: [ 1010, 2020, 3030, 4040 ]),
383+
384+
FindLastTest(
385+
expected: 3,
386+
comparisons: 2,
387+
element: 2020,
388+
sequence: [ 1010, 2020, 3030, 2020, 4040 ]),
389+
]
390+
308391
extension Collection {
309392
public func nthIndex(_ offset: Int) -> Index {
310393
return self.index(self.startIndex, offsetBy: numericCast(offset))
@@ -1222,6 +1305,12 @@ extension TestSuite {
12221305
return makeCollection(elements.map(wrapValue))
12231306
}
12241307

1308+
func makeWrappedCollectionWithEquatableElement(
1309+
_ elements: [MinimalEquatableValue]
1310+
) -> CollectionWithEquatableElement {
1311+
return makeCollectionOfEquatable(elements.map(wrapValueIntoEquatable))
1312+
}
1313+
12251314
testNamePrefix += String(describing: C.Type.self)
12261315

12271316
// FIXME: swift-3-indexing-model - add tests for the follow?
@@ -1254,6 +1343,92 @@ extension TestSuite {
12541343
}
12551344
}
12561345

1346+
//===------------------------------------------------------------------===//
1347+
// last(where:)
1348+
//===------------------------------------------------------------------===//
1349+
1350+
self.test("\(testNamePrefix).last(where:)/semantics") {
1351+
for test in findLastTests {
1352+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1353+
var closureCounter = 0
1354+
let closureLifetimeTracker = LifetimeTracked(0)
1355+
let found = c.last(where: {
1356+
_blackHole(closureLifetimeTracker)
1357+
closureCounter += 1
1358+
return $0 == wrapValueIntoEquatable(test.element)
1359+
})
1360+
expectEqual(
1361+
test.expected == nil ? nil : wrapValueIntoEquatable(test.element),
1362+
found,
1363+
stackTrace: SourceLocStack().with(test.loc))
1364+
expectEqual(
1365+
test.comparisons,
1366+
closureCounter,
1367+
stackTrace: SourceLocStack().with(test.loc))
1368+
if let expectedIdentity = test.expected {
1369+
expectEqual(
1370+
expectedIdentity, extractValueFromEquatable(found!).identity,
1371+
"last(where:) should find only the first element matching its predicate")
1372+
}
1373+
}
1374+
}
1375+
1376+
//===------------------------------------------------------------------===//
1377+
// lastIndex(of:)/lastIndex(where:)
1378+
//===------------------------------------------------------------------===//
1379+
1380+
self.test("\(testNamePrefix).lastIndex(of:)/semantics") {
1381+
for test in findLastTests {
1382+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1383+
MinimalEquatableValue.timesEqualEqualWasCalled = 0
1384+
let wrappedElement = wrapValueIntoEquatable(test.element)
1385+
var result = c.lastIndex(of: wrappedElement)
1386+
expectType(
1387+
Optional<CollectionWithEquatableElement.Index>.self,
1388+
&result)
1389+
let zeroBasedIndex = result.map {
1390+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1391+
}
1392+
expectEqual(
1393+
test.expected,
1394+
zeroBasedIndex,
1395+
stackTrace: SourceLocStack().with(test.loc))
1396+
if wrappedElement is MinimalEquatableValue {
1397+
expectEqual(
1398+
test.comparisons,
1399+
MinimalEquatableValue.timesEqualEqualWasCalled,
1400+
stackTrace: SourceLocStack().with(test.loc))
1401+
}
1402+
}
1403+
}
1404+
1405+
self.test("\(testNamePrefix).lastIndex(where:)/semantics") {
1406+
for test in findLastTests {
1407+
let closureLifetimeTracker = LifetimeTracked(0)
1408+
expectEqual(1, LifetimeTracked.instances)
1409+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1410+
var closureCounter = 0
1411+
let result = c.lastIndex(where: {
1412+
(candidate) in
1413+
_blackHole(closureLifetimeTracker)
1414+
closureCounter += 1
1415+
return
1416+
extractValueFromEquatable(candidate).value == test.element.value
1417+
})
1418+
let zeroBasedIndex = result.map {
1419+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1420+
}
1421+
expectEqual(
1422+
test.expected,
1423+
zeroBasedIndex,
1424+
stackTrace: SourceLocStack().with(test.loc))
1425+
expectEqual(
1426+
test.comparisons,
1427+
closureCounter,
1428+
stackTrace: SourceLocStack().with(test.loc))
1429+
}
1430+
}
1431+
12571432
//===------------------------------------------------------------------===//
12581433
// removeLast()/slice
12591434
//===------------------------------------------------------------------===//

stdlib/private/StdlibCollectionUnittest/CheckSequenceType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public struct FindTest {
149149

150150
public init(
151151
expected: Int?, element: Int, sequence: [Int],
152-
expectedLeftoverSequence: [Int],
152+
expectedLeftoverSequence: [Int] = [],
153153
file: String = #file, line: UInt = #line
154154
) {
155155
self.expected = expected

stdlib/public/core/ClosedRange.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,12 @@ where Bound : Strideable, Bound.Stride : SignedInteger
308308
return lowerBound <= element && element <= upperBound
309309
? .inRange(element) : nil
310310
}
311+
312+
@inlinable
313+
public func _customLastIndexOfEquatableElement(_ element: Bound) -> Index?? {
314+
// The first and last elements are the same because each element is unique.
315+
return _customIndexOfEquatableElement(element)
316+
}
311317
}
312318

313319
extension Comparable {

stdlib/public/core/Collection.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,8 +621,8 @@ public protocol Collection: Sequence where SubSequence: Collection {
621621
/// of the collection.
622622
var count: Int { get }
623623

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

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

634+
/// Customization point for `Collection.lastIndex(of:)`.
635+
///
636+
/// Define this method if the collection can find an element in less than
637+
/// O(*n*) by exploiting collection-specific knowledge.
638+
///
639+
/// - Returns: `nil` if a linear search should be attempted instead,
640+
/// `Optional(nil)` if the element was not found, or
641+
/// `Optional(Optional(index))` if an element was found.
642+
///
643+
/// - Complexity: Hopefully less than O(`count`).
644+
func _customLastIndexOfEquatableElement(_ element: Element) -> Index??
645+
634646
/// The first element of the collection.
635647
///
636648
/// If the collection is empty, the value of this property is `nil`.
@@ -1196,6 +1208,22 @@ extension Collection {
11961208
func _customIndexOfEquatableElement(_: Iterator.Element) -> Index?? {
11971209
return nil
11981210
}
1211+
1212+
/// Customization point for `Collection.lastIndex(of:)`.
1213+
///
1214+
/// Define this method if the collection can find an element in less than
1215+
/// O(*n*) by exploiting collection-specific knowledge.
1216+
///
1217+
/// - Returns: `nil` if a linear search should be attempted instead,
1218+
/// `Optional(nil)` if the element was not found, or
1219+
/// `Optional(Optional(index))` if an element was found.
1220+
///
1221+
/// - Complexity: Hopefully less than O(`count`).
1222+
@inlinable
1223+
public // dispatching
1224+
func _customLastIndexOfEquatableElement(_ element: Element) -> Index?? {
1225+
return nil
1226+
}
11991227
}
12001228

12011229
//===----------------------------------------------------------------------===//

stdlib/public/core/CollectionAlgorithms.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,97 @@ extension Collection {
122122
}
123123
}
124124

125+
//===----------------------------------------------------------------------===//
126+
// lastIndex(of:)/lastIndex(where:)
127+
//===----------------------------------------------------------------------===//
128+
129+
extension BidirectionalCollection {
130+
/// Returns the last element of the sequence that satisfies the given
131+
/// predicate.
132+
///
133+
/// This example uses the `last(where:)` method to find the last
134+
/// negative number in an array of integers:
135+
///
136+
/// let numbers = [3, 7, 4, -2, 9, -6, 10, 1]
137+
/// if let lastNegative = numbers.last(where: { $0 < 0 }) {
138+
/// print("The last negative number is \(firstNegative).")
139+
/// }
140+
/// // Prints "The last negative number is -6."
141+
///
142+
/// - Parameter predicate: A closure that takes an element of the sequence as
143+
/// its argument and returns a Boolean value indicating whether the
144+
/// element is a match.
145+
/// - Returns: The last element of the sequence that satisfies `predicate`,
146+
/// or `nil` if there is no element that satisfies `predicate`.
147+
@inlinable
148+
public func last(
149+
where predicate: (Element) throws -> Bool
150+
) rethrows -> Element? {
151+
return try lastIndex(where: predicate).map { self[$0] }
152+
}
153+
154+
/// Returns the index of the last element in the collection that matches the
155+
/// given predicate.
156+
///
157+
/// You can use the predicate to find an element of a type that doesn't
158+
/// conform to the `Equatable` protocol or to find an element that matches
159+
/// particular criteria. This example finds the index of the last name that
160+
/// begins with the letter "A":
161+
///
162+
/// let students = ["Kofi", "Abena", "Peter", "Kweku", "Akosua"]
163+
/// if let i = students.lastIndex(where: { $0.hasPrefix("A") }) {
164+
/// print("\(students[i]) starts with 'A'!")
165+
/// }
166+
/// // Prints "Akosua starts with 'A'!"
167+
///
168+
/// - Parameter predicate: A closure that takes an element as its argument
169+
/// and returns a Boolean value that indicates whether the passed element
170+
/// represents a match.
171+
/// - Returns: The index of the last element in the collection that matches
172+
/// `predicate`, or `nil` if no elements match.
173+
@inlinable
174+
public func lastIndex(
175+
where predicate: (Element) throws -> Bool
176+
) rethrows -> Index? {
177+
var i = endIndex
178+
while i != startIndex {
179+
formIndex(before: &i)
180+
if try predicate(self[i]) {
181+
return i
182+
}
183+
}
184+
return nil
185+
}
186+
}
187+
188+
extension BidirectionalCollection where Element : Equatable {
189+
/// Returns the last index where the specified value appears in the
190+
/// collection.
191+
///
192+
/// After using `lastIndex(of:)` to find the position of the last instance of
193+
/// a particular element in a collection, you can use it to access the
194+
/// element by subscripting. This example shows how you can modify one of
195+
/// the names in an array of students.
196+
///
197+
/// var students = ["Ben", "Ivy", "Jordell", "Ben", "Maxime"]
198+
/// if let i = students.lastIndex(of: "Ben") {
199+
/// students[i] = "Benjamin"
200+
/// }
201+
/// print(students)
202+
/// // Prints "["Ben", "Ivy", "Jordell", "Benjamin", "Max"]"
203+
///
204+
/// - Parameter element: An element to search for in the collection.
205+
/// - Returns: The last index where `element` is found. If `element` is not
206+
/// found in the collection, returns `nil`.
207+
@inlinable
208+
public func lastIndex(of element: Element) -> Index? {
209+
if let result = _customLastIndexOfEquatableElement(element) {
210+
return result
211+
}
212+
return lastIndex(where: { $0 == element })
213+
}
214+
}
215+
125216
//===----------------------------------------------------------------------===//
126217
// partition(by:)
127218
//===----------------------------------------------------------------------===//

stdlib/public/core/Dictionary.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,12 @@ extension Dictionary {
12621262
return Optional(_variantBuffer.index(forKey: element))
12631263
}
12641264

1265+
@inlinable // FIXME(sil-serialize-all)
1266+
public func _customLastIndexOfEquatableElement(_ element: Element) -> Index?? {
1267+
// The first and last elements are the same because each element is unique.
1268+
return _customIndexOfEquatableElement(element)
1269+
}
1270+
12651271
@inlinable // FIXME(sil-serialize-all)
12661272
public static func ==(lhs: Keys, rhs: Keys) -> Bool {
12671273
// Equal if the two dictionaries share storage.

stdlib/public/core/ExistentialCollection.swift.gyb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ internal class _AnyRandomAccessCollectionBox<Element>
349349
// TODO: swift-3-indexing-model: forward the following methods.
350350
/*
351351
func _customIndexOfEquatableElement(element: Element) -> Index??
352+
func _customLastIndexOfEquatableElement(element: Element) -> Index??
352353
*/
353354

354355
@inlinable // FIXME(sil-serialize-all)

0 commit comments

Comments
 (0)