Skip to content

Commit 23a8abf

Browse files
committed
O(n) replacement in NSString.replaceOccurrances
1 parent 8ebf49c commit 23a8abf

File tree

2 files changed

+86
-4
lines changed

2 files changed

+86
-4
lines changed

Foundation/NSString.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,18 +1420,35 @@ extension NSMutableString {
14201420
return _replaceOccurrencesOfRegularExpressionPattern(target, withTemplate:replacement, options:options, range: searchRange)
14211421
}
14221422

1423+
guard let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) else {
1424+
return 0
1425+
}
1426+
let numOccurrences = CFArrayGetCount(findResults)
14231427

1424-
if let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) {
1425-
let numOccurrences = CFArrayGetCount(findResults)
1428+
guard type(of: self) == NSMutableString.self else {
1429+
// If we're dealing with non NSMutableString, mutations must go through `replaceCharacters` (documented behavior)
14261430
for cnt in 0..<numOccurrences {
14271431
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
14281432
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
14291433
}
14301434
return numOccurrences
1431-
} else {
1432-
return 0
14331435
}
14341436

1437+
let start = _storage.utf16.startIndex
1438+
var newStorage = Substring()
1439+
var sourceStringCurrentIndex = _storage.startIndex
1440+
for cnt in 0..<numOccurrences {
1441+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? numOccurrences - cnt - 1 : cnt)
1442+
let range = NSRange(rangePtr!.load(as: CFRange.self))
1443+
let matchStartIndex = _storage.utf16.index(start, offsetBy: range.location).samePosition(in: _storage)!
1444+
let matchEndIndex = _storage.utf16.index(start, offsetBy: range.location + range.length).samePosition(in: _storage)!
1445+
newStorage += _storage[sourceStringCurrentIndex..<matchStartIndex]
1446+
newStorage += replacement
1447+
sourceStringCurrentIndex = matchEndIndex
1448+
}
1449+
newStorage += _storage[sourceStringCurrentIndex..<_storage.endIndex]
1450+
_storage = String(newStorage)
1451+
return numOccurrences
14351452
}
14361453

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

TestFoundation/TestNSString.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class TestNSString: LoopbackServerTest {
8888
("test_emptyStringPrefixAndSuffix",test_emptyStringPrefixAndSuffix),
8989
("test_reflection", { _ in test_reflection }),
9090
("test_replacingOccurrences", test_replacingOccurrences),
91+
("test_replacingOccurrencesInSubclass", test_replacingOccurrencesInSubclass),
9192
("test_getLineStart", test_getLineStart),
9293
("test_substringWithRange", test_substringWithRange),
9394
("test_createCopy", test_createCopy),
@@ -1250,4 +1251,68 @@ extension TestNSString {
12501251
let replaceSuffixWithMultibyte = testString.replacingOccurrences(of: testSuffix, with: testReplacementEmoji)
12511252
XCTAssertEqual(replaceSuffixWithMultibyte, testPrefix + testEmoji + testReplacementEmoji)
12521253
}
1254+
1255+
func test_replacingOccurrencesInSubclass() {
1256+
class TestMutableString: NSMutableString {
1257+
internal var _storage: String
1258+
private var _replaceCharactersCount: Int = 0
1259+
var replaceCharactersCount: Int {
1260+
return _replaceCharactersCount
1261+
}
1262+
1263+
override var length: Int {
1264+
return _storage.utf16.count
1265+
}
1266+
1267+
override func character(at index: Int) -> unichar {
1268+
let start = _storage.utf16.startIndex
1269+
return _storage.utf16[start.advanced(by: index)]
1270+
}
1271+
1272+
override func replaceCharacters(in range: NSRange, with aString: String) {
1273+
_replaceCharactersCount = _replaceCharactersCount + 1
1274+
let start = _storage.utf16.startIndex
1275+
let min = _storage.utf16.index(start, offsetBy: range.location).samePosition(in: _storage)!
1276+
let max = _storage.utf16.index(start, offsetBy: range.location + range.length).samePosition(in: _storage)!
1277+
_storage.replaceSubrange(min..<max, with: aString)
1278+
}
1279+
1280+
override func mutableCopy(with zone: NSZone? = nil) -> Any {
1281+
return self
1282+
}
1283+
1284+
required init(stringLiteral value: StaticString) {
1285+
_storage = String(describing: value)
1286+
super.init(stringLiteral: value)
1287+
}
1288+
1289+
required init(capacity: Int) {
1290+
fatalError("init(capacity:) has not been implemented")
1291+
}
1292+
1293+
required init(string aString: String) {
1294+
fatalError("init(string:) has not been implemented")
1295+
}
1296+
1297+
required convenience init?(coder aDecoder: NSCoder) {
1298+
fatalError("init(coder:) has not been implemented")
1299+
}
1300+
1301+
required init(characters: UnsafePointer<unichar>, length: Int) {
1302+
fatalError("init(characters:length:) has not been implemented")
1303+
}
1304+
1305+
required convenience init(extendedGraphemeClusterLiteral value: StaticString) {
1306+
fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented")
1307+
}
1308+
1309+
required convenience init(unicodeScalarLiteral value: StaticString) {
1310+
fatalError("init(unicodeScalarLiteral:) has not been implemented")
1311+
}
1312+
}
1313+
let testString = TestMutableString(stringLiteral: "ababab")
1314+
XCTAssertEqual(testString.replacingOccurrences(of: "ab", with: "xx"), "xxxxxx")
1315+
XCTAssertEqual(testString.replaceCharactersCount, 3)
1316+
}
1317+
12531318
}

0 commit comments

Comments
 (0)