Skip to content

Commit 920e42f

Browse files
authored
Merge pull request #12151 from moiseev/ns-substring
2 parents cf80110 + 7322b63 commit 920e42f

File tree

3 files changed

+187
-28
lines changed

3 files changed

+187
-28
lines changed

stdlib/public/SDK/Foundation/NSStringAPI.swift

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ func _toNSArray<T, U : AnyObject>(_ a: [T], f: (T) -> U) -> NSArray {
4242
return result
4343
}
4444

45-
func _toNSRange(_ r: Range<String.Index>) -> NSRange {
46-
return NSRange(
47-
location: r.lowerBound.encodedOffset,
48-
length: r.upperBound.encodedOffset - r.lowerBound.encodedOffset)
49-
}
50-
5145
#if !DEPLOYMENT_RUNTIME_SWIFT
5246
// We only need this for UnsafeMutablePointer, but there's not currently a way
5347
// to write that constraint.
@@ -439,10 +433,26 @@ extension StringProtocol where Index == String.Index {
439433
return self._ephemeralString._bridgeToObjectiveC()
440434
}
441435

436+
// self can be a Substring so we need to subtract/add this offset when
437+
// passing _ns to the Foundation APIs. Will be 0 if self is String.
438+
@_inlineable
439+
@_versioned
440+
internal var _substringOffset: Int {
441+
return self.startIndex.encodedOffset
442+
}
443+
442444
/// Return an `Index` corresponding to the given offset in our UTF-16
443445
/// representation.
444446
func _index(_ utf16Index: Int) -> Index {
445-
return Index(encodedOffset: utf16Index)
447+
return Index(encodedOffset: utf16Index + _substringOffset)
448+
}
449+
450+
@_inlineable
451+
@_versioned
452+
internal func _toRelativeNSRange(_ r: Range<String.Index>) -> NSRange {
453+
return NSRange(
454+
location: r.lowerBound.encodedOffset - _substringOffset,
455+
length: r.upperBound.encodedOffset - r.lowerBound.encodedOffset)
446456
}
447457

