Skip to content

Implement NSTextCheckingResult.range(withName:) #1522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CoreFoundation/String.subproj/CFRegularExpression.c
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,41 @@ CFIndex _CFRegularExpressionGetNumberOfCaptureGroups(_CFRegularExpressionRef reg
return (CFIndex)uregex_groupCount(regex->regex, &errorCode);
}

CFIndex _CFRegularExpressionGetCaptureGroupNumberWithName(_CFRegularExpressionRef regex, CFStringRef groupName) {
UniChar stackBuffer[STACK_BUFFER_SIZE], *nameBuffer = NULL;
Boolean freeNameBuffer = false;

CFIndex nameLength = CFStringGetLength(groupName);
UErrorCode errorCode = U_ZERO_ERROR;

nameBuffer = (UniChar *)CFStringGetCharactersPtr(groupName);
if (!nameBuffer) {
if (nameLength <= STACK_BUFFER_SIZE) {
nameBuffer = stackBuffer;
CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer);
} else {
nameBuffer = (UniChar *)malloc(sizeof(UniChar) * nameLength);
if (nameBuffer) {
CFStringGetCharacters(groupName, CFRangeMake(0, nameLength), nameBuffer);
freeNameBuffer = true;
} else {
HALT;
}
}
}

CFIndex idx = uregex_groupNumberFromName(regex->regex, nameBuffer, nameLength, &errorCode);
if (U_FAILURE(errorCode) || idx < 0) {
idx = kCFNotFound;
}

if (freeNameBuffer) {
free(nameBuffer);
}

return idx;
}

struct regexCallBackContext {
void *context;
void (*match)(void *context, CFRange *ranges, CFIndex count, _CFRegularExpressionMatchingFlags flags, Boolean *stop);
Expand Down
1 change: 1 addition & 0 deletions CoreFoundation/String.subproj/CFRegularExpression.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ _CFRegularExpressionRef _Nullable _CFRegularExpressionCreate(CFAllocatorRef allo
void _CFRegularExpressionDestroy(_CFRegularExpressionRef regex);

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

CFStringRef _CFRegularExpressionGetPattern(_CFRegularExpressionRef regex);
Expand Down
6 changes: 5 additions & 1 deletion Foundation/NSRegularExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,11 @@ open class NSRegularExpression: NSObject, NSCopying, NSCoding {
open var numberOfCaptureGroups: Int {
return _CFRegularExpressionGetNumberOfCaptureGroups(_internal)
}


internal func _captureGroupNumber(withName name: String) -> Int {
return _CFRegularExpressionGetCaptureGroupNumberWithName(_internal, name._cfObject)
}

/* 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.
*/
open class func escapedPattern(for string: String) -> String {
Expand Down
11 changes: 11 additions & 0 deletions Foundation/NSTextCheckingResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ open class NSTextCheckingResult: NSObject, NSCopying, NSCoding {
open var range: NSRange { return range(at: 0) }
/* 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. */
open func range(at idx: Int) -> NSRange { NSRequiresConcreteImplementation() }
@available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *)
open func range(withName: String) -> NSRange { NSRequiresConcreteImplementation() }
open var regularExpression: NSRegularExpression? { return nil }
open var numberOfRanges: Int { return 1 }
}
Expand Down Expand Up @@ -81,6 +83,15 @@ internal class _NSRegularExpressionNSTextCheckingResultResult : NSTextCheckingRe

override var resultType: CheckingType { return .RegularExpression }
override func range(at idx: Int) -> NSRange { return _ranges[idx] }
override func range(withName name: String) -> NSRange {
let idx = _regularExpression._captureGroupNumber(withName: name)
if idx != kCFNotFound, idx < numberOfRanges {
return range(at: idx)
}

return NSRange(location: NSNotFound, length: 0)
}

override var numberOfRanges: Int { return _ranges.count }
override var regularExpression: NSRegularExpression? { return _regularExpression }
}
Expand Down
27 changes: 26 additions & 1 deletion TestFoundation/TestNSRegularExpression.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ class TestNSRegularExpression : XCTestCase {
("test_NSCoding", test_NSCoding),
("test_defaultOptions", test_defaultOptions),
("test_badPattern", test_badPattern),
("test_unicodeNamedGroup", test_unicodeNamedGroup),
("test_conflictingNamedGroups", test_conflictingNamedGroups),
]
}

func simpleRegularExpressionTestWithPattern(_ patternString: String, target searchString: String, looking: Bool, match: Bool, file: StaticString = #file, line: UInt = #line) {
do {
let str = NSString(string: searchString)
Expand Down Expand Up @@ -371,4 +373,27 @@ class TestNSRegularExpression : XCTestCase {
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(}")
}
}

func test_unicodeNamedGroup() {
let patternString = "(?<りんご>a)"
do {
_ = try NSRegularExpression(pattern: patternString, options: [])
XCTFail("Building regular expression for pattern with unicode group name should fail.")
} catch {
let err = String(describing: error)
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<りんご>a)}")
}
}

