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.
This commit is contained in:
Nate Cook
2024-11-12 13:47:24 -06:00
committed by GitHub
parent 655336cceb
commit e12e968570
6 changed files with 326 additions and 5 deletions

View File

@@ -159,6 +159,7 @@ set(SWIFT_BENCH_MODULES
single-source/RandomTree
single-source/RandomValues
single-source/RangeAssignment
single-source/RangeContains
single-source/RangeIteration
single-source/RangeOverlaps
single-source/RangeReplaceableCollectionPlusDefault

View File

@@ -0,0 +1,96 @@
//===--- RangeContains.swift ----------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import TestsUtils
public let benchmarks = [
BenchmarkInfo(
name: "RangeContainsRange",
runFunction: run_RangeContainsRange,
tags: [.validation, .api],
setUpFunction: buildRanges),
BenchmarkInfo(
name: "RangeContainsClosedRange",
runFunction: run_RangeContainsClosedRange,
tags: [.validation, .api],
setUpFunction: buildRanges),
BenchmarkInfo(
name: "ClosedRangeContainsRange",
runFunction: run_ClosedRangeContainsRange,
tags: [.validation, .api],
setUpFunction: buildRanges),
BenchmarkInfo(
name: "ClosedRangeContainsClosedRange",
runFunction: run_ClosedRangeContainsClosedRange,
tags: [.validation, .api],
setUpFunction: buildRanges),
]
private func buildRanges() {
blackHole(ranges)
blackHole(closedRanges)
}
private let ranges: [Range<Int>] = (-8...8).flatMap { a in (0...16).map { l in a..<(a+l) } }
private let closedRanges: [ClosedRange<Int>] = (-8...8).flatMap { a in (0...16).map { l in a...(a+l) } }
@inline(never)
public func run_RangeContainsRange(_ n: Int) {
var checksum: UInt64 = 0
for _ in 0..<n {
for lhs in ranges {
for rhs in ranges {
if lhs.contains(rhs) { checksum += 1 }
}
}
}
check(checksum == 15725 * UInt64(n))
}
@inline(never)
public func run_RangeContainsClosedRange(_ n: Int) {
var checksum: UInt64 = 0
for _ in 0..<n {
for lhs in ranges {
for rhs in closedRanges {
if lhs.contains(rhs) { checksum += 1 }
}
}
}
check(checksum == 10812 * UInt64(n))
}
@inline(never)
public func run_ClosedRangeContainsRange(_ n: Int) {
var checksum: UInt64 = 0
for _ in 0..<n {
for lhs in closedRanges {
for rhs in ranges {
if lhs.contains(rhs) { checksum += 1 }
}
}
}
check(checksum == 17493 * UInt64(n))
}
@inline(never)
public func run_ClosedRangeContainsClosedRange(_ n: Int) {
var checksum: UInt64 = 0
for _ in 0..<n {
for lhs in closedRanges {
for rhs in closedRanges {
if lhs.contains(rhs) { checksum += 1 }
}
}
}
check(checksum == 12597 * UInt64(n))
}

View File

@@ -163,6 +163,7 @@ import RandomShuffle
import RandomTree
import RandomValues
import RangeAssignment
import RangeContains
import RangeIteration
import RangeOverlaps
import RangeReplaceableCollectionPlusDefault
@@ -362,6 +363,7 @@ register(RandomShuffle.benchmarks)
register(RandomTree.benchmarks)
register(RandomValues.benchmarks)
register(RangeAssignment.benchmarks)
register(RangeContains.benchmarks)
register(RangeIteration.benchmarks)
register(RangeOverlaps.benchmarks)
register(RangeReplaceableCollectionPlusDefault.benchmarks)

View File

@@ -325,6 +325,64 @@ where Bound: Strideable, Bound.Stride: SignedInteger
// The first and last elements are the same because each element is unique.
return _customIndexOfEquatableElement(element)
}
/// Returns a Boolean value indicating whether the given range is contained
/// within this closed range.
///
/// The given range is contained within this closed range if the elements of
/// the range are all contained within this closed range.
///
/// let range = 0...10
/// range.contains(5..<7) // true
/// range.contains(5..<10) // true
/// range.contains(5..<12) // false
///
/// // Note that `5..<11` contains 5, 6, 7, 8, 9, and 10.
/// range.contains(5..<11) // true
///
/// Additionally, passing any empty range as `other` results in the value
/// `true`, even if the empty range's bounds are outside the bounds of this
/// closed range.
///
/// range.contains(3..<3) // true
/// range.contains(20..<20) // true
///
/// - Parameter other: A range to check for containment within this closed
/// range.
/// - Returns: `true` if `other` is empty or wholly contained within this
/// closed range; otherwise, `false`.
///
/// - Complexity: O(1)
@_alwaysEmitIntoClient
public func contains(_ other: Range<Bound>) -> Bool {
if other.isEmpty { return true }
let otherInclusiveUpper = other.upperBound.advanced(by: -1)
return lowerBound <= other.lowerBound && upperBound >= otherInclusiveUpper
}
}
extension ClosedRange {
/// Returns a Boolean value indicating whether the given closed range is
/// contained within this closed range.
///
/// The given closed range is contained within this range if its bounds are
/// contained within this closed range.
///
/// let range = 0...10
/// range.contains(2...5) // true
/// range.contains(2...10) // true
/// range.contains(2...12) // false
///
/// - Parameter other: A closed range to check for containment within this
/// closed range.
/// - Returns: `true` if `other` is wholly contained within this closed range;
/// otherwise, `false`.
///
/// - Complexity: O(1)
@_alwaysEmitIntoClient
public func contains(_ other: ClosedRange<Bound>) -> Bool {
lowerBound <= other.lowerBound && upperBound >= other.upperBound
}
}
extension Comparable {
@@ -459,6 +517,18 @@ extension ClosedRange where Bound: Strideable, Bound.Stride: SignedInteger {
}
extension ClosedRange {
/// Returns a Boolean value indicating whether this range and the given closed
/// range contain an element in common.
///
/// This example shows two overlapping ranges:
///
/// let x: Range = 0...20
/// print(x.overlaps(10...1000))
/// // Prints "true"
///
/// - Parameter other: A range to check for elements in common.
/// - Returns: `true` if this range and `other` have at least one element in
/// common; otherwise, `false`.
@inlinable
public func overlaps(_ other: ClosedRange<Bound>) -> Bool {
// Disjoint iff the other range is completely before or after our range.
@@ -469,6 +539,25 @@ extension ClosedRange {
return !isDisjoint
}
/// Returns a Boolean value indicating whether this range and the given range
/// contain an element in common.
///
/// This example shows two overlapping ranges:
///
/// let x: Range = 0...20
/// print(x.overlaps(10..<1000))
/// // Prints "true"
///
/// Because a closed range includes its upper bound, the ranges in the
/// following example overlap:
///
/// let y = 20..<30
/// print(x.overlaps(y))
/// // Prints "true"
///
/// - Parameter other: A range to check for elements in common.
/// - Returns: `true` if this range and `other` have at least one element in
/// common; otherwise, `false`.
@inlinable
public func overlaps(_ other: Range<Bound>) -> Bool {
return other.overlaps(self)

View File

@@ -986,7 +986,7 @@ extension Range {
/// This example shows two overlapping ranges:
///
/// let x: Range = 0..<20
/// print(x.overlaps(10...1000))
/// print(x.overlaps(10..<1000))
/// // Prints "true"
///
/// Because a half-open range does not include its upper bound, the ranges
@@ -1011,6 +1011,25 @@ extension Range {
return !isDisjoint
}
/// Returns a Boolean value indicating whether this range and the given closed
/// range contain an element in common.
///
/// This example shows two overlapping ranges:
///
/// let x: Range = 0..<20
/// print(x.overlaps(10...1000))
/// // Prints "true"
///
/// Because a half-open range does not include its upper bound, the ranges
/// in the following example do not overlap:
///
/// let y = 20...30
/// print(x.overlaps(y))
/// // Prints "false"
///
/// - Parameter other: A closed range to check for elements in common.
/// - Returns: `true` if this range and `other` have at least one element in
/// common; otherwise, `false`.
@inlinable
public func overlaps(_ other: ClosedRange<Bound>) -> Bool {
// Disjoint iff the other range is completely before or after our range.
@@ -1024,6 +1043,64 @@ extension Range {
}
}
extension Range {
/// Returns a Boolean value indicating whether the given range is contained
/// within this range.
///
/// The given range is contained within this range if its bounds are equal to
/// or within the bounds of this range.
///
/// let range = 0..<10
/// range.contains(2..<5) // true
/// range.contains(2..<10) // true
/// range.contains(2..<12) // false
///
/// Additionally, passing any empty range as `other` results in the value
/// `true`, even if the empty range's bounds are outside the bounds of this
/// range.
///
/// let emptyRange = 3..<3
/// emptyRange.contains(3..<3) // true
/// emptyRange.contains(5..<5) // true
///
/// - Parameter other: A range to check for containment within this range.
/// - Returns: `true` if `other` is empty or wholly contained within this
/// range; otherwise, `false`.
///
/// - Complexity: O(1)
@_alwaysEmitIntoClient
public func contains(_ other: Range<Bound>) -> Bool {
other.isEmpty ||
(lowerBound <= other.lowerBound && upperBound >= other.upperBound)
}
/// Returns a Boolean value indicating whether the given closed range is
/// contained within this range.
///
/// The given closed range is contained within this range if its bounds are
/// contained within this range. If this range is empty, it cannot contain a
/// closed range, since closed ranges by definition contain their boundaries.
///
/// let range = 0..<10
/// range.contains(2...5) // true
/// range.contains(2...10) // false
/// range.contains(2...12) // false
///
/// let emptyRange = 3..<3
/// emptyRange.contains(3...3) // false
///
/// - Parameter other: A closed range to check for containment within this
/// range.
/// - Returns: `true` if `other` is wholly contained within this range;
/// otherwise, `false`.
///
/// - Complexity: O(1)
@_alwaysEmitIntoClient
public func contains(_ other: ClosedRange<Bound>) -> Bool {
lowerBound <= other.lowerBound && upperBound > other.upperBound
}
}
// Note: this is not for compatibility only, it is considered a useful
// shorthand. TODO: Add documentation
public typealias CountableRange<Bound: Strideable> = Range<Bound>

View File

@@ -122,6 +122,42 @@ struct OverlapsTest {
}
}
typealias ContainsRangeTest = OverlapsTest
let containsRangeTests: [ContainsRangeTest] = [
// Same bounds
ContainsRangeTest(expected: true, lhs: 0..<*10, rhs: 0..<*10),
ContainsRangeTest(expected: false, lhs: 0..<*10, rhs: 0...*10),
ContainsRangeTest(expected: true, lhs: 0..<*10, rhs: 0...*9),
ContainsRangeTest(expected: true, lhs: 0...*10, rhs: 0...*10),
ContainsRangeTest(expected: true, lhs: 0...*10, rhs: 0..<*10),
ContainsRangeTest(expected: true, lhs: 0...*10, rhs: 0..<*11),
// Interior
ContainsRangeTest(expected: true, lhs: 0..<*10, rhs: 1..<*9),
ContainsRangeTest(expected: true, lhs: 0..<*10, rhs: 1...*9),
ContainsRangeTest(expected: true, lhs: 0...*10, rhs: 1...*9),
ContainsRangeTest(expected: true, lhs: 0...*10, rhs: 1..<*9),
// Failures
ContainsRangeTest(expected: false, lhs: 0..<*10, rhs: -10..<*5),
ContainsRangeTest(expected: false, lhs: 0..<*10, rhs: -10...*5),
ContainsRangeTest(expected: false, lhs: 0...*10, rhs: -10...*5),
ContainsRangeTest(expected: false, lhs: 0...*10, rhs: -10..<*5),
ContainsRangeTest(expected: false, lhs: 0..<*10, rhs: 5..<*15),
ContainsRangeTest(expected: false, lhs: 0..<*10, rhs: 5...*15),
ContainsRangeTest(expected: false, lhs: 0...*10, rhs: 5...*15),
ContainsRangeTest(expected: false, lhs: 0...*10, rhs: 5..<*15),
// "Empty" ranges
ContainsRangeTest(expected: true, lhs: 0..<*0, rhs: 0..<*0),
ContainsRangeTest(expected: true, lhs: 0..<*0, rhs: 1..<*1),
ContainsRangeTest(expected: false, lhs: 0..<*0, rhs: 0...*0),
ContainsRangeTest(expected: true, lhs: 0...*0, rhs: 0...*0),
ContainsRangeTest(expected: true, lhs: 0...*0, rhs: 0..<*0),
ContainsRangeTest(expected: true, lhs: 0...*0, rhs: 1..<*1),
]
let overlapsTests: [OverlapsTest] = [
// 0-4, 5-10
OverlapsTest(expected: false, lhs: 0..<*4, rhs: 5..<*10),
@@ -507,20 +543,20 @@ ${TestSuite}.test("contains(_:)/semantics, ~=/semantics")
expectEqual(expected, range ~= value)
}
% for (OtherSelf, other_op, OtherBound, __elementIsTestProtocol1) in all_range_types:
% for (OtherSelf, other_op, _, _) in all_range_types:
${TestSuite}.test("overlaps(${OtherSelf})/semantics")
.forEach(in: overlapsTests) {
(test) in
if test.lhs.isHalfOpen != ${Self}<${Bound}>._isHalfOpen ||
test.rhs.isHalfOpen != ${OtherSelf}<${OtherBound}>._isHalfOpen {
test.rhs.isHalfOpen != ${OtherSelf}<${Bound}>._isHalfOpen {
return
}
let lhs: ${Self}<${Bound}>
= ${Bound}(test.lhs.lowerBound)${op}${Bound}(test.lhs.upperBound)
let rhs: ${Self}<${Bound}>
= ${Bound}(test.rhs.lowerBound)${op}${Bound}(test.rhs.upperBound)
let rhs: ${OtherSelf}<${Bound}>
= ${Bound}(test.rhs.lowerBound)${other_op}${Bound}(test.rhs.upperBound)
expectEqual(test.expected, lhs.overlaps(rhs))
expectEqual(test.expected, rhs.overlaps(lhs))
@@ -528,6 +564,26 @@ ${TestSuite}.test("overlaps(${OtherSelf})/semantics")
expectEqual(!lhs.isEmpty, lhs.overlaps(lhs))
expectEqual(!rhs.isEmpty, rhs.overlaps(rhs))
}
// ClosedRange.contains(_: Range) is only available for stridable bounds
% if Self == 'Range' or (Self == 'ClosedRange' and OtherSelf == 'ClosedRange') or 'Stridable' in Bound:
${TestSuite}.test("contains(${OtherSelf})/semantics")
.forEach(in: containsRangeTests) {
(test) in
if test.lhs.isHalfOpen != ${Self}<${Bound}>._isHalfOpen ||
test.rhs.isHalfOpen != ${OtherSelf}<${Bound}>._isHalfOpen {
return
}
let lhs: ${Self}<${Bound}>
= ${Bound}(test.lhs.lowerBound)${op}${Bound}(test.lhs.upperBound)
let rhs: ${OtherSelf}<${Bound}>
= ${Bound}(test.rhs.lowerBound)${other_op}${Bound}(test.rhs.upperBound)
expectEqual(test.expected, lhs.contains(rhs))
}
% end
% end
${TestSuite}.test("clamped(to:)/semantics")