Skip to content

Commit 11b1d46

Browse files
authored
[4.2] [stdlib] Add last(where) and lastIndex(of/where) (#16134)
* Merge pull request #16089 from natecook1000/nc-firstindex [stdlib] Rename index(...) methods to firstIndex(...) * [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 a23329b commit 11b1d46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+544
-179
lines changed

benchmark/single-source/CSVParsing.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func parseQuotedField(_ remainder: inout Substring) throws -> Substring? {
3131
var result: Substring = "" // we accumulate the result
3232

3333
while !remainder.isEmpty {
34-
guard let nextQuoteIndex = remainder.index(of: "\"") else {
34+
guard let nextQuoteIndex = remainder.firstIndex(of: "\"") else {
3535
throw ParseError(message: "Expected a closing \"")
3636
}
3737

benchmark/single-source/RemoveWhere.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension RangeReplaceableCollection {
4646

4747
extension RangeReplaceableCollection where Self: MutableCollection {
4848
mutating func removeWhere_move(where match: (Element) throws -> Bool) rethrows {
49-
guard var i = try index(where: match) else { return }
49+
guard var i = try firstIndex(where: match) else { return }
5050

5151
var j = index(after: i)
5252
while j != endIndex {
@@ -62,7 +62,7 @@ extension RangeReplaceableCollection where Self: MutableCollection {
6262
}
6363

6464
mutating func removeWhere_swap(where match: (Element) throws -> Bool) rethrows {
65-
guard var i = try index(where: match) else { return }
65+
guard var i = try firstIndex(where: match) else { return }
6666

6767
var j = index(after: i)
6868
while j != endIndex {

stdlib/private/StdlibCollectionUnittest/CheckCollectionType.swift

Lines changed: 180 additions & 5 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))
@@ -814,13 +897,13 @@ extension TestSuite {
814897
}
815898

816899
//===------------------------------------------------------------------===//
817-
// index(of:)/index(where:)
900+
// firstIndex(of:)/firstIndex(where:)
818901
//===------------------------------------------------------------------===//
819902

820-
self.test("\(testNamePrefix).index(of:)/semantics") {
903+
self.test("\(testNamePrefix).firstIndex(of:)/semantics") {
821904
for test in findTests {
822905
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
823-
var result = c.index(of: wrapValueIntoEquatable(test.element))
906+
var result = c.firstIndex(of: wrapValueIntoEquatable(test.element))
824907
expectType(
825908
Optional<CollectionWithEquatableElement.Index>.self,
826909
&result)
@@ -834,12 +917,12 @@ extension TestSuite {
834917
}
835918
}
836919

837-
self.test("\(testNamePrefix).index(where:)/semantics") {
920+
self.test("\(testNamePrefix).firstIndex(where:)/semantics") {
838921
for test in findTests {
839922
let closureLifetimeTracker = LifetimeTracked(0)
840923
expectEqual(1, LifetimeTracked.instances)
841924
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
842-
let result = c.index {
925+
let result = c.firstIndex {
843926
(candidate) in
844927
_blackHole(closureLifetimeTracker)
845928
return
@@ -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: 5 additions & 5 deletions
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
@@ -1935,17 +1935,17 @@ self.test("\(testNamePrefix).forEach/semantics") {
19351935
}
19361936

19371937
//===----------------------------------------------------------------------===//
1938-
// first()
1938+
// first(where:)
19391939
//===----------------------------------------------------------------------===//
19401940

1941-
self.test("\(testNamePrefix).first/semantics") {
1941+
self.test("\(testNamePrefix).first(where:)/semantics") {
19421942
for test in findTests {
19431943
let s = makeWrappedSequenceWithEquatableElement(test.sequence)
19441944
let closureLifetimeTracker = LifetimeTracked(0)
1945-
let found = s.first {
1945+
let found = s.first(where: {
19461946
_blackHole(closureLifetimeTracker)
19471947
return $0 == wrapValueIntoEquatable(test.element)
1948-
}
1948+
})
19491949
expectEqual(
19501950
test.expected == nil ? nil : wrapValueIntoEquatable(test.element),
19511951
found,

stdlib/private/StdlibCollectionUnittest/MinimalCollections.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ internal struct _CollectionStateTransition {
333333
transitions = Box<[_CollectionStateTransition]>([])
334334
_CollectionStateTransition._allTransitions[previousState] = transitions
335335
}
336-
if let i = transitions!.value.index(where: { $0._operation == operation }) {
336+
if let i = transitions!.value.firstIndex(where: { $0._operation == operation }) {
337337
self = transitions!.value[i]
338338
return
339339
}

stdlib/public/core/Arrays.swift.gyb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,22 @@ if True:
137137
/// tasked with finding the first two days with absences in the session. To
138138
/// find the indices of the two days in question, follow these steps:
139139
///
140-
/// 1) Call `index(where:)` to find the index of the first element in the
140+
/// 1) Call `firstIndex(where:)` to find the index of the first element in the
141141
/// `absences` array that is greater than zero.
142142
/// 2) Create a slice of the `absences` array starting after the index found in
143143
/// step 1.
144-
/// 3) Call `index(where:)` again, this time on the slice created in step 2.
145-
/// Where in some languages you might pass a starting index into an
144+
/// 3) Call `firstIndex(where:)` again, this time on the slice created in step
145+
/// 2. Where in some languages you might pass a starting index into an
146146
/// `indexOf` method to find the second day, in Swift you perform the same
147147
/// operation on a slice of the original array.
148148
/// 4) Print the results using the indices found in steps 1 and 3 on the
149149
/// original `absences` array.
150150
///
151151
/// Here's an implementation of those steps:
152152
///
153-
/// if let i = absences.index(where: { $0 > 0 }) { // 1
153+
/// if let i = absences.firstIndex(where: { $0 > 0 }) { // 1
154154
/// let absencesAfterFirst = absences[(i + 1)...] // 2
155-
/// if let j = absencesAfterFirst.index(where: { $0 > 0 }) { // 3
155+
/// if let j = absencesAfterFirst.firstIndex(where: { $0 > 0 }) { // 3
156156
/// print("The first day with absences had \(absences[i]).") // 4
157157
/// print("The second day with absences had \(absences[j]).")
158158
/// }
@@ -293,7 +293,7 @@ if True:
293293
/// You can replace an existing element with a new value by assigning the new
294294
/// value to the subscript.
295295
///
296-
/// if let i = students.index(of: "Maxime") {
296+
/// if let i = students.firstIndex(of: "Maxime") {
297297
/// students[i] = "Max"
298298
/// }
299299
/// // ["Ivy", "Jordell", "Liam", "Max", "Shakia"]
@@ -533,7 +533,7 @@ extension ${Self}: RandomAccessCollection, MutableCollection {
533533
/// safe to use with `endIndex`. For example:
534534
///
535535
/// let numbers = [10, 20, 30, 40, 50]
536-
/// if let i = numbers.index(of: 30) {
536+
/// if let i = numbers.firstIndex(of: 30) {
537537
/// print(numbers[i ..< numbers.endIndex])
538538
/// }
539539
/// // Prints "[30, 40, 50]"
@@ -784,7 +784,7 @@ extension ${Self}: RandomAccessCollection, MutableCollection {
784784
/// print(streetsSlice)
785785
/// // Prints "["Channing", "Douglas", "Evarts"]"
786786
///
787-
/// let i = streetsSlice.index(of: "Evarts") // 4
787+
/// let i = streetsSlice.firstIndex(of: "Evarts") // 4
788788
/// print(streets[i!])
789789
/// // Prints "Evarts"
790790
///

stdlib/public/core/BidirectionalCollection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ where SubSequence: BidirectionalCollection, Indices: BidirectionalCollection {
119119
/// print(streetsSlice)
120120
/// // Prints "["Channing", "Douglas", "Evarts"]"
121121
///
122-
/// let index = streetsSlice.index(of: "Evarts") // 4
122+
/// let index = streetsSlice.firstIndex(of: "Evarts") // 4
123123
/// print(streets[index!])
124124
/// // Prints "Evarts"
125125
///

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 {

0 commit comments

Comments
 (0)