Skip to content

Commit d549b4e

Browse files
author
Tim Vermeulen
committed
Add firstMatch/lastMatch and match find/replace
1 parent 2c8abbc commit d549b4e

File tree

7 files changed

+232
-10
lines changed

7 files changed

+232
-10
lines changed

Sources/_StringProcessing/Algorithms/Algorithms/Replace.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ extension RangeReplaceableCollection {
5050
maxReplacements: maxReplacements)
5151
}
5252

53-
public mutating func replace<Searcher: CollectionSearcher, R: Collection>(
53+
public mutating func replace<
54+
Searcher: CollectionSearcher, Replacement: Collection
55+
>(
5456
_ searcher: Searcher,
55-
with replacement: R,
57+
with replacement: Replacement,
5658
maxReplacements: Int = .max
57-
) where Searcher.Searched == SubSequence, R.Element == Element {
59+
) where Searcher.Searched == SubSequence, Replacement.Element == Element {
5860
self = replacing(
5961
searcher,
6062
with: replacement,

Sources/_StringProcessing/Algorithms/Consumers/RegexConsumer.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public struct RegexConsumer<
3434
// well, taking advantage of the fact that the captures can be ignored
3535

3636
extension RegexConsumer: MatchingCollectionConsumer {
37+
public typealias Match = Capture
38+
3739
public func matchingConsuming(
3840
_ consumed: Consumed, in range: Range<Consumed.Index>
3941
) -> (Capture, String.Index)? {
@@ -76,12 +78,12 @@ extension RegexConsumer: MatchingStatelessCollectionSearcher {
7678
}
7779

7880
// TODO: Bake in search-back to engine too
79-
extension RegexConsumer: BackwardStatelessCollectionSearcher {
81+
extension RegexConsumer: BackwardMatchingStatelessCollectionSearcher {
8082
public typealias BackwardSearched = Consumed
8183

82-
public func searchBack(
84+
public func matchingSearchBack(
8385
_ searched: BackwardSearched, in range: Range<Searched.Index>
84-
) -> Range<String.Index>? {
85-
ConsumerSearcher(consumer: self).searchBack(searched, in: range)
86+
) -> (Capture, Range<String.Index>)? {
87+
ConsumerSearcher(consumer: self).matchingSearchBack(searched, in: range)
8688
}
8789
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021-2022 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+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
// MARK: `CollectionSearcher` algorithms
13+
14+
extension Collection {
15+
public func firstMatch<S: MatchingCollectionSearcher>(
16+
of searcher: S
17+
) -> (S.Match, Range<S.Searched.Index>)? where S.Searched == Self {
18+
var state = searcher.state(for: self, in: startIndex..<endIndex)
19+
return searcher.matchingSearch(self, &state)
20+
}
21+
}
22+
23+
extension BidirectionalCollection {
24+
public func lastMatch<S: BackwardMatchingCollectionSearcher>(
25+
of searcher: S
26+
) -> (S.Match, Range<S.BackwardSearched.Index>)?
27+
where S.BackwardSearched == Self
28+
{
29+
var state = searcher.backwardState(for: self, in: startIndex..<endIndex)
30+
return searcher.matchingSearchBack(self, &state)
31+
}
32+
}
33+
34+
// MARK: Regex algorithms
35+
36+
extension BidirectionalCollection where SubSequence == Substring {
37+
public func firstMatch<Capture>(
38+
of regex: Regex<Capture>
39+
) -> (Capture, Range<String.Index>)? {
40+
firstMatch(of: RegexConsumer(regex))
41+
}
42+
43+
public func lastMatch<Capture>(
44+
of regex: Regex<Capture>
45+
) -> (Capture, Range<String.Index>)? {
46+
lastMatch(of: RegexConsumer(regex))
47+
}
48+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021-2022 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+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
// MARK: `MatchingCollectionSearcher` algorithms
13+
14+
extension RangeReplaceableCollection {
15+
public func replacing<
16+
Searcher: MatchingCollectionSearcher, Replacement: Collection
17+
>(
18+
_ searcher: Searcher,
19+
with replacement: (Searcher.Match,
20+
Range<Searcher.Searched.Index>) -> Replacement,
21+
subrange: Range<Index>,
22+
maxReplacements: Int = .max
23+
) -> Self where Searcher.Searched == SubSequence,
24+
Replacement.Element == Element
25+
{
26+
precondition(maxReplacements >= 0)
27+
28+
var index = subrange.lowerBound
29+
var result = Self()
30+
result.append(contentsOf: self[..<index])
31+
32+
for (match, range) in self[subrange].matches(of: searcher)
33+
.prefix(maxReplacements)
34+
{
35+
result.append(contentsOf: self[index..<range.lowerBound])
36+
result.append(contentsOf: replacement(match, range))
37+
index = range.upperBound
38+
}
39+
40+
result.append(contentsOf: self[index...])
41+
return result
42+
}
43+
44+
public func replacing<
45+
Searcher: MatchingCollectionSearcher, Replacement: Collection
46+
>(
47+
_ searcher: Searcher,
48+
with replacement: (Searcher.Match,
49+
Range<Searcher.Searched.Index>) -> Replacement,
50+
maxReplacements: Int = .max
51+
) -> Self where Searcher.Searched == SubSequence,
52+
Replacement.Element == Element
53+
{
54+
replacing(
55+
searcher,
56+
with: replacement,
57+
subrange: startIndex..<endIndex,
58+
maxReplacements: maxReplacements)
59+
}
60+
61+
public mutating func replace<
62+
Searcher: MatchingCollectionSearcher, Replacement: Collection
63+
>(
64+
_ searcher: Searcher,
65+
with replacement: (Searcher.Match,
66+
Range<Searcher.Searched.Index>) -> Replacement,
67+
maxReplacements: Int = .max
68+
) where Searcher.Searched == SubSequence, Replacement.Element == Element {
69+
self = replacing(
70+
searcher,
71+
with: replacement,
72+
maxReplacements: maxReplacements)
73+
}
74+
}
75+
76+
// MARK: Regex algorithms
77+
78+
extension RangeReplaceableCollection where SubSequence == Substring {
79+
public func replacing<Capture, Replacement: Collection>(
80+
_ regex: Regex<Capture>,
81+
with replacement: (Capture, Range<String.Index>) -> Replacement,
82+
subrange: Range<Index>,
83+
maxReplacements: Int = .max
84+
) -> Self where Replacement.Element == Element {
85+
replacing(
86+
RegexConsumer(regex),
87+
with: replacement,
88+
subrange: subrange,
89+
maxReplacements: maxReplacements)
90+
}
91+
92+
public func replacing<Capture, Replacement: Collection>(
93+
_ regex: Regex<Capture>,
94+
with replacement: (Capture, Range<String.Index>) -> Replacement,
95+
maxReplacements: Int = .max
96+
) -> Self where Replacement.Element == Element {
97+
replacing(
98+
regex,
99+
with: replacement,
100+
subrange: startIndex..<endIndex,
101+
maxReplacements: maxReplacements)
102+
}
103+
104+
public mutating func replace<Capture, Replacement: Collection>(
105+
_ regex: Regex<Capture>,
106+
with replacement: (Capture, Range<String.Index>) -> Replacement,
107+
maxReplacements: Int = .max
108+
) where Replacement.Element == Element {
109+
self = replacing(
110+
regex,
111+
with: replacement,
112+
maxReplacements: maxReplacements)
113+
}
114+
}

Sources/_StringProcessing/Algorithms/Matching/Matches.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ extension MatchesCollection: Collection {
6363
// TODO: Custom `SubSequence` for the sake of more efficient slice iteration
6464

6565
public struct Index {
66-
var match: (value: Searcher.Match, range: Range<Searcher.Searched.Index>)?
66+
var match: (value: Searcher.Match, range: Range<Base.Index>)?
6767
var state: Searcher.State
6868
}
6969

@@ -157,4 +157,38 @@ extension ReversedMatchesCollection: Sequence {
157157
}
158158
}
159159

160-
//// TODO: `Collection` conformance
160+
// TODO: `Collection` conformance
161+
162+
// MARK: `CollectionSearcher` algorithms
163+
164+
extension Collection {
165+
public func matches<S: MatchingCollectionSearcher>(
166+
of searcher: S
167+
) -> MatchesCollection<S> where S.Searched == Self {
168+
MatchesCollection(base: self, searcher: searcher)
169+
}
170+
}
171+
172+
extension BidirectionalCollection {
173+
public func matchesFromBack<S: BackwardMatchingCollectionSearcher>(
174+
of searcher: S
175+
) -> ReversedMatchesCollection<S> where S.BackwardSearched == Self {
176+
ReversedMatchesCollection(base: self, searcher: searcher)
177+
}
178+
}
179+
180+
// MARK: Regex algorithms
181+
182+
extension BidirectionalCollection where SubSequence == Substring {
183+
public func matches<Capture>(
184+
of regex: Regex<Capture>
185+
) -> MatchesCollection<RegexConsumer<Self, Capture>> {
186+
matches(of: RegexConsumer(regex))
187+
}
188+
189+
public func matchesFromBack<Capture>(
190+
of regex: Regex<Capture>
191+
) -> ReversedMatchesCollection<RegexConsumer<Self, Capture>> {
192+
matchesFromBack(of: RegexConsumer(regex))
193+
}
194+
}

Sources/_StringProcessing/Algorithms/Matching/MatchingCollectionSearcher.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public protocol MatchingStatelessCollectionSearcher:
3636
}
3737

3838
extension MatchingStatelessCollectionSearcher {
39-
// for disambiguation
39+
// for disambiguation between the `MatchingCollectionSearcher` and
40+
// `StatelessCollectionSearcher` overloads
4041
public func search(
4142
_ searched: Searched,
4243
_ state: inout State
@@ -96,6 +97,13 @@ public protocol BackwardMatchingStatelessCollectionSearcher:
9697
}
9798

9899
extension BackwardMatchingStatelessCollectionSearcher {
100+
public func searchBack(
101+
_ searched: BackwardSearched,
102+
in range: Range<BackwardSearched.Index>
103+
) -> Range<BackwardSearched.Index>? {
104+
matchingSearchBack(searched, in: range)?.1
105+
}
106+
99107
public func matchingSearchBack(
100108
_ searched: BackwardSearched,
101109
_ state: inout BackwardState) -> (Match, Range<BackwardSearched.Index>)?

Tests/RegexTests/AlgorithmsTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ class RegexConsumerTests: XCTestCase {
114114
expectReplace("aab", "a+", "X", "Xb")
115115
expectReplace("aab", "a*", "X", "XXbX")
116116
}
117+
118+
func testMatches() {
119+
let regex = Regex(OneOrMore(.digit).capture { 2 * Int($0)! })
120+
let str = "foo 160 bar 99 baz"
121+
XCTAssertEqual(str.matches(of: regex).map(\.0.1), [320, 198])
122+
}
123+
124+
func testMatchReplace() {
125+
let regex = Regex(OneOrMore(.digit).capture { Int($0)! })
126+
let str = "foo 160 bar 99 baz"
127+
XCTAssertEqual(
128+
str.replacing(regex, with: { match, _ in String(match.1, radix: 8) }),
129+
"foo 240 bar 143 baz")
130+
}
117131

118132
func testAdHoc() {
119133
let r = try! Regex("a|b+")

0 commit comments

Comments
 (0)