Skip to content

Commit dd6d257

Browse files
committed
[stdlib] Implement last... methods on Seq/Collection
1 parent 659e7ce commit dd6d257

File tree

11 files changed

+379
-7
lines changed

11 files changed

+379
-7
lines changed

stdlib/private/StdlibCollectionUnittest/CheckCollectionType.swift

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,12 @@ extension TestSuite {
12221222
return makeCollection(elements.map(wrapValue))
12231223
}
12241224

1225+
func makeWrappedCollectionWithEquatableElement(
1226+
_ elements: [MinimalEquatableValue]
1227+
) -> CollectionWithEquatableElement {
1228+
return makeCollectionOfEquatable(elements.map(wrapValueIntoEquatable))
1229+
}
1230+
12251231
testNamePrefix += String(describing: C.Type.self)
12261232

12271233
// FIXME: swift-3-indexing-model - add tests for the follow?
@@ -1254,6 +1260,149 @@ extension TestSuite {
12541260
}
12551261
}
12561262

1263+
//===------------------------------------------------------------------===//
1264+
// last(where:)
1265+
//===------------------------------------------------------------------===//
1266+
1267+
let lastPerformanceTest = FindTest(
1268+
expected: 3,
1269+
element: 2020,
1270+
sequence: [ 1010, 2020, 3030, 2020, 4040 ],
1271+
expectedLeftoverSequence: [ 3030, 2020, 4040 ])
1272+
1273+
self.test("\(testNamePrefix).last(where:)/semantics") {
1274+
for test in findLastTests {
1275+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1276+
let closureLifetimeTracker = LifetimeTracked(0)
1277+
let found = c.last(where: {
1278+
_blackHole(closureLifetimeTracker)
1279+
return $0 == wrapValueIntoEquatable(test.element)
1280+
})
1281+
expectEqual(
1282+
test.expected == nil ? nil : wrapValueIntoEquatable(test.element),
1283+
found,
1284+
stackTrace: SourceLocStack().with(test.loc))
1285+
if let expectedIdentity = test.expected {
1286+
expectEqual(
1287+
expectedIdentity, extractValueFromEquatable(found!).identity,
1288+
"last(where:) should find only the first element matching its predicate")
1289+
}
1290+
}
1291+
}
1292+
1293+
self.test("\(testNamePrefix).last(where:)/performance") {
1294+
let test = lastPerformanceTest
1295+
let closureLifetimeTracker = LifetimeTracked(0)
1296+
expectEqual(1, LifetimeTracked.instances)
1297+
let c = makeCollectionOfEquatable(test.sequence.map(wrapValueIntoEquatable))
1298+
var closureCounter = 0
1299+
let found = c.last(where: {
1300+
(candidate) in
1301+
_blackHole(closureLifetimeTracker)
1302+
closureCounter += 1
1303+
return
1304+
extractValueFromEquatable(candidate).value == test.element.value
1305+
})
1306+
expectEqual(
1307+
test.expected == nil ? nil : wrapValueIntoEquatable(test.element),
1308+
found,
1309+
stackTrace: SourceLocStack().with(test.loc))
1310+
expectEqual(
1311+
2,
1312+
closureCounter,
1313+
stackTrace: SourceLocStack().with(test.loc))
1314+
}
1315+
1316+
//===------------------------------------------------------------------===//
1317+
// lastIndex(of:)/lastIndex(where:)
1318+
//===------------------------------------------------------------------===//
1319+
1320+
self.test("\(testNamePrefix).lastIndex(of:)/semantics") {
1321+
for test in findLastTests {
1322+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1323+
var result = c.lastIndex(of: wrapValueIntoEquatable(test.element))
1324+
expectType(
1325+
Optional<CollectionWithEquatableElement.Index>.self,
1326+
&result)
1327+
let zeroBasedIndex = result.map {
1328+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1329+
}
1330+
expectEqual(
1331+
test.expected,
1332+
zeroBasedIndex,
1333+
stackTrace: SourceLocStack().with(test.loc))
1334+
}
1335+
}
1336+
1337+
self.test("\(testNamePrefix).lastIndex(of:)/performance") {
1338+
let test = lastPerformanceTest
1339+
let wrappedElement = wrapValueIntoEquatable(test.element)
1340+
// Only test specialization when we can actually track calls to `==`
1341+
guard wrappedElement is MinimalEquatableValue else { return }
1342+
1343+
let c = makeCollectionOfEquatable(test.sequence.map(wrapValueIntoEquatable))
1344+
MinimalEquatableValue.timesEqualEqualWasCalled = 0
1345+
let result = c.lastIndex(of: wrappedElement)
1346+
let zeroBasedIndex = result.map {
1347+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1348+
}
1349+
expectEqual(
1350+
test.expected,
1351+
zeroBasedIndex,
1352+
stackTrace: SourceLocStack().with(test.loc))
1353+
expectEqual(
1354+
2,
1355+
MinimalEquatableValue.timesEqualEqualWasCalled,
1356+
stackTrace: SourceLocStack().with(test.loc))
1357+
}
1358+
1359+
self.test("\(testNamePrefix).lastIndex(where:)/semantics") {
1360+
for test in findLastTests {
1361+
let closureLifetimeTracker = LifetimeTracked(0)
1362+
expectEqual(1, LifetimeTracked.instances)
1363+
let c = makeWrappedCollectionWithEquatableElement(test.sequence)
1364+
let result = c.lastIndex(where: {
1365+
(candidate) in
1366+
_blackHole(closureLifetimeTracker)
1367+
return
1368+
extractValueFromEquatable(candidate).value == test.element.value
1369+
})
1370+
let zeroBasedIndex = result.map {
1371+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1372+
}
1373+
expectEqual(
1374+
test.expected,
1375+
zeroBasedIndex,
1376+
stackTrace: SourceLocStack().with(test.loc))
1377+
}
1378+
}
1379+
1380+
self.test("\(testNamePrefix).lastIndex(where:)/performance") {
1381+
let test = lastPerformanceTest
1382+
let closureLifetimeTracker = LifetimeTracked(0)
1383+
expectEqual(1, LifetimeTracked.instances)
1384+
let c = makeCollectionOfEquatable(test.sequence.map(wrapValueIntoEquatable))
1385+
var closureCounter = 0
1386+
let result = c.lastIndex(where: {
1387+
(candidate) in
1388+
_blackHole(closureLifetimeTracker)
1389+
closureCounter += 1
1390+
return
1391+
extractValueFromEquatable(candidate).value == test.element.value
1392+
})
1393+
let zeroBasedIndex = result.map {
1394+
numericCast(c.distance(from: c.startIndex, to: $0)) as Int
1395+
}
1396+
expectEqual(
1397+
test.expected,
1398+
zeroBasedIndex,
1399+
stackTrace: SourceLocStack().with(test.loc))
1400+
expectEqual(
1401+
2,
1402+
closureCounter,
1403+
stackTrace: SourceLocStack().with(test.loc))
1404+
}
1405+
12571406
//===------------------------------------------------------------------===//
12581407
// removeLast()/slice
12591408
//===------------------------------------------------------------------===//

