Skip to content

Commit e12e968

Browse files
authored
Add contains(_:) methods to (Closed)Range (#76891)
The _StringProcessing module provides a generic, collection-based `contains` method that performs poorly for ranges and closed ranges. This addresses the primary issue by providing concrete overloads for Range and ClosedRange which match the expected performance for these operations. This change also fixes an issue with the existing range overlap tests. The generated `(Closed)Range.overlap` tests are ignoring the "other" range type when generating ranges for testing, so all overlap tests are only being run against ranges of the same type. This fixes things so that heterogeneous testing is included.
1 parent 655336c commit e12e968

File tree

6 files changed

+326
-5
lines changed

6 files changed

+326
-5
lines changed

benchmark/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ set(SWIFT_BENCH_MODULES
159159
single-source/RandomTree
160160
single-source/RandomValues
161161
single-source/RangeAssignment
162+
single-source/RangeContains
162163
single-source/RangeIteration
163164
single-source/RangeOverlaps
164165
single-source/RangeReplaceableCollectionPlusDefault
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//===--- RangeContains.swift ----------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import TestsUtils
14+
15+
public let benchmarks = [
16+
BenchmarkInfo(
17+
name: "RangeContainsRange",
18+
runFunction: run_RangeContainsRange,
19+
tags: [.validation, .api],
20+
setUpFunction: buildRanges),
21+
BenchmarkInfo(
22+
name: "RangeContainsClosedRange",
23+
runFunction: run_RangeContainsClosedRange,
24+
tags: [.validation, .api],
25+
setUpFunction: buildRanges),
26+
BenchmarkInfo(
27+
name: "ClosedRangeContainsRange",
28+
runFunction: run_ClosedRangeContainsRange,
29+
tags: [.validation, .api],
30+
setUpFunction: buildRanges),
31+
BenchmarkInfo(
32+
name: "ClosedRangeContainsClosedRange",
33+
runFunction: run_ClosedRangeContainsClosedRange,
34+
tags: [.validation, .api],
35+
setUpFunction: buildRanges),
36+
]
37+
38+
private func buildRanges() {
39+
blackHole(ranges)
40+
blackHole(closedRanges)
41+
}
42+
43+
private let ranges: [Range<Int>] = (-8...8).flatMap { a in (0...16).map { l in a..<(a+l) } }
44+
private let closedRanges: [ClosedRange<Int>] = (-8...8).flatMap { a in (0...16).map { l in a...(a+l) } }
45+
46+
@inline(never)
47+
public func run_RangeContainsRange(_ n: Int) {
48+
var checksum: UInt64 = 0
49+
for _ in 0..<n {
50+
for lhs in ranges {
51+
for rhs in ranges {
52+
if lhs.contains(rhs) { checksum += 1 }
53+
}
54+
}
55+
}
56+
check(checksum == 15725 * UInt64(n))
57+
}
58+
59+
@inline(never)
60+
public func run_RangeContainsClosedRange(_ n: Int) {
61+
var checksum: UInt64 = 0
62+
for _ in 0..<n {
63+
for lhs in ranges {
64+
for rhs in closedRanges {
65+
if lhs.contains(rhs) { checksum += 1 }
66+
}
67+
}
68+
}
69+
check(checksum == 10812 * UInt64(n))
70+
}
71+
72+
@inline(never)
73+
public func run_ClosedRangeContainsRange(_ n: Int) {
74+
var checksum: UInt64 = 0
75+
for _ in 0..<n {
76+
for lhs in closedRanges {
77+
for rhs in ranges {
78+
if lhs.contains(rhs) { checksum += 1 }
79+
}
80+
}
81+
}
82+
check(checksum == 17493 * UInt64(n))
83+
}
84+
85+
@inline(never)
86+
public func run_ClosedRangeContainsClosedRange(_ n: Int) {
87+
var checksum: UInt64 = 0
88+
for _ in 0..<n {
89+
for lhs in closedRanges {
90+
for rhs in closedRanges {
91+
if lhs.contains(rhs) { checksum += 1 }
92+
}
93+
}
94+
}
95+
check(checksum == 12597 * UInt64(n))
96+
}

benchmark/utils/main.swift

+2
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ import RandomShuffle
163163
import RandomTree
164164
import RandomValues
165165
import RangeAssignment
166+
import RangeContains
166167
import RangeIteration
167168
import RangeOverlaps
168169
import RangeReplaceableCollectionPlusDefault
@@ -362,6 +363,7 @@ register(RandomShuffle.benchmarks)
362363
register(RandomTree.benchmarks)
363364
register(RandomValues.benchmarks)
364365
register(RangeAssignment.benchmarks)
366+
register(RangeContains.benchmarks)
365367
register(RangeIteration.benchmarks)
366368
register(RangeOverlaps.benchmarks)
367369
register(RangeReplaceableCollectionPlusDefault.benchmarks)

stdlib/public/core/ClosedRange.swift

+89
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,64 @@ where Bound: Strideable, Bound.Stride: SignedInteger
325325
// The first and last elements are the same because each element is unique.
326326
return _customIndexOfEquatableElement(element)
327327
}
328+
329+
/// Returns a Boolean value indicating whether the given range is contained
330+
/// within this closed range.
331+
///
332+
/// The given range is contained within this closed range if the elements of
333+
/// the range are all contained within this closed range.
334+
///
335+
/// let range = 0...10
336+
/// range.contains(5..<7) // true
337+
/// range.contains(5..<10) // true
338+
/// range.contains(5..<12) // false
339+
///
340+
/// // Note that `5..<11` contains 5, 6, 7, 8, 9, and 10.
341+
/// range.contains(5..<11) // true
342+
///
343+
/// Additionally, passing any empty range as `other` results in the value
344+
/// `true`, even if the empty range's bounds are outside the bounds of this
345+
/// closed range.
346+
///
347+
/// range.contains(3..<3) // true
348+
/// range.contains(20..<20) // true
349+
///
350+
/// - Parameter other: A range to check for containment within this closed
351+
/// range.
352+
/// - Returns: `true` if `other` is empty or wholly contained within this
353+
/// closed range; otherwise, `false`.
354+
///
355+
/// - Complexity: O(1)
356+
@_alwaysEmitIntoClient
357+
public func contains(_ other: Range<Bound>) -> Bool {
358+
if other.isEmpty { return true }
359+
let otherInclusiveUpper = other.upperBound.advanced(by: -1)
360+
return lowerBound <= other.lowerBound && upperBound >= otherInclusiveUpper
361+
}
362+
}
363+
364+
extension ClosedRange {
365+
/// Returns a Boolean value indicating whether the given closed range is
366+
/// contained within this closed range.
367+
///
368+
/// The given closed range is contained within this range if its bounds are
369+
/// contained within this closed range.
370+
///
371+
/// let range = 0...10
372+
/// range.contains(2...5) // true
373+
/// range.contains(2...10) // true
374+
/// range.contains(2...12) // false
375+
///
376+
/// - Parameter other: A closed range to check for containment within this
377+
/// closed range.
378+
/// - Returns: `true` if `other` is wholly contained within this closed range;
379+
/// otherwise, `false`.
380+
///
381+
/// - Complexity: O(1)
382+
@_alwaysEmitIntoClient
383+
public func contains(_ other: ClosedRange<Bound>) -> Bool {
384+
lowerBound <= other.lowerBound && upperBound >= other.upperBound
385+
}
328386
}
329387

