Skip to content

Commit 4cd59de

Browse files
committed
[SR-7749] Poor performance of String.replacingOccurrences(of:with:) in corelibs-foundation
This supercedes #1620.
1 parent 6a1ca96 commit 4cd59de

File tree

2 files changed

+94
-8
lines changed

2 files changed

+94
-8
lines changed

Foundation/NSString.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,18 @@ extension NSMutableString {
14521452
}
14531453
return 0
14541454
}
1455+
1456+
private static func makeFindResultsRangeIterator(findResults: CFArray, count: Int, backwards: Bool) -> AnyIterator<NSRange> {
1457+
var index = 0
1458+
return AnyIterator<NSRange>() { () -> NSRange? in
1459+
defer { index += 1 }
1460+
if index < count {
1461+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? count - index - 1 : index)
1462+
return NSRange(rangePtr!.load(as: CFRange.self))
1463+
}
1464+
return nil
1465+
}
1466+
}
14551467

14561468
public func replaceOccurrences(of target: String, with replacement: String, options: CompareOptions = [], range searchRange: NSRange) -> Int {
14571469
let backwards = options.contains(.backwards)
@@ -1462,19 +1474,35 @@ extension NSMutableString {
14621474
if options.contains(.regularExpression) {
14631475
return _replaceOccurrencesOfRegularExpressionPattern(target, withTemplate:replacement, options:options, range: searchRange)
14641476
}
1465-
14661477

1467-
if let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) {
1468-
let numOccurrences = CFArrayGetCount(findResults)
1469-
for cnt in 0..<numOccurrences {
1470-
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
1471-
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
1478+
guard let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) else {
1479+
return 0
1480+
}
1481+
let numOccurrences = CFArrayGetCount(findResults)
1482+
1483+
let rangeIterator = NSMutableString.makeFindResultsRangeIterator(findResults: findResults, count: numOccurrences, backwards: backwards)
1484+
1485+
guard type(of: self) == NSMutableString.self else {
1486+
// If we're dealing with non NSMutableString, mutations must go through `replaceCharacters` (documented behavior)
1487+
for range in rangeIterator {
1488+
replaceCharacters(in: range, with: replacement)
14721489
}
1490+
14731491
return numOccurrences
1474-
} else {
1475-
return 0
14761492
}
14771493

1494+
var newStorage = Substring()
1495+
var sourceStringCurrentIndex = _storage.startIndex
1496+
for range in rangeIterator {
1497+
let matchStartIndex = String.Index(utf16Offset: range.location, in: _storage)
1498+
let matchEndIndex = String.Index(utf16Offset: range.location + range.length, in: _storage)
1499+
newStorage += _storage[sourceStringCurrentIndex ..< matchStartIndex]
1500+
newStorage += replacement
1501+
sourceStringCurrentIndex = matchEndIndex
1502+
}
1503+
newStorage += _storage[sourceStringCurrentIndex ..< _storage.endIndex]
1504+
_storage = String(newStorage)
1505+
return numOccurrences
14781506
}
14791507

14801508
public func applyTransform(_ transform: String, reverse: Bool, range: NSRange, updatedRange resultingRange: NSRangePointer?) -> Bool {

TestFoundation/TestNSString.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,64 @@ extension TestNSString {
14131413
XCTAssertEqual(str4.replacingOccurrences(of: "\n\r", with: " "), "Hello\r\rworld.")
14141414
}
14151415

1416+
func test_replacingOccurrencesInSubclass() {
1417+
class TestMutableString: NSMutableString {
1418+
private var wrapped: NSMutableString
1419+
var replaceCharactersCount: Int = 0
1420+
1421+
override var length: Int {
1422+
return wrapped.length
1423+
}
1424+
1425+
override func character(at index: Int) -> unichar {
1426+
return wrapped.character(at: index)
1427+
}
1428+
1429+
override func replaceCharacters(in range: NSRange, with aString: String) {
1430+
defer { replaceCharactersCount += 1 }
1431+
wrapped.replaceCharacters(in: range, with: aString)
1432+
}
1433+
1434+
override func mutableCopy(with zone: NSZone? = nil) -> Any {
1435+
return wrapped.mutableCopy()
1436+
}
1437+
1438+
required init(stringLiteral value: StaticString) {
1439+
wrapped = .init(stringLiteral: value)
1440+
super.init(stringLiteral: value)
1441+
}
1442+
1443+
required init(capacity: Int) {
1444+
fatalError("init(capacity:) has not been implemented")
1445+
}
1446+
1447+
required init(string aString: String) {
1448+
fatalError("init(string:) has not been implemented")
1449+
}
1450+
1451+
required convenience init?(coder aDecoder: NSCoder) {
1452+
fatalError("init(coder:) has not been implemented")
1453+
}
1454+
1455+
required init(characters: UnsafePointer<unichar>, length: Int) {
1456+
fatalError("init(characters:length:) has not been implemented")
1457+
}
1458+
1459+
required convenience init(extendedGraphemeClusterLiteral value: StaticString) {
1460+
fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented")
1461+
}
1462+
1463+
required convenience init(unicodeScalarLiteral value: StaticString) {
1464+
fatalError("init(unicodeScalarLiteral:) has not been implemented")
1465+
}
1466+
}
1467+
1468+
let testString = TestMutableString(stringLiteral: "ababab")
1469+
XCTAssertEqual(testString.replacingOccurrences(of: "ab", with: "xx"), "xxxxxx")
1470+
XCTAssertEqual(testString.replaceCharactersCount, 3)
1471+
}
1472+
1473+
14161474
func test_fileSystemRepresentation() {
14171475
let name = "" as NSString
14181476
let result = name.fileSystemRepresentation

0 commit comments

Comments
 (0)