Files
swift-mirror/validation-test/stdlib/RangeSet.swift
Nate Cook 846a861660 Improve RangeSet initialization performance (#75089)
When initializing a range set with a group of overlapping, identical,
or empty ranges, the initializer can exhibit poor performance due
to removing the unneeded ranges during processing. This change uses
a partitioning scheme instead, only removing the unnecessary ranges
at the end of initialization.
2024-07-12 15:01:38 -05:00

677 lines
20 KiB
Swift

// RUN: %target-run-simple-swift
// REQUIRES: executable_test
import StdlibUnittest
import StdlibCollectionUnittest
@available(SwiftStdlib 6.0, *)
extension RangeSet: ExpressibleByArrayLiteral {
public init(arrayLiteral elements: Range<Bound>...) {
self.init(elements)
}
}
extension Collection {
func every(_ n: Int) -> [Element] {
sequence(first: startIndex) { i in
self.index(i, offsetBy: n, limitedBy: self.endIndex)
}.map { self[$0] }
}
}
let RangeSetTests = TestSuite("RangeSet")
if #available(SwiftStdlib 6.0, *) {
let parent = -200..<200
let source: RangeSet = [1..<5, 8..<10, 20..<22, 27..<29]
let letterString = "ABCdefGHIjklMNOpqrStUvWxyz"
let lowercaseLetters = letterString.filter { $0.isLowercase }
let uppercaseLetters = letterString.filter { $0.isUppercase }
func buildRandomRangeSet(iterations: Int = 100) -> RangeSet<Int> {
var set = RangeSet<Int>()
for _ in 0..<100 {
var (a, b) = (Int.random(in: -100...100), Int.random(in: -100...100))
if (a > b) { swap(&a, &b) }
if Double.random(in: 0..<1) > 0.3 {
set.insert(contentsOf: a..<b)
} else {
set.remove(contentsOf: a..<b)
}
}
return set
}
RangeSetTests.test("initialization") {
// Test coalescing and elimination of empty ranges
do {
let empty = RangeSet(Array(repeating: 0..<0, count: 100))
expectTrue(empty.isEmpty)
let repeated = RangeSet(Array(repeating: 0..<3, count: 100))
expectEqual(repeated, [0..<3])
let singleAfterEmpty = RangeSet(Array(repeating: 0..<0, count: 100) + [0..<3])
expectEqual(singleAfterEmpty, [0..<3])
let contiguousRanges = (0..<100).map { $0 ..< ($0 + 1) }
expectEqual(RangeSet(contiguousRanges), [0..<100])
expectEqual(RangeSet(contiguousRanges.shuffled()), [0..<100])
}
// The `buildRandomRangeSet()` function builds a range set via additions
// and removals. This function creates an array of potentially empty or
// overlapping ranges that can be used to initialize a range set.
func randomRanges() -> [Range<Int>] {
(0..<100).map { _ in
let low = Int.random(in: 0...100)
let count = Int.random(in: 0...20)
return low ..< (low + count)
}
}
for _ in 0..<1000 {
let ranges = randomRanges()
let set = RangeSet(ranges)
// Manually construct a range set for comparison
var comparison = RangeSet<Int>()
for r in ranges {
comparison.insert(contentsOf: r)
}
expectEqual(set, comparison)
}
}
RangeSetTests.test("contains") {
expectFalse(source.contains(0))
expectTrue(source.contains(1))
expectTrue(source.contains(4))
expectFalse(source.contains(5))
expectTrue(source.contains(28))
expectFalse(source.contains(29))
for _ in 0..<1000 {
let set = buildRandomRangeSet()
for i in parent.indices[set] {
expectTrue(set.contains(i))
}
for i in parent.indices.removingSubranges(set) {
expectFalse(set.contains(i))
}
}
}
RangeSetTests.test("insertions") {
do {
// Overlap from middle to middle
var s = source
s.insert(contentsOf: 3..<21)
expectEqualSequence(s.ranges, [1..<22, 27..<29])
}
do {
// insert in middle
var s = source
s.insert(contentsOf: 13..<15)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 13..<15, 20..<22, 27..<29])
}
do {
// extend a range
var s = source
s.insert(contentsOf: 22..<25)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 20..<25, 27..<29])
}
do {
// extend at beginning of range
var s = source
s.insert(contentsOf: 17..<20)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 17..<22, 27..<29])
}
do {
// insert at the beginning
var s = source
s.insert(contentsOf: -10 ..< -5)
expectEqualSequence(
s.ranges,
[-10 ..< -5, 1..<5, 8..<10, 20..<22, 27..<29]
)
}
do {
// insert at the end
var s = source
s.insert(contentsOf: 35 ..< 40)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 20..<22, 27..<29, 35..<40])
}
do {
// Overlap multiple ranges
var s = source
s.insert(contentsOf: 0..<21)
expectEqualSequence(s.ranges, [0..<22, 27..<29])
}
do {
// Insert at end of range
var s = source
s.insert(22, within: parent)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 20..<23, 27..<29])
}
do {
// Insert between ranges
var s = source
s.insert(14, within: parent)
expectEqualSequence(s.ranges, [1..<5, 8..<10, 14..<15, 20..<22, 27..<29])
}
}
RangeSetTests.test("removals") {
do {
var s = source
s.remove(contentsOf: 4..<28)
expectEqualSequence(s.ranges, [1..<4, 28..<29])
s.remove(3, within: parent)
expectEqualSequence(s.ranges, [1..<3, 28..<29])
}
do {
var s = source
s.remove(contentsOf: 5..<7)
expectEqualSequence(s.ranges, source.ranges)
s.remove(5, within: parent)
expectEqualSequence(s.ranges, source.ranges)
s.remove(contentsOf: 5..<9)
expectEqualSequence(s.ranges, [1..<5, 9..<10, 20..<22, 27..<29])
}
do {
var s = source
s.remove(contentsOf: 6..<8)
expectEqualSequence(s.ranges, source.ranges)
s.remove(7, within: parent)
expectEqualSequence(s.ranges, source.ranges)
s.remove(contentsOf: 4..<8)
expectEqualSequence(s.ranges, [1..<4, 8..<10, 20..<22, 27..<29])
}
}
RangeSetTests.test("invariants") {
for _ in 0..<1000 {
let set = buildRandomRangeSet()
// No empty ranges allowed
expectTrue(set.ranges.allSatisfy { !$0.isEmpty })
// No overlapping / out-of-order ranges allowed
let adjacentRanges = zip(set.ranges, set.ranges.dropFirst())
expectTrue(adjacentRanges.allSatisfy { $0.upperBound < $1.lowerBound })
}
}
RangeSetTests.test("union") {
func unionViaSet(
_ s1: RangeSet<Int>,
_ s2: RangeSet<Int>
) -> RangeSet<Int> {
let set1 = Set(parent.indices[s1])
let set2 = Set(parent.indices[s2])
return RangeSet(set1.union(set2), within: parent)
}
func testUnion(
_ set1: RangeSet<Int>,
_ set2: RangeSet<Int>,
expect union: RangeSet<Int>
) {
expectEqual(set1.union(set2), union)
expectEqual(set2.union(set1), union)
var set3 = set1
set3.formUnion(set2)
expectEqual(set3, union)
set3 = set2
set3.formUnion(set1)
expectEqual(set3, union)
}
// Simple tests
testUnion([0..<5, 9..<14],
[1..<3, 4..<6, 8..<12],
expect: [0..<6, 8..<14])
testUnion([10..<20, 50..<60],
[15..<55, 58..<65],
expect: [10..<65])
// Test with upper bound / lower bound equality
testUnion([10..<20, 30..<40],
[15..<30, 40..<50],
expect: [10..<50])
for _ in 0..<100 {
let set1 = buildRandomRangeSet()
let set2 = buildRandomRangeSet()
testUnion(set1, set2,
expect: unionViaSet(set1, set2))
}
}
RangeSetTests.test("intersection") {
func intersectionViaSet(
_ s1: RangeSet<Int>,
_ s2: RangeSet<Int>
) -> RangeSet<Int> {
let set1 = Set(parent.indices[s1])
let set2 = Set(parent.indices[s2])
return RangeSet(set1.intersection(set2), within: parent)
}
do {
// Simple test
let set1: RangeSet = [0..<5, 9..<14]
let set2: RangeSet = [1..<3, 4..<6, 8..<12]
let intersection: RangeSet = [1..<3, 4..<5, 9..<12]
expectEqual(set1.intersection(set2), intersection)
expectEqual(set2.intersection(set1), intersection)
}
do {
// Test with upper bound / lower bound equality
let set1: RangeSet = [10..<20, 30..<40]
let set2: RangeSet = [15..<30, 40..<50]
let intersection: RangeSet = [15..<20]
expectEqual(set1.intersection(set2), intersection)
expectEqual(set2.intersection(set1), intersection)
}
for _ in 0..<100 {
let set1 = buildRandomRangeSet()
let set2 = buildRandomRangeSet()
let rangeSetIntersection = set1.intersection(set2)
let stdlibSetIntersection = intersectionViaSet(set1, set2)
expectEqual(rangeSetIntersection, stdlibSetIntersection)
}
}
RangeSetTests.test("symmetricDifference") {
func symmetricDifferenceViaSet(
_ s1: RangeSet<Int>,
_ s2: RangeSet<Int>
) -> RangeSet<Int> {
let set1 = Set(parent.indices[s1])
let set2 = Set(parent.indices[s2])
return RangeSet(set1.symmetricDifference(set2), within: parent)
}
do {
// Simple test
let set1: RangeSet = [0..<5, 9..<14]
let set2: RangeSet = [1..<3, 4..<6, 8..<12]
let difference: RangeSet = [0..<1, 3..<4, 5..<6, 8..<9, 12..<14]
expectEqual(set1.symmetricDifference(set2), difference)
expectEqual(set2.symmetricDifference(set1), difference)
}
do {
// Test with upper bound / lower bound equality
let set1: RangeSet = [10..<20, 30..<40]
let set2: RangeSet = [15..<30, 40..<50]
let difference: RangeSet = [10..<15, 20..<50]
expectEqual(set1.symmetricDifference(set2), difference)
expectEqual(set2.symmetricDifference(set1), difference)
}
for _ in 0..<100 {
let set1 = buildRandomRangeSet()
let set2 = buildRandomRangeSet()
let rangeSetDifference = set1.symmetricDifference(set2)
let stdlibSetDifference = symmetricDifferenceViaSet(set1, set2)
expectEqual(rangeSetDifference, stdlibSetDifference)
}
}
RangeSetTests.test("isDisjoint") {
func isDisjointViaSet(_ s1: RangeSet<Int>, _ s2: RangeSet<Int>) -> Bool {
let set1 = Set(parent.indices[s1])
let set2 = Set(parent.indices[s2])
return set1.isDisjoint(with: set2)
}
do {
// Simple test
let set1: RangeSet = [0..<5, 9..<14]
let set2: RangeSet = [1..<3, 4..<6, 8..<12]
let set3: RangeSet = [6..<9, 14..<20]
expectFalse(set1.isDisjoint(set2))
expectFalse(set2.isDisjoint(set1))
expectTrue(set1.isDisjoint(set3))
expectTrue(set3.isDisjoint(set1))
}
for _ in 0..<100 {
let set1 = buildRandomRangeSet()
let set2 = buildRandomRangeSet()
let rangeSetDisjoint = set1.isDisjoint(set2)
let stdlibSetDisjoint = isDisjointViaSet(set1, set2)
expectEqual(rangeSetDisjoint, stdlibSetDisjoint)
}
}
RangeSetTests.test("isSubset") {
func isSubsetViaSet(_ s1: RangeSet<Int>, _ s2: RangeSet<Int>) -> Bool {
let set1 = Set(parent.indices[s1])
let set2 = Set(parent.indices[s2])
return set1.isSubset(of: set2)
}
do {
// Simple test
let set1: RangeSet = [0..<5, 9..<14]
let set2: RangeSet = [1..<3, 4..<6, 8..<12]
let set3: RangeSet = [6..<9, 14..<20]
let set4: RangeSet = [1..<2, 10..<12]
expectFalse(set1.isSubset(of: set2))
expectFalse(set2.isSubset(of: set1))
expectFalse(set1.isSubset(of: set3))
expectFalse(set3.isSubset(of: set1))
expectFalse(set1.isSubset(of: set4))
expectFalse(set2.isSubset(of: set4))
expectTrue(set4.isSubset(of: set1))
expectTrue(set4.isSubset(of: set2))
}
for _ in 0..<100 {
let set1 = buildRandomRangeSet()
let set2 = buildRandomRangeSet()
let rangeSetSubset = set1.isSubset(of: set2)
let stdlibSetSubset = isSubsetViaSet(set1, set2)
expectEqual(rangeSetSubset, stdlibSetSubset)
}
}
RangeSetTests.test("indices(of:/where:)") {
let a = [1, 2, 3, 4, 3, 3, 4, 5, 3, 4, 3, 3, 3]
let indices = a.indices(of: 3)
expectEqual(indices, [2..<3, 4..<6, 8..<9, 10..<13])
let allTheThrees = a[indices]
expectEqual(allTheThrees.count, 7)
expectTrue(allTheThrees.allSatisfy { $0 == 3 })
expectEqual(Array(allTheThrees), Array(repeating: 3, count: 7))
let lowerIndices = letterString.indices(where: { $0.isLowercase })
let lowerOnly = letterString[lowerIndices]
expectEqualSequence(lowerOnly, lowercaseLetters)
expectEqualSequence(lowerOnly.reversed(), lowercaseLetters.reversed())
let upperOnly = letterString.removingSubranges(lowerIndices)
expectEqualSequence(upperOnly, uppercaseLetters)
expectEqualSequence(upperOnly.reversed(), uppercaseLetters.reversed())
}
RangeSetTests.test("removeSubranges") {
var a = [1, 2, 3, 4, 3, 3, 4, 5, 3, 4, 3, 3, 3]
let indices = a.indices(of: 3)
a.removeSubranges(indices)
expectEqual(a, [1, 2, 4, 4, 5, 4])
var numbers = Array(1...20)
numbers.removeSubranges(RangeSet([2..<5, 10..<15, 18..<20]))
expectEqual(numbers, [1, 2, 6, 7, 8, 9, 10, 16, 17, 18])
numbers = Array(1...20)
numbers.removeSubranges([])
expectEqual(numbers, Array(1...20))
let sameNumbers = numbers.removingSubranges([])
expectEqualSequence(numbers, sameNumbers)
let noNumbers = numbers.removingSubranges(RangeSet(numbers.indices))
expectEqualSequence(EmptyCollection(), noNumbers)
var str = letterString
let lowerIndices = str.indices(where: { $0.isLowercase })
let upperOnly = str.removingSubranges(lowerIndices)
expectEqualSequence(upperOnly, uppercaseLetters)
str.removeSubranges(lowerIndices)
expectEqualSequence(str, uppercaseLetters)
}
RangeSetTests.test("moveSubranges/rangeset") {
// Move before
var numbers = Array(1...20)
let range1 = numbers.moveSubranges(RangeSet([10..<15, 18..<20]), to: 4)
expectEqual(range1, 4..<11)
expectEqual(numbers, [
1, 2, 3, 4,
11, 12, 13, 14, 15,
19, 20,
5, 6, 7, 8, 9, 10, 16, 17, 18])
// Move to start
numbers = Array(1...20)
let range2 = numbers.moveSubranges(RangeSet([10..<15, 18..<20]), to: 0)
expectEqual(range2, 0..<7)
expectEqual(numbers, [
11, 12, 13, 14, 15,
19, 20,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 18])
// Move to end
numbers = Array(1...20)
let range3 = numbers.moveSubranges(RangeSet([10..<15, 18..<20]), to: 20)
expectEqual(range3, 13..<20)
expectEqual(numbers, [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 16, 17, 18,
11, 12, 13, 14, 15,
19, 20,
])
// Move to middle of selected elements
numbers = Array(1...20)
let range4 = numbers.moveSubranges(RangeSet([10..<15, 18..<20]), to: 14)
expectEqual(range4, 10..<17)
expectEqual(numbers, [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15,
19, 20,
16, 17, 18])
// Move none
numbers = Array(1...20)
let range5 = numbers.moveSubranges(RangeSet(), to: 10)
expectEqual(range5, 10..<10)
expectEqual(numbers, Array(1...20))
}
RangeSetTests.test("moveSubranges/noCOW") {
let numbers = Array(1...20)
expectNoCopyOnWrite(numbers) { numbers in
numbers.moveSubranges(RangeSet([10..<15, 18..<20]), to: 4)
}
expectNoCopyOnWrite(numbers) { numbers in
numbers.removeSubranges(RangeSet([2..<5, 10..<15, 18..<20]))
}
}
RangeSetTests.test("DiscontiguousSliceSequence") {
let initial = 1...100
// Build an array of ranges that include alternating groups of 5 elements
// e.g. 1...5, 11...15, etc
let rangeStarts = initial.indices.every(10)
let rangeEnds = rangeStarts.compactMap {
initial.index($0, offsetBy: 5, limitedBy: initial.endIndex)
}
let ranges = zip(rangeStarts, rangeEnds).map(Range.init)
let set = RangeSet(ranges)
let slice = initial[set]
// Test various public, underscored members for the slice's
// Sequence/Collection conformance
expectTrue(slice.contains(2))
expectFalse(slice.contains(7))
expectEqualSequence(Array(slice), slice)
expectEqual(slice.firstIndex(of: 2), slice.index(after: slice.startIndex))
expectEqual(slice.firstIndex(of: 7), nil)
expectEqual(slice.lastIndex(of: 2), slice.index(after: slice.startIndex))
expectEqual(slice.lastIndex(of: 7), nil)
}
RangeSetTests.test("DiscontiguousSliceSlicing") {
let initial = 1...100
// Build an array of ranges that include alternating groups of 5 elements
// e.g. 1...5, 11...15, etc
let rangeStarts = initial.indices.every(10)
let rangeEnds = rangeStarts.compactMap {
initial.index($0, offsetBy: 5, limitedBy: initial.endIndex)
}
let ranges = zip(rangeStarts, rangeEnds).map(Range.init)
// Create a collection of the elements represented by `ranges` without
// using `RangeSet`
let chosenElements = ranges.map { initial[$0] }.joined()
let set = RangeSet(ranges)
let discontiguousSlice = initial[set]
expectEqualSequence(discontiguousSlice, chosenElements)
for (chosenIdx, disIdx) in
zip(chosenElements.indices, discontiguousSlice.indices)
{
expectEqualSequence(
chosenElements[chosenIdx...],
discontiguousSlice[disIdx...]
)
expectEqualSequence(
chosenElements[..<chosenIdx],
discontiguousSlice[..<disIdx]
)
for (chosenUpper, disUpper) in
zip(
chosenElements.indices[chosenIdx...],
discontiguousSlice.indices[disIdx...]
)
{
expectEqualSequence(
Array(chosenElements[chosenIdx..<chosenUpper]),
Array(discontiguousSlice[disIdx..<disUpper]))
}
}
}
RangeSetTests.test("DiscontiguousSliceIntermediateIndices") {
do {
let string = "Cafe\u{301} A"
let indexOfAccent = string.unicodeScalars
.index(string.startIndex, offsetBy: 4)
var set = RangeSet<String.Index>()
set.insert(indexOfAccent, within: string)
expectEqualSequence(string[set], ["\u{301}"])
expectEqualSequence(string[set].reversed(), ["\u{301}"])
set.insert(string.startIndex, within: string)
expectEqualSequence(string[set], ["C", "\u{301}"])
expectEqualSequence(string[set].reversed(), ["\u{301}", "C"])
set.insert(string.index(before: string.endIndex), within: string)
expectEqualSequence(string[set], ["C", "\u{301}", "A"])
expectEqualSequence(string[set].reversed(), ["A", "\u{301}", "C"])
let indexOfE = string.index(string.startIndex, offsetBy: 3)
let rangeOfE = indexOfE ..< indexOfAccent
set.insert(contentsOf: rangeOfE)
expectEqualSequence(string[set], ["C", "e\u{301}", "A"])
expectEqualSequence(string[set].reversed(), ["A", "e\u{301}", "C"])
}
do {
let string = Array(
repeating: "\u{1F1E8}\u{1F1ED}\u{1F1FA}\u{1F1E6}",
count: 4
).joined()
let scalars = Array(string.unicodeScalars.indices)
print(scalars.count)
let r1 = string.startIndex ..< scalars[4]
let r2 = scalars[5] ..< scalars[9]
let r3 = scalars[10] ..< string.endIndex
let set = RangeSet([r1, r2, r3])
let expected: [Character] = [
"\u{1F1E8}\u{1F1ED}", "\u{1F1FA}\u{1F1E6}",
"\u{1F1ED}\u{1F1FA}", "\u{1F1E6}\u{1F1E8}",
"\u{1F1FA}\u{1F1E6}", "\u{1F1E8}\u{1F1ED}", "\u{1F1FA}\u{1F1E6}"
]
expectEqualSequence(string[set], expected)
expectEqualSequence(string[set].reversed(), expected.reversed())
}
}
RangeSetTests.test("InsertionReturningPreviousContainment") {
do {
var s = RangeSet<Int>()
expectTrue(s.insert(20, within: parent))
}
do {
var s = source
expectTrue(s.insert(-100, within: parent))
}
do {
var s = source
expectFalse(s.insert(20, within: parent))
}
do {
var s = source
expectFalse(s.insert(21, within: parent))
}
do {
var s = source
expectTrue(s.insert(22, within: parent))
}
do {
var s = source
expectTrue(s.insert(23, within: parent))
}
do {
var s = source
expectTrue(s.insert(26, within: parent))
}
do {
var s = source
expectFalse(s.insert(27, within: parent))
}
do {
var s = source
expectFalse(s.insert(28, within: parent))
}
do {
var s = source
expectTrue(s.insert(29, within: parent))
}
do {
var s = source
expectTrue(s.insert(100, within: parent))
}
}
}
runAllTests()