Skip to content

Commit 35739c9

Browse files
committed
Implement NSTextCheckingResult.range(withName:)
- Added the missing range(withName:) and corresponding CF function. The implementation relies on uregex_groupNumberFromName which is available as a draft API from ICU 55. - Added tests related to named capture groups. - Add availability checks in the tests to run with DarwinCompatibilityTests.
1 parent e6b2444 commit 35739c9

File tree

6 files changed

+141
-3
lines changed

6 files changed

+141
-3
lines changed

CoreFoundation/String.subproj/CFRegularExpression.c

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,41 @@ CFIndex _CFRegularExpressionGetNumberOfCaptureGroups(_CFRegularExpressionRef reg
171171
return (CFIndex)uregex_groupCount(regex->regex, &errorCode);
172172
}
173173

174+
CFIndex _CFRegularExpressionGetCaptureGroupNumberWithName(_CFRegularExpressionRef regex, CFStringRef groupName) {
175+
UniChar stackBuffer[STACK_BUFFER_SIZE], *nameBuffer = NULL;
176+
Boolean freeNameBuffer = false;
177+
178+
CFIndex nameLength = CFStringGetLength(groupName);
179+
UErrorCode errorCode = U_ZERO_ERROR;
180+
181+
nameBuffer = (UniChar *)CFStringGetCharactersPtr(groupName);
182+
if (!nameBuffer) {
183+
if (nameLength <= STACK_BUFFER_SIZE) {
184+
nameBuffer = stackBuffer;
185+
CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer);
186+
} else {
187+
nameBuffer = (UniChar *)malloc(sizeof(UniChar) * nameLength);
188+
if (nameBuffer) {
189+
CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer);
190+
freeNameBuffer = true;
191+
} else {
192+
HALT;
193+
}
194+
}
195+
}
196+
197+
CFIndex idx = uregex_groupNumberFromName(regex->regex, nameBuffer, nameLength, &errorCode);
198+
if (U_FAILURE(errorCode) || idx < 0) {
199+
idx = kCFNotFound;
200+
}
201+
202+
if (freeNameBuffer) {
203+
free(nameBuffer);
204+
}
205+
206+
return idx;
207+
}
208+
174209
struct regexCallBackContext {
175210
void *context;
176211
void (*match)(void *context, CFRange *ranges, CFIndex count, _CFRegularExpressionMatchingFlags flags, Boolean *stop);

CoreFoundation/String.subproj/CFRegularExpression.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ _CFRegularExpressionRef _Nullable _CFRegularExpressionCreate(CFAllocatorRef allo
5757
void _CFRegularExpressionDestroy(_CFRegularExpressionRef regex);
5858

5959
CFIndex _CFRegularExpressionGetNumberOfCaptureGroups(_CFRegularExpressionRef regex);
60+
CFIndex _CFRegularExpressionGetCaptureGroupNumberWithName(_CFRegularExpressionRef regex, CFStringRef groupName);
6061
void _CFRegularExpressionEnumerateMatchesInString(_CFRegularExpressionRef regexObj, CFStringRef string, _CFRegularExpressionMatchingOptions options, CFRange range, void *_Nullable context, _CFRegularExpressionMatch match);
6162

6263
CFStringRef _CFRegularExpressionGetPattern(_CFRegularExpressionRef regex);

Foundation/NSRegularExpression.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ open class NSRegularExpression: NSObject, NSCopying, NSCoding {
106106
open var numberOfCaptureGroups: Int {
107107
return _CFRegularExpressionGetNumberOfCaptureGroups(_internal)
108108
}
109-
109+
110+
internal func _captureGroupNumber(withName name: String) -> Int {
111+
return _CFRegularExpressionGetCaptureGroupNumberWithName(_internal, name._cfObject)
112+
}
113+
110114
/* This class method will produce a string by adding backslash escapes as necessary to the given string, to escape any characters that would otherwise be treated as pattern metacharacters.
111115
*/
112116
open class func escapedPattern(for string: String) -> String {

Foundation/NSTextCheckingResult.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ open class NSTextCheckingResult: NSObject, NSCopying, NSCoding {
5454
open var range: NSRange { return range(at: 0) }
5555
/* A result must have at least one range, but may optionally have more (for example, to represent regular expression capture groups). The range at index 0 always matches the range property. Additional ranges, if any, will have indexes from 1 to numberOfRanges-1. */
5656
open func range(at idx: Int) -> NSRange { NSRequiresConcreteImplementation() }
57+
@available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *)
58+
open func range(withName: String) -> NSRange { NSRequiresConcreteImplementation() }
5759
open var regularExpression: NSRegularExpression? { return nil }
5860
open var numberOfRanges: Int { return 1 }
5961
}
@@ -81,6 +83,15 @@ internal class _NSRegularExpressionNSTextCheckingResultResult : NSTextCheckingRe
8183

8284
override var resultType: CheckingType { return .RegularExpression }
8385
override func range(at idx: Int) -> NSRange { return _ranges[idx] }
86+
override func range(withName name: String) -> NSRange {
87+
let idx = _regularExpression._captureGroupNumber(withName: name)
88+
if idx != kCFNotFound, idx < numberOfRanges {
89+
return range(at: idx)
90+
}
91+
92+
return NSRange(location: NSNotFound, length: 0)
93+
}
94+
8495
override var numberOfRanges: Int { return _ranges.count }
8596
override var regularExpression: NSRegularExpression? { return _regularExpression }
8697
}

TestFoundation/TestNSRegularExpression.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ class TestNSRegularExpression : XCTestCase {
1818
("test_NSCoding", test_NSCoding),
1919
("test_defaultOptions", test_defaultOptions),
2020
("test_badPattern", test_badPattern),
21+
("test_unicodeNamedGroup", test_unicodeNamedGroup),
22+
("test_conflictingNamedGroups", test_conflictingNamedGroups),
2123
]
2224
}
23-
25+
2426
func simpleRegularExpressionTestWithPattern(_ patternString: String, target searchString: String, looking: Bool, match: Bool, file: StaticString = #file, line: UInt = #line) {
2527
do {
2628
let str = NSString(string: searchString)
@@ -371,4 +373,27 @@ class TestNSRegularExpression : XCTestCase {
371373
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(}")
372374
}
373375
}
376+
377+
func test_unicodeNamedGroup() {
378+
let patternString = "(?<りんご>a)"
379+
do {
380+
_ = try NSRegularExpression(pattern: patternString, options: [])
381+
XCTFail("Building regular expression for pattern with unicode group name should fail.")
382+
} catch {
383+
let err = String(describing: error)
384+
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<りんご>a)}")
385+
}
386+
}
387+
388+
func test_conflictingNamedGroups() {
389+
let patternString = "(?<name>a)(?<name>b)"
390+
do {
391+
_ = try NSRegularExpression(pattern: patternString, options: [])
392+
XCTFail("Building regular expression for pattern with identically named groups should fail.")
393+
} catch {
394+
let err = String(describing: error)
395+
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<name>a)(?<name>b)}")
396+
}
397+
}
398+
374399
}