330388
extension Comparable {
@@ -459,6 +517,18 @@ extension ClosedRange where Bound: Strideable, Bound.Stride: SignedInteger {
459517
}
460518

461519
extension ClosedRange {
520+
/// Returns a Boolean value indicating whether this range and the given closed
521+
/// range contain an element in common.
522+
///
523+
/// This example shows two overlapping ranges:
524+
///
525+
/// let x: Range = 0...20
526+
/// print(x.overlaps(10...1000))
527+
/// // Prints "true"
528+
///
529+
/// - Parameter other: A range to check for elements in common.
530+
/// - Returns: `true` if this range and `other` have at least one element in
531+
/// common; otherwise, `false`.
462532
@inlinable
463533
public func overlaps(_ other: ClosedRange<Bound>) -> Bool {
464534
// Disjoint iff the other range is completely before or after our range.
@@ -469,6 +539,25 @@ extension ClosedRange {
469539
return !isDisjoint
470540
}
471541

542+
/// Returns a Boolean value indicating whether this range and the given range
543+
/// contain an element in common.
544+
///
545+
/// This example shows two overlapping ranges:
546+
///
547+
/// let x: Range = 0...20
548+
/// print(x.overlaps(10..<1000))
549+
/// // Prints "true"
550+
///
551+
/// Because a closed range includes its upper bound, the ranges in the
552+
/// following example overlap:
553+
///
554+
/// let y = 20..<30
555+
/// print(x.overlaps(y))
556+
/// // Prints "true"
557+
///
558+
/// - Parameter other: A range to check for elements in common.
559+
/// - Returns: `true` if this range and `other` have at least one element in
560+
/// common; otherwise, `false`.
472561
@inlinable
473562
public func overlaps(_ other: Range<Bound>) -> Bool {
474563
return other.overlaps(self)

stdlib/public/core/Range.swift

+78-1
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ extension Range {
986986
/// This example shows two overlapping ranges:
987987
///
988988
/// let x: Range = 0..<20
989-
/// print(x.overlaps(10...1000))
989+
/// print(x.overlaps(10..<1000))
990990
/// // Prints "true"
991991
///
992992
/// Because a half-open range does not include its upper bound, the ranges
@@ -1011,6 +1011,25 @@ extension Range {
10111011
return !isDisjoint
10121012
}
10131013

1014+
/// Returns a Boolean value indicating whether this range and the given closed
1015+
/// range contain an element in common.
1016+
///
1017+
/// This example shows two overlapping ranges:
1018+
///
1019+
/// let x: Range = 0..<20
1020+
/// print(x.overlaps(10...1000))
1021+
/// // Prints "true"
1022+
///
1023+
/// Because a half-open range does not include its upper bound, the ranges
1024+
/// in the following example do not overlap:
1025+
///
1026+
/// let y = 20...30
1027+
/// print(x.overlaps(y))
1028+
/// // Prints "false"
1029+
///
1030+
/// - Parameter other: A closed range to check for elements in common.
1031+
/// - Returns: `true` if this range and `other` have at least one element in
1032+
/// common; otherwise, `false`.
10141033
@inlinable
10151034
public func overlaps(_ other: ClosedRange<Bound>) -> Bool {
10161035
// Disjoint iff the other range is completely before or after our range.
@@ -1024,6 +1043,64 @@ extension Range {
10241043
}
10251044
}
10261045

1046+
extension Range {
1047+
/// Returns a Boolean value indicating whether the given range is contained
1048+
/// within this range.
1049+
///
1050+
/// The given range is contained within this range if its bounds are equal to
1051+
/// or within the bounds of this range.
1052+
///
1053+
/// let range = 0..<10
1054+
/// range.contains(2..<5) // true
1055+
/// range.contains(2..<10) // true
1056+
/// range.contains(2..<12) // false
1057+
///
1058+
/// Additionally, passing any empty range as `other` results in the value
1059+
/// `true`, even if the empty range's bounds are outside the bounds of this
1060+
/// range.
1061+
///
1062+
/// let emptyRange = 3..<3
1063+
/// emptyRange.contains(3..<3) // true
1064+
/// emptyRange.contains(5..<5) // true
1065+
///
1066+
/// - Parameter other: A range to check for containment within this range.
1067+
/// - Returns: `true` if `other` is empty or wholly contained within this
1068+
/// range; otherwise, `false`.
1069+
///
1070+
/// - Complexity: O(1)
1071+
@_alwaysEmitIntoClient
1072+
public func contains(_ other: Range<Bound>) -> Bool {
1073+
other.isEmpty ||
1074+
(lowerBound <= other.lowerBound && upperBound >= other.upperBound)
1075+
}
1076+
1077+
/// Returns a Boolean value indicating whether the given closed range is
1078+
/// contained within this range.
1079+
///
1080+
/// The given closed range is contained within this range if its bounds are
1081+
/// contained within this range. If this range is empty, it cannot contain a
1082+
/// closed range, since closed ranges by definition contain their boundaries.
1083+
///
1084+
/// let range = 0..<10
1085+
/// range.contains(2...5) // true
1086+
/// range.contains(2...10) // false
1087+
/// range.contains(2...12) // false
1088+
///
1089+
/// let emptyRange = 3..<3
1090+
/// emptyRange.contains(3...3) // false
1091+
///
1092+
/// - Parameter other: A closed range to check for containment within this
1093+
/// range.
1094+
/// - Returns: `true` if `other` is wholly contained within this range;
1095+
/// otherwise, `false`.
1096+
///
1097+
/// - Complexity: O(1)
1098+
@_alwaysEmitIntoClient
1099+
public func contains(_ other: ClosedRange<Bound>) -> Bool {
1100+
lowerBound <= other.lowerBound && upperBound > other.upperBound
1101+
}
1102+
}
1103+
10271104
// Note: this is not for compatibility only, it is considered a useful
10281105
// shorthand. TODO: Add documentation
10291106
public typealias CountableRange<Bound: Strideable> = Range<Bound>

0 commit comments

Comments
 (0)