-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Regex.swift
292 lines (257 loc) · 10.6 KB
/
Regex.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
// Originally from: https://github.com/sharplet/Regex & https://github.com/FlineDev/HandySwift (modified).
import Foundation
/// `Regex` is a swifty regex engine built on top of the NSRegularExpression api.
public struct Regex {
/// The recommended default options passed to any Regex if not otherwise specified.
public static let defaultOptions: Options = [.anchorsMatchLines]
// MARK: - Properties
private let regularExpression: NSRegularExpression
/// The regex patterns string.
public let pattern: String
/// The regex options.
public let options: Options
// MARK: - Initializers
/// Create a `Regex` based on a pattern string.
///
/// If `pattern` is not a valid regular expression, an error is thrown
/// describing the failure.
///
/// - parameters:
/// - pattern: A pattern string describing the regex.
/// - options: Configure regular expression matching options.
/// For details, see `Regex.Options`.
///
/// - throws: A value of `ErrorType` describing the invalid regular expression.
public init(_ pattern: String, options: Options = defaultOptions) throws {
self.pattern = pattern
self.options = options
regularExpression = try NSRegularExpression(
pattern: pattern,
options: options.toNSRegularExpressionOptions
)
}
// MARK: - Methods: Matching
/// Returns `true` if the regex matches `string`, otherwise returns `false`.
///
/// - parameter string: The string to test.
///
/// - returns: `true` if the regular expression matches, otherwise `false`.
public func matches(_ string: String) -> Bool {
firstMatch(in: string) != nil
}
/// If the regex matches `string`, returns a `Match` describing the
/// first matched string and any captures. If there are no matches, returns
/// `nil`.
///
/// - parameter string: The string to match against.
///
/// - returns: An optional `Match` describing the first match, or `nil`.
public func firstMatch(in string: String) -> Match? {
let firstMatch = regularExpression
.firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
.map { Match(result: $0, in: string) }
return firstMatch
}
/// If the regex matches `string`, returns an array of `Match`, describing
/// every match inside `string`. If there are no matches, returns an empty
/// array.
///
/// - parameter string: The string to match against.
///
/// - returns: An array of `Match` describing every match in `string`.
public func matches(in string: String) -> [Match] {
let matches = regularExpression
.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
.map { Match(result: $0, in: string) }
return matches
}
// MARK: Replacing
/// Returns a new string where each substring matched by `regex` is replaced
/// with `template`.
///
/// The template string may be a literal string, or include template variables:
/// the variable `$0` will be replaced with the entire matched substring, `$1`
/// with the first capture group, etc.
///
/// For example, to include the literal string "$1" in the replacement string,
/// you must escape the "$": `\$1`.
///
/// - parameters:
/// - regex: A regular expression to match against `self`.
/// - template: A template string used to replace matches.
/// - count: The maximum count of matches to replace, beginning with the first match.
///
/// - returns: A string with all matches of `regex` replaced by `template`.
public func replacingMatches(in input: String, with template: String, count: Int? = nil) -> String {
var output = input
let matches = self.matches(in: input)
let rangedMatches = Array(matches[0 ..< min(matches.count, count ?? .max)])
for match in rangedMatches.reversed() {
let replacement = match.string(applyingTemplate: template)
output.replaceSubrange(match.range, with: replacement)
}
return output
}
}
// MARK: - CustomStringConvertible
extension Regex: CustomStringConvertible {
/// Returns a string describing the regex using its pattern string.
public var description: String {
"/\(regularExpression.pattern)/\(options)"
}
}
// MARK: - Equatable
extension Regex: Equatable {
/// Determines the equality of to `Regex`` instances.
/// Two `Regex` are considered equal, if both the pattern string and the options
/// passed on initialization are equal.
public static func == (lhs: Regex, rhs: Regex) -> Bool {
lhs.regularExpression.pattern == rhs.regularExpression.pattern &&
lhs.regularExpression.options == rhs.regularExpression.options
}
}
// MARK: - Hashable
extension Regex: Hashable {
/// Manages hashing of the `Regex` instance.
public func hash(into hasher: inout Hasher) {
hasher.combine(pattern)
hasher.combine(options)
}
}
// MARK: - Options
extension Regex {
/// `Options` defines alternate behaviours of regular expressions when matching.
public struct Options: OptionSet {
// MARK: - Properties
/// Ignores the case of letters when matching.
public static let ignoreCase = Options(rawValue: 1)
/// Ignore any metacharacters in the pattern, treating every character as
/// a literal.
public static let ignoreMetacharacters = Options(rawValue: 1 << 1)
/// By default, "^" matches the beginning of the string and "$" matches the
/// end of the string, ignoring any newlines. With this option, "^" will
/// the beginning of each line, and "$" will match the end of each line.
public static let anchorsMatchLines = Options(rawValue: 1 << 2)
/// Usually, "." matches all characters except newlines (\n). Using this,
/// options will allow "." to match newLines
public static let dotMatchesLineSeparators = Options(rawValue: 1 << 3)
/// The raw value of the `OptionSet`
public let rawValue: Int
/// Transform an instance of `Regex.Options` into the equivalent `NSRegularExpression.Options`.
///
/// - returns: The equivalent `NSRegularExpression.Options`.
var toNSRegularExpressionOptions: NSRegularExpression.Options {
var options = NSRegularExpression.Options()
if contains(.ignoreCase) { options.insert(.caseInsensitive) }
if contains(.ignoreMetacharacters) { options.insert(.ignoreMetacharacters) }
if contains(.anchorsMatchLines) { options.insert(.anchorsMatchLines) }
if contains(.dotMatchesLineSeparators) { options.insert(.dotMatchesLineSeparators) }
return options
}
// MARK: - Initializers
/// The raw value init for the `OptionSet`
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
}
extension Regex.Options: CustomStringConvertible {
public var description: String {
var description = ""
if contains(.ignoreCase) { description += "i" }
if contains(.ignoreMetacharacters) { description += "x" }
if !contains(.anchorsMatchLines) { description += "a" }
if contains(.dotMatchesLineSeparators) { description += "m" }
return description
}
}
extension Regex.Options: Equatable, Hashable {
public static func == (lhs: Regex.Options, rhs: Regex.Options) -> Bool {
lhs.rawValue == rhs.rawValue
}
public func hash(into hasher: inout Hasher) {
hasher.combine(rawValue)
}
}
// MARK: - Match
extension Regex {
/// A `Match` encapsulates the result of a single match in a string,
/// providing access to the matched string, as well as any capture groups within
/// that string.
public class Match: CustomStringConvertible {
// MARK: Properties
/// The entire matched string.
public lazy var string: String = {
String(describing: self.baseString[self.range])
}()
/// The range of the matched string.
public lazy var range: Range<String.Index> = {
Range(self.result.range, in: self.baseString)!
}()
/// The matching string for each capture group in the regular expression
/// (if any).
///
/// **Note:** Usually if the match was successful, the captures will by
/// definition be non-nil. However if a given capture group is optional, the
/// captured string may also be nil, depending on the particular string that
/// is being matched against.
///
/// Example:
///
/// let regex = Regex("(a)?(b)")
///
/// regex.matches(in: "ab")first?.captures // [Optional("a"), Optional("b")]
/// regex.matches(in: "b").first?.captures // [nil, Optional("b")]
public lazy var captures: [String?] = {
let captureRanges = stride(from: 0, to: result.numberOfRanges, by: 1)
.map(result.range)
.dropFirst()
.map { [unowned self] in
Range($0, in: self.baseString)
}
return captureRanges.map { [unowned self] captureRange in
guard let captureRange = captureRange else { return nil }
return String(describing: self.baseString[captureRange])
}
}()
let result: NSTextCheckingResult
let baseString: String
// MARK: - Initializers
internal init(result: NSTextCheckingResult, in string: String) {
precondition(
result.regularExpression != nil,
"NSTextCheckingResult must originate from regular expression parsing."
)
self.result = result
self.baseString = string
}
// MARK: - Methods
/// Returns a new string where the matched string is replaced according to the `template`.
///
/// The template string may be a literal string, or include template variables:
/// the variable `$0` will be replaced with the entire matched substring, `$1`
/// with the first capture group, etc.
///
/// For example, to include the literal string "$1" in the replacement string,
/// you must escape the "$": `\$1`.
///
/// - parameters:
/// - template: The template string used to replace matches.
///
/// - returns: A string with `template` applied to the matched string.
public func string(applyingTemplate template: String) -> String {
let replacement = result.regularExpression!.replacementString(
for: result,
in: baseString,
offset: 0,
template: template
)
return replacement
}
// MARK: - CustomStringConvertible
/// Returns a string describing the match.
public var description: String {
"Match<\"\(string)\">"
}
}
}