Skip to content

Commit 3953f66

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.
1 parent 7d8bbf0 commit 3953f66

File tree

6 files changed

+126
-3
lines changed

6 files changed

+126
-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,7 @@ 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+
open func range(withName: String) -> NSRange { NSRequiresConcreteImplementation() }
5758
open var regularExpression: NSRegularExpression? { return nil }
5859
open var numberOfRanges: Int { return 1 }
5960
}
@@ -81,6 +82,16 @@ internal class _NSRegularExpressionNSTextCheckingResultResult : NSTextCheckingRe
8182

8283
override var resultType: CheckingType { return .RegularExpression }
8384
override func range(at idx: Int) -> NSRange { return _ranges[idx] }
85+
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
@@ -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: 48 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,61 @@ 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+
XCTAssertEqual(result.range(withName: "aname").location, 6)
4144
//Negative offset
4245
result = match.adjustingRanges(offset: -2)
4346
XCTAssertEqual(result.range(at: 0).location, 3)
4447
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
4548
XCTAssertEqual(result.range(at: 2).location, 3)
49+
XCTAssertEqual(result.range(withName: "aname").location, 3)
4650
//ZeroOffset
4751
result = match.adjustingRanges(offset: 0)
4852
XCTAssertEqual(result.range(at: 0).location, 5)
4953
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
5054
XCTAssertEqual(result.range(at: 2).location, 5)
55+
XCTAssertEqual(result.range(withName: "aname").location, 5)
56+
} catch {
57+
XCTFail("Unable to build regular expression for pattern \(patternString)")
58+
}
59+
}
60+
61+
func test_multipleMatches() {
62+
let patternString = "(?<name>hello)[0-9]"
63+
64+
do {
65+
let regex = try NSRegularExpression(pattern: patternString, options: [])
66+
let searchString = "hello1 hello2"
67+
let searchRange = NSRange(location: 0, length: searchString.count)
68+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
69+
XCTAssertEqual(matches.count, 2)
70+
XCTAssertEqual(matches[0].numberOfRanges, 2)
71+
XCTAssertEqual(matches[0].range, NSRange(location: 0, length: 6))
72+
XCTAssertEqual(matches[0].range(at: 0), NSRange(location: 0, length: 6))
73+
XCTAssertEqual(matches[0].range(at: 1), NSRange(location: 0, length: 5))
74+
XCTAssertEqual(matches[0].range(withName: "name"), NSRange(location: 0, length: 5))
75+
XCTAssertEqual(matches[1].numberOfRanges, 2)
76+
XCTAssertEqual(matches[1].range, NSRange(location: 7, length: 6))
77+
XCTAssertEqual(matches[1].range(at: 0), NSRange(location: 7, length: 6))
78+
XCTAssertEqual(matches[1].range(at: 1), NSRange(location: 7, length: 5))
79+
XCTAssertEqual(matches[1].range(withName: "name"), NSRange(location: 7, length: 5))
80+
} catch {
81+
XCTFail("Unable to build regular expression for pattern \(patternString)")
82+
}
83+
}
84+
85+
func test_rangeWithName() {
86+
let patternString = "(?<name1>hel)lo, (?<name2>worl)d"
87+
88+
do {
89+
let regex = try NSRegularExpression(pattern: patternString, options: [])
90+
let searchString = "hello, world"
91+
let searchRange = NSRange(location: 0, length: searchString.count)
92+
let matches = regex.matches(in: searchString, options: [], range: searchRange)
93+
XCTAssertEqual(matches.count, 1)
94+
XCTAssertEqual(matches[0].numberOfRanges, 3)
95+
XCTAssertEqual(matches[0].range(withName: "incorrect").location, NSNotFound)
96+
XCTAssertEqual(matches[0].range(withName: "name1"), NSRange(location: 0, length: 3))
97+
XCTAssertEqual(matches[0].range(withName: "name2"), NSRange(location: 7, length: 4))
5198
} catch {
5299
XCTFail("Unable to build regular expression for pattern \(patternString)")
53100
}

0 commit comments

Comments
 (0)