Skip to content

Commit e167c45

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 6c54c84 commit e167c45

File tree

6 files changed

+144
-3
lines changed

6 files changed

+144
-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) API_AVAILABLE(macosx(10.13), ios(11.0), watchos(4.0), tvos(11.0));
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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ open class NSRegularExpression: NSObject, NSCopying, NSCoding {
106106
open var numberOfCaptureGroups: Int {
107107
return _CFRegularExpressionGetNumberOfCaptureGroups(_internal)
108108
}
109-
109+
110+
@available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *)
111+
internal func _captureGroupNumber(withName name: String) -> Int {
112+
return _CFRegularExpressionGetCaptureGroupNumberWithName(_internal, name._cfObject)
113+
}
114+
110115
/* 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.
111116
*/
112117
open class func escapedPattern(for string: String) -> String {

Foundation/NSTextCheckingResult.swift

Lines changed: 13 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,17 @@ internal class _NSRegularExpressionNSTextCheckingResultResult : NSTextCheckingRe
8183

8284
override var resultType: CheckingType { return .RegularExpression }
8385
override func range(at idx: Int) -> NSRange { return _ranges[idx] }
86+
87+
@available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *)
88+
override func range(withName name: String) -> NSRange {
89+
let idx = _regularExpression._captureGroupNumber(withName: name)
90+
if idx != kCFNotFound, idx < numberOfRanges {
91+
return range(at: idx)
92+
}
93+
94+
return NSRange(location: NSNotFound, length: 0)
95+
}
96+
8497
override var numberOfRanges: Int { return _ranges.count }
8598
override var regularExpression: NSRegularExpression? { return _regularExpression }
8699
}

TestFoundation/TestNSRegularExpression.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ class TestNSRegularExpression : XCTestCase {
3030
("test_NSCoding", test_NSCoding),
3131
("test_defaultOptions", test_defaultOptions),
3232
("test_badPattern", test_badPattern),
33+
("test_unicodeNamedGroup", test_unicodeNamedGroup),
34+
("test_conflictingNamedGroups", test_conflictingNamedGroups),
3335
]
3436
}
35-
37+
3638
func simpleRegularExpressionTestWithPattern(_ patternString: String, target searchString: String, looking: Bool, match: Bool, file: StaticString = #file, line: UInt = #line) {
3739
do {
3840
let str = NSString(string: searchString)
@@ -383,4 +385,27 @@ class TestNSRegularExpression : XCTestCase {
383385
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(}")
384386
}
385387
}
388+
389+
func test_unicodeNamedGroup() {
390+
let patternString = "(?<りんご>a)"
391+
do {
392+
_ = try NSRegularExpression(pattern: patternString, options: [])
393+
XCTFail("Building regular expression for pattern with unicode group name should fail.")
394+
} catch {
395+
let err = String(describing: error)
396+
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<りんご>a)}")
397+
}
398+
}
399+
400+
func test_conflictingNamedGroups() {
401+
let patternString = "(?<name>a)(?<name>b)"
402+
do {
403+
_ = try NSRegularExpression(pattern: patternString, options: [])
404+
XCTFail("Building regular expression for pattern with identically named groups should fail.")
405+
} catch {
406+
let err = String(describing: error)
407+
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<name>a)(?<name>b)}")
408+
}
409+
}
410+
386411
}