TestFoundation/TestNSTextCheckingResult.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ class TestNSTextCheckingResult: XCTestCase {
1111
static var allTests: [(String, (TestNSTextCheckingResult) -> () throws -> Void)] {
1212
return [
1313
("test_textCheckingResult", test_textCheckingResult),
14+
("test_multipleMatches", test_multipleMatches),
15+
("test_rangeWithName", test_rangeWithName),
1416
]
1517
}
1618

1719
func test_textCheckingResult() {
18-
let patternString = "(a|b)x|123|(c|d)y"
20+
let patternString = "(a|b)x|123|(?<aname>c|d)y"
1921
do {
2022
let patternOptions: NSRegularExpression.Options = []
2123
let regex = try NSRegularExpression(pattern: patternString, options: patternOptions)
@@ -28,16 +30,76 @@ class TestNSTextCheckingResult: XCTestCase {
2830
XCTAssertEqual(result.range(at: 0).location, 6)
2931
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
3032
XCTAssertEqual(result.range(at: 2).location, 6)
33+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
34+
XCTAssertEqual(result.range(withName: "aname").location, 6)
35+
}
3136
//Negative offset
3237
result = match.adjustingRanges(offset: -2)
3338
XCTAssertEqual(result.range(at: 0).location, 3)
3439
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
3540
XCTAssertEqual(result.range(at: 2).location, 3)
41+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
42+
XCTAssertEqual(result.range(withName: "aname").location, 3)
43+
}
3644
//ZeroOffset
3745
result = match.adjustingRanges(offset: 0)
3846
XCTAssertEqual(result.range(at: 0).location, 5)
3947
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
4048
XCTAssertEqual(result.range(at: 2).location, 5)
49+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
50+
XCTAssertEqual(result.range(withName: "aname").location, 5)
51+
}
52+
} catch {
53+
XCTFail("Unable to build regular expression for pattern \(patternString)")
54+
}
55+
}
56+
57+
func test_multipleMatches() {
58+
let patternString = "(?<name>hello)[0-9]"
59+
60+
do {
61+
let regex = try NSRegularExpression(pattern: patternString, options: [])
62+
let searchString = "hello1 hello2"
63+
let searchRange = NSRange(location: 0, length: searchString.count)
64+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
65+
XCTAssertEqual(matches.count, 2)
66+
XCTAssertEqual(matches[0].numberOfRanges, 2)
67+
XCTAssertEqual(matches[0].range, NSRange(location: 0, length: 6))
68+
XCTAssertEqual(matches[0].range(at: 0), NSRange(location: 0, length: 6))
69+
XCTAssertEqual(matches[0].range(at: 1), NSRange(location: 0, length: 5))
70+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
71+
XCTAssertEqual(matches[0].range(withName: "name"), NSRange(location: 0, length: 5))
72+
}
73+
XCTAssertEqual(matches[1].numberOfRanges, 2)
74+
XCTAssertEqual(matches[1].range, NSRange(location: 7, length: 6))
75+
XCTAssertEqual(matches[1].range(at: 0), NSRange(location: 7, length: 6))
76+
XCTAssertEqual(matches[1].range(at: 1), NSRange(location: 7, length: 5))
77+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
78+
XCTAssertEqual(matches[1].range(withName: "name"), NSRange(location: 7, length: 5))
79+
}
80+
} catch {
81+
XCTFail("Unable to build regular expression for pattern \(patternString)")
82+
}
83+
}
84+
85+
86+
func test_rangeWithName() {
87+
guard #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) else {
88+
return
89+
}
90+
91+
let patternString = "(?<name1>hel)lo, (?<name2>worl)d"
92+
93+
do {
94+
let regex = try NSRegularExpression(pattern: patternString, options: [])
95+
let searchString = "hello, world"
96+
let searchRange = NSRange(location: 0, length: searchString.count)
97+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
98+
XCTAssertEqual(matches.count, 1)
99+
XCTAssertEqual(matches[0].numberOfRanges, 3)
100+
XCTAssertEqual(matches[0].range(withName: "incorrect").location, NSNotFound)
101+
XCTAssertEqual(matches[0].range(withName: "name1"), NSRange(location: 0, length: 3))
102+
XCTAssertEqual(matches[0].range(withName: "name2"), NSRange(location: 7, length: 4))
41103
} catch {
42104
XCTFail("Unable to build regular expression for pattern \(patternString)")
43105
}

0 commit comments

Comments
 (0)