Skip to content

Commit 5e58226

Browse files
authored
Merge pull request #2534 from kevints/sr-7749
[SR-7749] Poor performance of String.replacingOccurrences(of:with:) i…
2 parents 63899f9 + 4cd59de commit 5e58226

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
@@ -1544,6 +1544,18 @@ extension NSMutableString {
15441544
}
15451545
return 0
15461546
}
1547+
1548+
private static func makeFindResultsRangeIterator(findResults: CFArray, count: Int, backwards: Bool) -> AnyIterator<NSRange> {
1549+
var index = 0
1550+
return AnyIterator<NSRange>() { () -> NSRange? in
1551+
defer { index += 1 }
1552+
if index < count {
1553+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? count - index - 1 : index)
1554+
return NSRange(rangePtr!.load(as: CFRange.self))
1555+
}
1556+
return nil
1557+
}
1558+
}
15471559

15481560
public func replaceOccurrences(of target: String, with replacement: String, options: CompareOptions = [], range searchRange: NSRange) -> Int {
15491561
let backwards = options.contains(.backwards)
@@ -1554,19 +1566,35 @@ extension NSMutableString {
15541566
if options.contains(.regularExpression) {
15551567
return _replaceOccurrencesOfRegularExpressionPattern(target, withTemplate:replacement, options:options, range: searchRange)
15561568
}
1557-
15581569

1559-
if let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) {
1560-
let numOccurrences = CFArrayGetCount(findResults)
1561-
for cnt in 0..<numOccurrences {
1562-
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
1563-
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
1570+
guard let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) else {
1571+
return 0
1572+
}
1573+
let numOccurrences = CFArrayGetCount(findResults)
1574+
1575+
let rangeIterator = NSMutableString.makeFindResultsRangeIterator(findResults: findResults, count: numOccurrences, backwards: backwards)
1576+
1577+
guard type(of: self) == NSMutableString.self else {
1578+
// If we're dealing with non NSMutableString, mutations must go through `replaceCharacters` (documented behavior)
1579+
for range in rangeIterator {
1580+
replaceCharacters(in: range, with: replacement)
15641581
}
1582+
15651583
return numOccurrences
1566-
} else {
1567-
return 0
15681584
}
15691585

1586+
var newStorage = Substring()
1587+
var sourceStringCurrentIndex = _storage.startIndex
1588+
for range in rangeIterator {
1589+
let matchStartIndex = String.Index(utf16Offset: range.location, in: _storage)
1590+
let matchEndIndex = String.Index(utf16Offset: range.location + range.length, in: _storage)
1591+
newStorage += _storage[sourceStringCurrentIndex ..< matchStartIndex]
1592+
newStorage += replacement
1593+
sourceStringCurrentIndex = matchEndIndex
1594+
}
1595+
newStorage += _storage[sourceStringCurrentIndex ..< _storage.endIndex]
1596+
_storage = String(newStorage)
1597+
return numOccurrences
15701598
}
15711599

15721600
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
@@ -1341,6 +1341,64 @@ extension TestNSString {
13411341
XCTAssertEqual(str4.replacingOccurrences(of: "\n\r", with: " "), "Hello\r\rworld.")
13421342
}
13431343

1344+
func test_replacingOccurrencesInSubclass() {
1345+
class TestMutableString: NSMutableString {
1346+
private var wrapped: NSMutableString
1347+
var replaceCharactersCount: Int = 0
1348+
1349+
override var length: Int {
1350+
return wrapped.length
1351+
}
1352+
1353+
override func character(at index: Int) -> unichar {
1354+
return wrapped.character(at: index)
1355+
}
1356+
1357+
override func replaceCharacters(in range: NSRange, with aString: String) {
1358+
defer { replaceCharactersCount += 1 }
1359+
wrapped.replaceCharacters(in: range, with: aString)
1360+
}
1361+
1362+
override func mutableCopy(with zone: NSZone? = nil) -> Any {
1363+
return wrapped.mutableCopy()
1364+
}
1365+
1366+
required init(stringLiteral value: StaticString) {
1367+
wrapped = .init(stringLiteral: value)
1368+
super.init(stringLiteral: value)
1369+
}
1370+
1371+
required init(capacity: Int) {
1372+
fatalError("init(capacity:) has not been implemented")
1373+
}
1374+
1375+
required init(string aString: String) {
1376+
fatalError("init(string:) has not been implemented")
1377+
}
1378+
1379+
required convenience init?(coder aDecoder: NSCoder) {
1380+
fatalError("init(coder:) has not been implemented")
1381+
}
1382+
1383+
required init(characters: UnsafePointer<unichar>, length: Int) {
1384+
fatalError("init(characters:length:) has not been implemented")
1385+
}
1386+
1387+
required convenience init(extendedGraphemeClusterLiteral value: StaticString) {
1388+
fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented")
1389+
}
1390+
1391+
required convenience init(unicodeScalarLiteral value: StaticString) {
1392+
fatalError("init(unicodeScalarLiteral:) has not been implemented")
1393+
}
1394+
}
1395+
1396+
let testString = TestMutableString(stringLiteral: "ababab")
1397+
XCTAssertEqual(testString.replacingOccurrences(of: "ab", with: "xx"), "xxxxxx")
1398+
XCTAssertEqual(testString.replaceCharactersCount, 3)
1399+
}
1400+
1401+
13441402
func test_fileSystemRepresentation() {
13451403
let name = "" as NSString
13461404
let result = name.fileSystemRepresentation

0 commit comments

Comments
 (0)