stdlib/private/StdlibCollectionUnittest/CheckSequenceType.swift

Lines changed: 63 additions & 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
@@ -584,6 +584,68 @@ public let findTests = [
584584
expectedLeftoverSequence: [ 3030, 2020, 4040 ]),
585585
]
586586

587+
public let findLastTests = [
588+
FindTest(
589+
expected: nil,
590+
element: 42,
591+
sequence: [],
592+
expectedLeftoverSequence: []),
593+
594+
FindTest(
595+
expected: nil,
596+
element: 42,
597+
sequence: [ 1010 ],
598+
expectedLeftoverSequence: []),
599+
FindTest(
600+
expected: 0,
601+
element: 1010,
602+
sequence: [ 1010 ],
603+
expectedLeftoverSequence: []),
604+
605+
FindTest(
606+
expected: nil,
607+
element: 42,
608+
sequence: [ 1010, 1010 ],
609+
expectedLeftoverSequence: []),
610+
FindTest(
611+
expected: 1,
612+
element: 1010,
613+
sequence: [ 1010, 1010 ],
614+
expectedLeftoverSequence: [ 1010 ]),
615+
616+
FindTest(
617+
expected: nil,
618+
element: 42,
619+
sequence: [ 1010, 2020, 3030, 4040 ],
620+
expectedLeftoverSequence: []),
621+
FindTest(
622+
expected: 0,
623+
element: 1010,
624+
sequence: [ 1010, 2020, 3030, 4040 ],
625+
expectedLeftoverSequence: [ ]),
626+
FindTest(
627+
expected: 1,
628+
element: 2020,
629+
sequence: [ 1010, 2020, 3030, 4040 ],
630+
expectedLeftoverSequence: [ 3030, 4040 ]),
631+
FindTest(
632+
expected: 2,
633+
element: 3030,
634+
sequence: [ 1010, 2020, 3030, 4040 ],
635+
expectedLeftoverSequence: [ 4040 ]),
636+
FindTest(
637+
expected: 3,
638+
element: 4040,
639+
sequence: [ 1010, 2020, 3030, 4040 ],
640+
expectedLeftoverSequence: []),
641+
642+
FindTest(
643+
expected: 3,
644+
element: 2020,
645+
sequence: [ 1010, 2020, 3030, 2020, 4040 ],
646+
expectedLeftoverSequence: [ 3030, 2020, 4040 ]),
647+
]
648+
587649
public let unionTests = [
588650
CollectionBinaryOperationTest(expected: [1, 2, 3, 4, 5], lhs: [1, 3, 5], rhs: [2, 4]),
589651
CollectionBinaryOperationTest(expected: [3, 5], lhs: [3], rhs: [5])

stdlib/public/core/ClosedRange.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ 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+
return _customIndexOfEquatableElement(element)
315+
}
311316
}
312317

313318
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
//===----------------------------------------------------------------------===//

0 commit comments

Comments
 (0)