Skip to content

Commit e81a8bd

Browse files
committed
Add tests / fixes for contains / firstRange(of:)
Better test coverage for both the regex and collection variants of these two methods, plus overloads for contains(_:) to win over Foundation's version from the overlay. Also fixes an error in TwoWaySearcher that tried to offset past the searched string's endIndex; might be a sign of something awry, but tests seem to be otherwise succeeding.
1 parent d6a01e7 commit e81a8bd

File tree

3 files changed

+69
-4
lines changed

3 files changed

+69
-4
lines changed

Sources/_StringProcessing/Algorithms/Algorithms/Contains.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ extension BidirectionalCollection where Element: Comparable {
4646
}
4747
}
4848

49+
// Overload breakers
50+
51+
extension StringProtocol {
52+
@available(SwiftStdlib 5.7, *)
53+
public func contains(_ other: String) -> Bool {
54+
firstRange(of: other) != nil
55+
}
56+
57+
@available(SwiftStdlib 5.7, *)
58+
public func contains(_ other: Substring) -> Bool {
59+
firstRange(of: other) != nil
60+
}
61+
}
62+
4963
// MARK: Regex algorithms
5064

5165
extension BidirectionalCollection where SubSequence == Substring {

Sources/_StringProcessing/Algorithms/Searchers/TwoWaySearcher.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ extension TwoWaySearcher: CollectionSearcher {
4747
for searched: Searched,
4848
in range: Range<Searched.Index>
4949
) -> State {
50+
// FIXME: Is this 'limitedBy' requirement a sign of error?
5051
let criticalIndex = searched.index(
51-
range.lowerBound, offsetBy: criticalIndex)
52+
range.lowerBound, offsetBy: criticalIndex, limitedBy: range.upperBound)
53+
?? range.upperBound
5254
return State(
5355
end: range.upperBound,
5456
index: range.lowerBound,
@@ -66,7 +68,10 @@ extension TwoWaySearcher: CollectionSearcher {
6668
let start = _searchLeft(searched, &state, end)
6769
{
6870
state.index = end
69-
state.criticalIndex = searched.index(end, offsetBy: criticalIndex)
71+
// FIXME: Is this 'limitedBy' requirement a sign of error?
72+
state.criticalIndex = searched.index(
73+
end, offsetBy: criticalIndex, limitedBy: searched.endIndex)
74+
?? searched.endIndex
7075
state.memory = nil
7176
return start..<end
7277
}

Tests/RegexTests/AlgorithmsTests.swift

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import _StringProcessing
1313
import XCTest
1414

1515
// TODO: Protocol-powered testing
16-
class AlgorithmTests: XCTestCase {
16+
class RegexConsumerTests: XCTestCase {
1717

1818
}
1919

@@ -32,7 +32,25 @@ func makeSingleUseSequence<T>(element: T, count: Int) -> UnfoldSequence<T, Void>
3232
}
3333
}
3434

35-
class RegexConsumerTests: XCTestCase {
35+
class AlgorithmTests: XCTestCase {
36+
func testContains() {
37+
XCTAssertTrue("".contains(""))
38+
XCTAssertTrue("abcde".contains(""))
39+
XCTAssertTrue("abcde".contains("abcd"))
40+
XCTAssertTrue("abcde".contains("bcde"))
41+
XCTAssertTrue("abcde".contains("bcd"))
42+
XCTAssertTrue("ababacabababa".contains("abababa"))
43+
44+
XCTAssertFalse("".contains("abcd"))
45+
46+
for start in 0..<9 {
47+
for end in start..<9 {
48+
XCTAssertTrue((0..<10).contains(start...end))
49+
XCTAssertFalse((0..<10).contains(start...10))
50+
}
51+
}
52+
}
53+
3654
func testRanges() {
3755
func expectRanges(
3856
_ string: String,
@@ -48,6 +66,9 @@ class RegexConsumerTests: XCTestCase {
4866
// `IndexingIterator` tests the collection conformance
4967
let actualCol: [Range<Int>] = string[...].ranges(of: regex)[...].map(string.offsets(of:))
5068
XCTAssertEqual(actualCol, expected, file: file, line: line)
69+
70+
let firstRange = string.firstRange(of: regex).map(string.offsets(of:))
71+
XCTAssertEqual(firstRange, expected.first, file: file, line: line)
5172
}
5273

5374
expectRanges("", "", [0..<0])
@@ -68,6 +89,31 @@ class RegexConsumerTests: XCTestCase {
6889
expectRanges("abc", "(a|b)*", [0..<2, 2..<2, 3..<3])
6990
expectRanges("abc", "(b|c)+", [1..<3])
7091
expectRanges("abc", "(b|c)*", [0..<0, 1..<3, 3..<3])
92+
93+
func expectStringRanges(
94+
_ input: String,
95+
_ pattern: String,
96+
_ expected: [Range<Int>],
97+
file: StaticString = #file, line: UInt = #line
98+
) {
99+
let actualSeq: [Range<Int>] = input.ranges(of: pattern).map(input.offsets(of:))
100+
XCTAssertEqual(actualSeq, expected, file: file, line: line)
101+
102+
// `IndexingIterator` tests the collection conformance
103+
let actualCol: [Range<Int>] = input.ranges(of: pattern)[...].map(input.offsets(of:))
104+
XCTAssertEqual(actualCol, expected, file: file, line: line)
105+
106+
let firstRange = input.firstRange(of: pattern).map(input.offsets(of:))
107+
XCTAssertEqual(firstRange, expected.first, file: file, line: line)
108+
}
109+
110+
expectStringRanges("", "", [0..<0])
111+
expectStringRanges("abcde", "", [0..<0, 1..<1, 2..<2, 3..<3, 4..<4, 5..<5])
112+
expectStringRanges("abcde", "abcd", [0..<4])
113+
expectStringRanges("abcde", "bcde", [1..<5])
114+
expectStringRanges("abcde", "bcd", [1..<4])
115+
expectStringRanges("ababacabababa", "abababa", [6..<13])
116+
expectStringRanges("ababacabababa", "aba", [0..<3, 6..<9, 10..<13])
71117
}
72118

73119
func testSplit() {

0 commit comments

Comments
 (0)