Skip to content

Commit 0fe5c5f

Browse files
authored
Implement unlocalized range(of:) for AttributedString (#249)
* Implement unlocalized range(of:) for AttributedString `String` has a natively-implemented `_range(of:options:)`. This PR implements `AttributedString`'s `range(of:options:)` with that when localization support is not needed. Ideally we should implement this on `BigString` to avoid iterating through `Characters`, which is tracked as a future TODO. This function takes a `Locale` argument, which isn't available in FoundationEssentials. We could move this to FoundationInternalization where `Locale` is defined, but some clients use it with `locale: nil` and do not need localized results. To make the best of this let's add a non-localized version for FoundationEssentials. * Enable AttributedString.range(of:) when locale is nil
1 parent 4a0637f commit 0fe5c5f

File tree

2 files changed

+32
-7
lines changed

2 files changed

+32
-7
lines changed

Sources/FoundationEssentials/AttributedString/AttributedStringProtocol.swift

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,13 +235,35 @@ extension AttributedStringProtocol {
235235
let clamped = Swift.min(Swift.max(result, bounds.lowerBound), bounds.upperBound)
236236
return AttributedString.Index(clamped)
237237
}
238+
239+
internal func _utf8Index(at utf8Offset: Int) -> AttributedString.Index {
240+
let startOffset = self.startIndex._value.utf8Offset
241+
return AttributedString.Index(self.__guts.utf8Index(at: startOffset + utf8Offset))
242+
}
238243
}
239244

240-
#if FOUNDATION_FRAMEWORK
241-
// TODO: Implement AttributedStringProtocol.range(of:) for FoundationPreview
242245
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
243246
extension AttributedStringProtocol {
247+
internal func _range<T: StringProtocol>(of stringToFind: T, options: String.CompareOptions = []) -> Range<AttributedString.Index>? {
248+
249+
// TODO: Implement this on BigString to avoid O(n) iteration
250+
let substring = Substring(characters)
251+
guard let range = try? substring._range(of: Substring(stringToFind), options: options) else {
252+
return nil
253+
}
254+
255+
let startOffset = substring.utf8.distance(from: substring.startIndex, to: range.lowerBound) // O(1)
256+
let endOffset = substring.utf8.distance(from: substring.startIndex, to: range.upperBound) // O(1)
257+
258+
return self._utf8Index(at: startOffset) ..< self._utf8Index(at: endOffset) // O(log(n))
259+
}
260+
244261
public func range<T: StringProtocol>(of stringToFind: T, options: String.CompareOptions = [], locale: Locale? = nil) -> Range<AttributedString.Index>? {
262+
if locale == nil {
263+
return _range(of: stringToFind, options: options)
264+
}
265+
#if FOUNDATION_FRAMEWORK
266+
// TODO: Implement localized AttributedStringProtocol.range(of:) for FoundationPreview
245267
// Since we have secret access to the String property, go ahead and use the full implementation given by Foundation rather than the limited reimplementation we needed for CharacterView.
246268
// FIXME: There is no longer a `String` property. This is going to be terribly slow.
247269
let bstring = self.__guts.string
@@ -258,7 +280,10 @@ extension AttributedStringProtocol {
258280
let end = bstring.utf8.index(bounds.lowerBound, offsetBy: utf8End)
259281

260282
return AttributedString.Index(start) ..< AttributedString.Index(end)
283+
#else
284+
// TODO: Implement localized AttributedStringProtocol.range(of:) for FoundationPreview
285+
return _range(of: stringToFind, options: options)
286+
#endif // FOUNDATION_FRAMEWORK
261287
}
262288
}
263289

264-
#endif // FOUNDATION_FRAMEWORK

Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
import TestSupport
1515
#endif
1616

17+
#if canImport(FoundationEssentials)
18+
@testable import FoundationEssentials
19+
#endif // FOUNDATION_FRAMEWORK
20+
1721
#if FOUNDATION_FRAMEWORK
1822
@testable @_spi(AttributedString) import Foundation
1923
// For testing default attribute scope conversion
@@ -2126,8 +2130,6 @@ E {
21262130
XCTAssertEqual(abc_lit, abc)
21272131
}
21282132

2129-
#if FOUNDATION_FRAMEWORK
2130-
// TODO: Implement AttributedStringProtocol.range(of:) in FoundationPreview
21312133
func testSearch() {
21322134
let testString = AttributedString("abcdefghi")
21332135
XCTAssertNil(testString.range(of: "baba"))
@@ -2218,8 +2220,6 @@ E {
22182220
XCTAssertNil(testString.range(of: "bcd", options: [.anchored]))
22192221
XCTAssertNil(testString.range(of: "abc", options: [.anchored, .backwards]))
22202222
}
2221-
2222-
#endif // FOUNDATION_FRAMEWORK
22232223

22242224
func testIndexConversion() {
22252225
let attrStr = AttributedString("ABCDE")

0 commit comments

Comments
 (0)