Skip to content

Commit b19c2cf

Browse files
committed
[String] Add generic String.Index and range inits within a String
Adds a generic version of String.Index.init?(_:within:) and Range<String.Index>.init?(_:in:). Tests added
1 parent 4967fc0 commit b19c2cf

File tree

3 files changed

+114
-12
lines changed

3 files changed

+114
-12
lines changed

stdlib/public/Darwin/Foundation/NSRange.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,33 @@ extension Range where Bound == Int {
175175
}
176176

177177
extension Range where Bound == String.Index {
178-
public init?(_ range: NSRange, in string: __shared String) {
178+
private init?<S: StringProtocol>(
179+
_ range: NSRange, _genericIn string: __shared S
180+
) {
181+
// Corresponding stdlib version
182+
guard #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) else {
183+
fatalError()
184+
}
179185
let u = string.utf16
180186
guard range.location != NSNotFound,
181-
let start = u.index(u.startIndex, offsetBy: range.location, limitedBy: u.endIndex),
182-
let end = u.index(u.startIndex, offsetBy: range.location + range.length, limitedBy: u.endIndex),
187+
let start = u.index(
188+
u.startIndex, offsetBy: range.location, limitedBy: u.endIndex),
189+
let end = u.index(
190+
start, offsetBy: range.length, limitedBy: u.endIndex),
183191
let lowerBound = String.Index(start, within: string),
184192
let upperBound = String.Index(end, within: string)
185193
else { return nil }
186-
194+
187195
self = lowerBound..<upperBound
188196
}
197+
198+
public init?(_ range: NSRange, in string: __shared String) {
199+
self.init(range, _genericIn: string)
200+
}
201+
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
202+
public init?<S: StringProtocol>(_ range: NSRange, in string: __shared S) {
203+
self.init(range, _genericIn: string)
204+
}
189205
}
190206

191207
extension NSRange : CustomReflectable {

stdlib/public/core/StringIndexConversions.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
extension String.Index {
14+
private init?<S: StringProtocol>(
15+
_ sourcePosition: String.Index, _genericWithin target: S
16+
) {
17+
guard target._wholeGuts.isOnGraphemeClusterBoundary(sourcePosition) else {
18+
return nil
19+
}
20+
self = sourcePosition
21+
}
22+
1423
/// Creates an index in the given string that corresponds exactly to the
1524
/// specified position.
1625
///
@@ -49,14 +58,53 @@ extension String.Index {
4958
/// `sourcePosition` must be a valid index of at least one of the views
5059
/// of `target`.
5160
/// - target: The string referenced by the resulting index.
52-
public init?(
53-
_ sourcePosition: String.Index,
54-
within target: String
61+
public init?(_ sourcePosition: String.Index, within target: String) {
62+
self.init(sourcePosition, _genericWithin: target)
63+
}
64+
65+
/// Creates an index in the given string that corresponds exactly to the
66+
/// specified position.
67+
///
68+
/// If the index passed as `sourcePosition` represents the start of an
69+
/// extended grapheme cluster---the element type of a string---then the
70+
/// initializer succeeds.
71+
///
72+
/// The following example converts the position of the Unicode scalar `"e"`
73+
/// into its corresponding position in the string. The character at that
74+
/// position is the composed `"é"` character.
75+
///
76+
/// let cafe = "Cafe\u{0301}"
77+
/// print(cafe)
78+
/// // Prints "Café"
79+
///
80+
/// let scalarsIndex = cafe.unicodeScalars.firstIndex(of: "e")!
81+
/// let stringIndex = String.Index(scalarsIndex, within: cafe)!
82+
///
83+
/// print(cafe[...stringIndex])
84+
/// // Prints "Café"
85+
///
86+
/// If the index passed as `sourcePosition` doesn't have an exact
87+
/// corresponding position in `target`, the result of the initializer is
88+
/// `nil`. For example, an attempt to convert the position of the combining
89+
/// acute accent (`"\u{0301}"`) fails. Combining Unicode scalars do not have
90+
/// their own position in a string.
91+
///
92+
/// let nextScalarsIndex = cafe.unicodeScalars.index(after: scalarsIndex)
93+
/// let nextStringIndex = String.Index(nextScalarsIndex, within: cafe)
94+
///
95+
/// print(nextStringIndex)
96+
/// // Prints "nil"
97+
///
98+
/// - Parameters:
99+
/// - sourcePosition: A position in a view of the `target` parameter.
100+
/// `sourcePosition` must be a valid index of at least one of the views
101+
/// of `target`.
102+
/// - target: The string referenced by the resulting index.
103+
@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *)
104+
public init?<S: StringProtocol>(
105+
_ sourcePosition: String.Index, within target: S
55106
) {
56-
guard target._guts.isOnGraphemeClusterBoundary(sourcePosition) else {
57-
return nil
58-
}
59-
self = sourcePosition
107+
self.init(sourcePosition, _genericWithin: target)
60108
}
61109

62110
/// Returns the position in the given UTF-8 view that corresponds exactly to
@@ -81,7 +129,7 @@ extension String.Index {
81129
/// position of a UTF-16 trailing surrogate returns `nil`.
82130
public func samePosition(
83131
in utf8: String.UTF8View
84-
) -> String.UTF8View.Index? {
132+
) -> String.UTF8View.Index? {
85133
return String.UTF8View.Index(self, within: utf8)
86134
}
87135

test/stdlib/StringIndex.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,44 @@ StringIndexTests.test("Scalar Align UTF-8 indices") {
202202
expectEqual(roundedIdx, roundedIdx3)
203203
}
204204

205+
import Foundation
206+
StringIndexTests.test("String.Index(_:within) / Range<String.Index>(_:in:)") {
207+
guard #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) else {
208+
return
209+
}
205210

211+
let str = simpleStrings.joined()
212+
let substr = str[...]
213+
for idx in str.utf8.indices {
214+
expectEqual(
215+
String.Index(idx, within: str), String.Index(idx, within: substr))
216+
}
217+
218+
let utf16Count = str.utf16.count
219+
let utf16Indices = Array(str.utf16.indices) + [str.utf16.endIndex]
220+
for location in 0..<utf16Count {
221+
for length in 0...(utf16Count - location) {
222+
let strLB = String.Index(utf16Indices[location], within: str)
223+
let substrLB = String.Index(utf16Indices[location], within: substr)
224+
let strUB = String.Index(utf16Indices[location+length], within: str)
225+
let substrUB = String.Index(utf16Indices[location+length], within: substr)
226+
expectEqual(strLB, substrLB)
227+
expectEqual(strUB, substrUB)
228+
229+
if #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) {
230+
let nsRange = NSRange(location: location, length: length)
231+
let strRange = Range<String.Index>(nsRange, in: str)
232+
let substrRange = Range<String.Index>(nsRange, in: substr)
233+
234+
expectEqual(strRange, substrRange)
235+
guard strLB != nil && strUB != nil else {
236+
expectNil(strRange)
237+
continue
238+
}
239+
expectEqual(strRange, Range(uncheckedBounds: (strLB!, strUB!)))
240+
}
241+
}
242+
}
243+
}
206244

207245
runAllTests()

0 commit comments

Comments
 (0)