func test_conflictingNamedGroups() {
let patternString = "(?<name>a)(?<name>b)"
do {
_ = try NSRegularExpression(pattern: patternString, options: [])
XCTFail("Building regular expression for pattern with identically named groups should fail.")
} catch {
let err = String(describing: error)
XCTAssertEqual(err, "Error Domain=NSCocoaErrorDomain Code=2048 \"(null)\" UserInfo={NSInvalidValue=(?<name>a)(?<name>b)}")
}
}

}
64 changes: 63 additions & 1 deletion TestFoundation/TestNSTextCheckingResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ class TestNSTextCheckingResult: XCTestCase {
static var allTests: [(String, (TestNSTextCheckingResult) -> () throws -> Void)] {
return [
("test_textCheckingResult", test_textCheckingResult),
("test_multipleMatches", test_multipleMatches),
("test_rangeWithName", test_rangeWithName),
]
}

func test_textCheckingResult() {
let patternString = "(a|b)x|123|(c|d)y"
let patternString = "(a|b)x|123|(?<aname>c|d)y"
do {
let patternOptions: NSRegularExpression.Options = []
let regex = try NSRegularExpression(pattern: patternString, options: patternOptions)
Expand All @@ -28,16 +30,76 @@ class TestNSTextCheckingResult: XCTestCase {
XCTAssertEqual(result.range(at: 0).location, 6)
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
XCTAssertEqual(result.range(at: 2).location, 6)
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
XCTAssertEqual(result.range(withName: "aname").location, 6)
}
//Negative offset
result = match.adjustingRanges(offset: -2)
XCTAssertEqual(result.range(at: 0).location, 3)
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
XCTAssertEqual(result.range(at: 2).location, 3)
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
XCTAssertEqual(result.range(withName: "aname").location, 3)
}
//ZeroOffset
result = match.adjustingRanges(offset: 0)
XCTAssertEqual(result.range(at: 0).location, 5)
XCTAssertEqual(result.range(at: 1).location, NSNotFound)
XCTAssertEqual(result.range(at: 2).location, 5)
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
XCTAssertEqual(result.range(withName: "aname").location, 5)
}
} catch {
XCTFail("Unable to build regular expression for pattern \(patternString)")
}
}

func test_multipleMatches() {
let patternString = "(?<name>hello)[0-9]"

do {
let regex = try NSRegularExpression(pattern: patternString, options: [])
let searchString = "hello1 hello2"
let searchRange = NSRange(location: 0, length: searchString.count)
let matches = regex.matches(in: searchString, options: [], range: searchRange)
XCTAssertEqual(matches.count, 2)
XCTAssertEqual(matches[0].numberOfRanges, 2)
XCTAssertEqual(matches[0].range, NSRange(location: 0, length: 6))
XCTAssertEqual(matches[0].range(at: 0), NSRange(location: 0, length: 6))
XCTAssertEqual(matches[0].range(at: 1), NSRange(location: 0, length: 5))
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
XCTAssertEqual(matches[0].range(withName: "name"), NSRange(location: 0, length: 5))
}
XCTAssertEqual(matches[1].numberOfRanges, 2)
XCTAssertEqual(matches[1].range, NSRange(location: 7, length: 6))
XCTAssertEqual(matches[1].range(at: 0), NSRange(location: 7, length: 6))
XCTAssertEqual(matches[1].range(at: 1), NSRange(location: 7, length: 5))
if #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) {
XCTAssertEqual(matches[1].range(withName: "name"), NSRange(location: 7, length: 5))
}
} catch {
XCTFail("Unable to build regular expression for pattern \(patternString)")
}
}


func test_rangeWithName() {
guard #available(OSX 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) else {
return
}

let patternString = "(?<name1>hel)lo, (?<name2>worl)d"

do {
let regex = try NSRegularExpression(pattern: patternString, options: [])
let searchString = "hello, world"
let searchRange = NSRange(location: 0, length: searchString.count)
let matches = regex.matches(in: searchString, options: [], range: searchRange)
XCTAssertEqual(matches.count, 1)
XCTAssertEqual(matches[0].numberOfRanges, 3)
XCTAssertEqual(matches[0].range(withName: "incorrect").location, NSNotFound)
XCTAssertEqual(matches[0].range(withName: "name1"), NSRange(location: 0, length: 3))
XCTAssertEqual(matches[0].range(withName: "name2"), NSRange(location: 7, length: 4))
} catch {
XCTFail("Unable to build regular expression for pattern \(patternString)")
}
Expand Down