TestFoundation/TestNSTextCheckingResult.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ class TestNSTextCheckingResult: XCTestCase {
2121
static var allTests: [(String, (TestNSTextCheckingResult) -> () throws -> Void)] {
2222
return [
2323
("test_textCheckingResult", test_textCheckingResult),
24+
("test_multipleMatches", test_multipleMatches),
25+
("test_rangeWithName", test_rangeWithName),
2426
]
2527
}
2628

2729
func test_textCheckingResult() {
28-
let patternString = "(a|b)x|123|(c|d)y"
30+
let patternString = "(a|b)x|123|(?<aname>c|d)y"
2931
do {
3032
let patternOptions: NSRegularExpression.Options = []
3133
let regex = try NSRegularExpression(pattern: patternString, options: patternOptions)
@@ -38,16 +40,76 @@ class TestNSTextCheckingResult: XCTestCase {
3840
XCTAssertEqual(result.range(at: 0).location, 6)
3941
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
4042
XCTAssertEqual(result.range(at: 2).location, 6)
43+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
44+
XCTAssertEqual(result.range(withName: "aname").location, 6)
45+
}
4146
//Negative offset
4247
result = match.adjustingRanges(offset: -2)
4348
XCTAssertEqual(result.range(at: 0).location, 3)
4449
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
4550
XCTAssertEqual(result.range(at: 2).location, 3)
51+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
52+
XCTAssertEqual(result.range(withName: "aname").location, 3)
53+
}
4654
//ZeroOffset
4755
result = match.adjustingRanges(offset: 0)
4856
XCTAssertEqual(result.range(at: 0).location, 5)
4957
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
5058
XCTAssertEqual(result.range(at: 2).location, 5)
59+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
60+
XCTAssertEqual(result.range(withName: "aname").location, 5)
61+
}
62+
} catch {
63+
XCTFail("Unable to build regular expression for pattern \(patternString)")
64+
}
65+
}
66+
67+
func test_multipleMatches() {
68+
let patternString = "(?<name>hello)[0-9]"
69+
70+
do {
71+
let regex = try NSRegularExpression(pattern: patternString, options: [])
72+
let searchString = "hello1 hello2"
73+
let searchRange = NSRange(location: 0, length: searchString.count)
74+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
75+
XCTAssertEqual(matches.count, 2)
76+
XCTAssertEqual(matches[0].numberOfRanges, 2)
77+
XCTAssertEqual(matches[0].range, NSRange(location: 0, length: 6))
78+
XCTAssertEqual(matches[0].range(at: 0), NSRange(location: 0, length: 6))
79+
XCTAssertEqual(matches[0].range(at: 1), NSRange(location: 0, length: 5))
80+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
81+
XCTAssertEqual(matches[0].range(withName: "name"), NSRange(location: 0, length: 5))
82+
}
83+
XCTAssertEqual(matches[1].numberOfRanges, 2)
84+
XCTAssertEqual(matches[1].range, NSRange(location: 7, length: 6))
85+
XCTAssertEqual(matches[1].range(at: 0), NSRange(location: 7, length: 6))
86+
XCTAssertEqual(matches[1].range(at: 1), NSRange(location: 7, length: 5))
87+
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
88+
XCTAssertEqual(matches[1].range(withName: "name"), NSRange(location: 7, length: 5))
89+
}
90+
} catch {
91+
XCTFail("Unable to build regular expression for pattern \(patternString)")
92+
}
93+
}
94+
95+
96+
func test_rangeWithName() {
97+
guard #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) else {
98+
return
99+
}
100+
101+
let patternString = "(?<name1>hel)lo, (?<name2>worl)d"
102+
103+
do {
104+
let regex = try NSRegularExpression(pattern: patternString, options: [])
105+
let searchString = "hello, world"
106+
let searchRange = NSRange(location: 0, length: searchString.count)
107+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
108+
XCTAssertEqual(matches.count, 1)
109+
XCTAssertEqual(matches[0].numberOfRanges, 3)
110+
XCTAssertEqual(matches[0].range(withName: "incorrect").location, NSNotFound)
111+
XCTAssertEqual(matches[0].range(withName: "name1"), NSRange(location: 0, length: 3))
112+
XCTAssertEqual(matches[0].range(withName: "name2"), NSRange(location: 7, length: 4))
51113
} catch {
52114
XCTFail("Unable to build regular expression for pattern \(patternString)")
53115
}

0 commit comments

Comments
 (0)