448458
/// Return a `Range<Index>` corresponding to the given `NSRange` of
@@ -603,7 +613,7 @@ extension StringProtocol where Index == String.Index {
603613
return locale != nil ? _ns.compare(
604614
aString,
605615
options: mask,
606-
range: _toNSRange(
616+
range: _toRelativeNSRange(
607617
range ?? startIndex..<endIndex
608618
),
609619
locale: locale?._bridgeToObjectiveC()
@@ -612,7 +622,7 @@ extension StringProtocol where Index == String.Index {
612622
: range != nil ? _ns.compare(
613623
aString,
614624
options: mask,
615-
range: _toNSRange(range!)
625+
range: _toRelativeNSRange(range!)
616626
)
617627

618628
: !mask.isEmpty ? _ns.compare(aString, options: mask)
@@ -1050,7 +1060,7 @@ extension StringProtocol where Index == String.Index {
10501060
T : StringProtocol, R : RangeExpression
10511061
>(in range: R, with replacement: T) -> String where R.Bound == Index {
10521062
return _ns.replacingCharacters(
1053-
in: _toNSRange(range.relative(to: self)),
1063+
in: _toRelativeNSRange(range.relative(to: self)),
10541064
with: replacement._ephemeralString)
10551065
}
10561066

@@ -1083,7 +1093,7 @@ extension StringProtocol where Index == String.Index {
10831093
of: target,
10841094
with: replacement,
10851095
options: options,
1086-
range: _toNSRange(
1096+
range: _toRelativeNSRange(
10871097
searchRange ?? startIndex..<endIndex
10881098
)
10891099
)
@@ -1208,7 +1218,7 @@ extension StringProtocol where Index == String.Index {
12081218
) where R.Bound == Index {
12091219
let range = range.relative(to: self)
12101220
_ns.enumerateLinguisticTags(
1211-
in: _toNSRange(range),
1221+
in: _toRelativeNSRange(range),
12121222
scheme: tagScheme._ephemeralString,
12131223
options: opts,
12141224
orthography: orthography != nil ? orthography! : nil
@@ -1273,7 +1283,7 @@ extension StringProtocol where Index == String.Index {
12731283
) -> Void
12741284
) where R.Bound == Index {
12751285
_ns.enumerateSubstrings(
1276-
in: _toNSRange(range.relative(to: self)), options: opts) {
1286+
in: _toRelativeNSRange(range.relative(to: self)), options: opts) {
12771287
var stop_ = false
12781288

12791289
body($0,
@@ -1346,7 +1356,7 @@ extension StringProtocol where Index == String.Index {
13461356
usedLength: usedBufferCount,
13471357
encoding: encoding.rawValue,
13481358
options: options,
1349-
range: _toNSRange(range.relative(to: self)),
1359+
range: _toRelativeNSRange(range.relative(to: self)),
13501360
remaining: $0)
13511361
}
13521362
}
@@ -1373,7 +1383,7 @@ extension StringProtocol where Index == String.Index {
13731383
contentsEnd in self._ns.getLineStart(
13741384
start, end: end,
13751385
contentsEnd: contentsEnd,
1376-
for: _toNSRange(range.relative(to: self)))
1386+
for: _toRelativeNSRange(range.relative(to: self)))
13771387
}
13781388
}
13791389
}
@@ -1401,7 +1411,7 @@ extension StringProtocol where Index == String.Index {
14011411
contentsEnd in self._ns.getParagraphStart(
14021412
start, end: end,
14031413
contentsEnd: contentsEnd,
1404-
for: _toNSRange(range.relative(to: self)))
1414+
for: _toRelativeNSRange(range.relative(to: self)))
14051415
}
14061416
}
14071417
}
@@ -1428,7 +1438,8 @@ extension StringProtocol where Index == String.Index {
14281438
public func lineRange<
14291439
R : RangeExpression
14301440
>(for aRange: R) -> Range<Index> where R.Bound == Index {
1431-
return _range(_ns.lineRange(for: _toNSRange(aRange.relative(to: self))))
1441+
return _range(_ns.lineRange(
1442+
for: _toRelativeNSRange(aRange.relative(to: self))))
14321443
}
14331444

14341445
#if !DEPLOYMENT_RUNTIME_SWIFT
@@ -1453,7 +1464,7 @@ extension StringProtocol where Index == String.Index {
14531464
var nsTokenRanges: NSArray?
14541465
let result = tokenRanges._withNilOrAddress(of: &nsTokenRanges) {
14551466
self._ns.linguisticTags(
1456-
in: _toNSRange(range.relative(to: self)),
1467+
in: _toRelativeNSRange(range.relative(to: self)),
14571468
scheme: tagScheme._ephemeralString,
14581469
options: opts,
14591470
orthography: orthography,
@@ -1477,7 +1488,7 @@ extension StringProtocol where Index == String.Index {
14771488
R : RangeExpression
14781489
>(for aRange: R) -> Range<Index> where R.Bound == Index {
14791490
return _range(
1480-
_ns.paragraphRange(for: _toNSRange(aRange.relative(to: self))))
1491+
_ns.paragraphRange(for: _toRelativeNSRange(aRange.relative(to: self))))
14811492
}
14821493
#endif
14831494

@@ -1504,7 +1515,7 @@ extension StringProtocol where Index == String.Index {
15041515
_ns.rangeOfCharacter(
15051516
from: aSet,
15061517
options: mask,
1507-
range: _toNSRange(
1518+
range: _toRelativeNSRange(
15081519
aRange ?? startIndex..<endIndex
15091520
)
15101521
)
@@ -1535,7 +1546,7 @@ extension StringProtocol where Index == String.Index {
15351546
// and output ranges due (if nothing else) to locale changes
15361547
return _range(
15371548
_ns.rangeOfComposedCharacterSequences(
1538-
for: _toNSRange(range.relative(to: self))))
1549+
for: _toRelativeNSRange(range.relative(to: self))))
15391550
}
15401551

15411552
// - (NSRange)rangeOfString:(NSString *)aString
@@ -1570,13 +1581,13 @@ extension StringProtocol where Index == String.Index {
15701581
locale != nil ? _ns.range(
15711582
of: aString,
15721583
options: mask,
1573-
range: _toNSRange(
1584+
range: _toRelativeNSRange(
15741585
searchRange ?? startIndex..<endIndex
15751586
),
15761587
locale: locale
15771588
)
15781589
: searchRange != nil ? _ns.range(
1579-
of: aString, options: mask, range: _toNSRange(searchRange!)
1590+
of: aString, options: mask, range: _toRelativeNSRange(searchRange!)
15801591
)
15811592
: !mask.isEmpty ? _ns.range(of: aString, options: mask)
15821593
: _ns.range(of: aString)
@@ -1687,7 +1698,7 @@ extension StringProtocol where Index == String.Index {
16871698
@available(swift, deprecated: 4.0,
16881699
message: "Please use String slicing subscript.")
16891700
public func substring(with aRange: Range<Index>) -> String {
1690-
return _ns.substring(with: _toNSRange(aRange))
1701+
return _ns.substring(with: _toRelativeNSRange(aRange))
16911702
}
16921703
}
16931704

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// RUN: rm -rf %t ; mkdir -p %t
2+
// RUN: %target-build-swift %s -o %t/a.out4 -swift-version 4 && %target-run %t/a.out4
3+
// REQUIRES: executable_test
4+
5+
// REQUIRES: objc_interop
6+
7+
//
8+
// Tests for the NSString APIs on Substring
9+
//
10+
11+
import StdlibUnittest
12+
13+
import Foundation
14+
15+
16+
extension String {
17+
func range(fromStart: Int, fromEnd: Int) -> Range<String.Index> {
18+
return index(startIndex, offsetBy: fromStart) ..<
19+
index(endIndex, offsetBy: fromEnd)
20+
}
21+
subscript(fromStart: Int, fromEnd: Int) -> SubSequence {
22+
return self[range(fromStart: fromStart, fromEnd: fromEnd)]
23+
}
24+
}
25+
26+
var tests = TestSuite("NSStringAPIs/Substring")
27+
28+
tests.test("range(of:)/NilRange") {
29+
let ss = "aabcdd"[1, -1]
30+
let range = ss.range(of: "bc")
31+
expectOptionalEqual("bc", range.map { ss[$0] })
32+
}
33+
34+
tests.test("range(of:)/NonNilRange") {
35+
let s = "aabcdd"
36+
let ss = s[1, -1]
37+
let searchRange = s.range(fromStart: 2, fromEnd: -2)
38+
let range = ss.range(of: "bc", range: searchRange)
39+
expectOptionalEqual("bc", range.map { ss[$0] })
40+
}
41+
42+
tests.test("rangeOfCharacter") {
43+
let ss = "__hello__"[2, -2]
44+
let range = ss.rangeOfCharacter(from: CharacterSet.alphanumerics)
45+
expectOptionalEqual("h", range.map { ss[$0] })
46+
}
47+
48+
tests.test("compare(_:options:range:locale:)/NilRange") {
49+
let needle = "hello"
50+
let haystack = "__hello__"[2, -2]
51+
expectEqual(.orderedSame, haystack.compare(needle))
52+
}
53+
54+
tests.test("compare(_:options:range:locale:)/NonNilRange") {
55+
let needle = "hello"
56+
let haystack = "__hello__"
57+
let range = haystack.range(fromStart: 2, fromEnd: -2)
58+
expectEqual(.orderedSame, haystack[range].compare(needle, range: range))
59+
}
60+
61+
tests.test("replacingCharacters(in:with:)") {
62+
let s = "__hello, world"
63+
let range = s.range(fromStart: 2, fromEnd: -7)
64+
let expected = "__goodbye, world"
65+
let replacement = "goodbye"
66+
expectEqual(expected,
67+
s.replacingCharacters(in: range, with: replacement))
68+
expectEqual(expected[2, 0],
69+
s[2, 0].replacingCharacters(in: range, with: replacement))
70+
71+
expectEqual(replacement,
72+
s.replacingCharacters(in: s.startIndex..., with: replacement))
73+
expectEqual(replacement,
74+
s.replacingCharacters(in: ..<s.endIndex, with: replacement))
75+
expectEqual(expected[2, 0],
76+
s[2, 0].replacingCharacters(in: range, with: replacement[...]))
77+
}
78+
79+
tests.test("replacingOccurrences(of:with:options:range:)/NilRange") {
80+
let s = "hello"
81+
82+
expectEqual("he11o", s.replacingOccurrences(of: "l", with: "1"))
83+
expectEqual("he11o", s.replacingOccurrences(of: "l"[...], with: "1"))
84+
expectEqual("he11o", s.replacingOccurrences(of: "l", with: "1"[...]))
85+
expectEqual("he11o", s.replacingOccurrences(of: "l"[...], with: "1"[...]))
86+
87+
expectEqual("he11o",
88+
s[...].replacingOccurrences(of: "l", with: "1"))
89+
expectEqual("he11o",
90+
s[...].replacingOccurrences(of: "l"[...], with: "1"))
91+
expectEqual("he11o",
92+
s[...].replacingOccurrences(of: "l", with: "1"[...]))
93+
expectEqual("he11o",
94+
s[...].replacingOccurrences(of: "l"[...], with: "1"[...]))
95+
}
96+
97+
tests.test("replacingOccurrences(of:with:options:range:)/NonNilRange") {
98+
let s = "hello"
99+
let r = s.range(fromStart: 1, fromEnd: -2)
100+
101+
expectEqual("he1lo",
102+
s.replacingOccurrences(of: "l", with: "1", range: r))
103+
expectEqual("he1lo",
104+
s.replacingOccurrences(of: "l"[...], with: "1", range: r))
105+
expectEqual("he1lo",
106+
s.replacingOccurrences(of: "l", with: "1"[...], range: r))
107+
expectEqual("he1lo",
108+
s.replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
109+
110+
expectEqual("he1lo",
111+
s[...].replacingOccurrences(of: "l", with: "1", range: r))
112+
expectEqual("he1lo",
113+
s[...].replacingOccurrences(of: "l"[...], with: "1", range: r))
114+
expectEqual("he1lo",
115+
s[...].replacingOccurrences(of: "l", with: "1"[...], range: r))
116+
expectEqual("he1lo",
117+
s[...].replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
118+
119+
let ss = s[1, -1]
120+
expectEqual("e1l",
121+
ss.replacingOccurrences(of: "l", with: "1", range: r))
122+
expectEqual("e1l",
123+
ss.replacingOccurrences(of: "l"[...], with: "1", range: r))
124+
expectEqual("e1l",
125+
ss.replacingOccurrences(of: "l", with: "1"[...], range: r))
126+
expectEqual("e1l",
127+
ss.replacingOccurrences(of: "l"[...], with: "1"[...], range: r))
128+
}
129+
130+
tests.test("substring(with:)") {
131+
let s = "hello, world"
132+
let r = s.range(fromStart: 7, fromEnd: 0)
133+
expectEqual("world", s.substring(with: r))
134+
expectEqual("world", s[...].substring(with: r))
135+
expectEqual("world", s[1, 0].substring(with: r))
136+
}
137+
138+
tests.test("substring(with:)/SubscriptEquivalence") {
139+
let s = "hello, world"
140+
let r = s.range(fromStart: 7, fromEnd: 0)
141+
expectEqual(s[r], s.substring(with: r))
142+
expectEqual(s[...][r], s[...].substring(with: r))
143+
expectEqual(s[1, 0][r], s[1, 0].substring(with: r))
144+
}
145+
146+
runAllTests()

test/stdlib/NSStringAPI.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// RUN: %target-run-simple-swift -swift-version 3
1+
// RUN: %target-run-simple-swift
22
// REQUIRES: executable_test
33

44
// REQUIRES: objc_interop
@@ -1144,9 +1144,11 @@ NSStringAPIs.test("rangeOfComposedCharacterSequences(for:)") {
11441144
for: s.index(s.startIndex, offsetBy: 8)..<s.index(s.startIndex, offsetBy: 10))])
11451145
}
11461146

1147-
func toIntRange(
1148-
_ string: String, _ maybeRange: Range<String.Index>?
1149-
) -> Range<Int>? {
1147+
func toIntRange<
1148+
S : StringProtocol
1149+
>(
1150+
_ string: S, _ maybeRange: Range<String.Index>?
1151+
) -> Range<Int>? where S.Index == String.Index, S.IndexDistance == Int {
11501152
guard let range = maybeRange else { return nil }
11511153

11521154
return

0 commit comments

Comments
 (0)