diff --git a/Benchmarks/Benchmarks/AttributedString/BenchmarkAttributedString.swift b/Benchmarks/Benchmarks/AttributedString/BenchmarkAttributedString.swift index ec72f6ec1..9230c89d4 100644 --- a/Benchmarks/Benchmarks/AttributedString/BenchmarkAttributedString.swift +++ b/Benchmarks/Benchmarks/AttributedString/BenchmarkAttributedString.swift @@ -431,6 +431,10 @@ let benchmarks = { blackHole(manyAttributesString == manyAttributesString2) } + Benchmark("isIdentical") { benchmark in + blackHole(manyAttributesString.isIdentical(to: manyAttributesString)) + } + Benchmark("equalityDifferingCharacters") { benchmark in blackHole(manyAttributesString == manyAttributesString3) } @@ -442,7 +446,11 @@ let benchmarks = { Benchmark("substringEquality") { benchmark in blackHole(manyAttributesSubstring == manyAttributes2Substring) } - + + Benchmark("substringIsIdentical") { benchmark in + blackHole(manyAttributesSubstring.isIdentical(to: manyAttributesSubstring)) + } + Benchmark("hashAttributedString") { benchmark in var hasher = Hasher() manyAttributesString.hash(into: &hasher) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString.swift b/Sources/FoundationEssentials/AttributedString/AttributedString.swift index 374e3aaa6..024b690c2 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString.swift @@ -403,3 +403,22 @@ extension Range where Bound == BigString.Index { Range(uncheckedBounds: (AttributedString.Index(lowerBound, version: version), AttributedString.Index(upperBound, version: version))) } } + +extension AttributedString { + /// Returns a boolean value indicating whether this string is identical to + /// `other`. + /// + /// Two string values are identical if there is no way to distinguish between + /// them. + /// + /// Comparing strings this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// string storage object. Therefore, identical strings are guaranteed to + /// compare equal with `==`, but not all equal strings are considered + /// identical. + /// + /// - Performance: O(1) + public func isIdentical(to other: Self) -> Bool { + self._guts === other._guts + } +} diff --git a/Sources/FoundationEssentials/AttributedString/AttributedSubstring.swift b/Sources/FoundationEssentials/AttributedString/AttributedSubstring.swift index 4191d9d46..df1313d8a 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedSubstring.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedSubstring.swift @@ -205,3 +205,23 @@ extension AttributedSubstring { } } } + +extension AttributedSubstring { + /// Returns a boolean value indicating whether this substring is identical to + /// `other`. + /// + /// Two substring values are identical if there is no way to distinguish + /// between them. + /// + /// Comparing substrings this way includes comparing (normally) hidden + /// implementation details such as the memory location of any underlying + /// substring storage object. Therefore, identical substrings are guaranteed + /// to compare equal with `==`, but not all equal substrings are considered + /// identical. + /// + /// - Performance: O(1) + public func isIdentical(to other: Self) -> Bool { + self._guts === other._guts && + self._range == other._range + } +} diff --git a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift index 36befc4e9..3adf0fad5 100644 --- a/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift +++ b/Tests/FoundationEssentialsTests/AttributedString/AttributedStringTests.swift @@ -33,6 +33,14 @@ import AppKit #endif #endif +func createManyAttributesString() -> AttributedString { + var str = AttributedString("a") + for i in 0..<10000 { + str += AttributedString("a", attributes: AttributeContainer().testInt(i)) + } + return str +} + /// Regression and coverage tests for `AttributedString` and its associated objects @Suite("AttributedString") private struct AttributedStringTests { @@ -360,6 +368,15 @@ private struct AttributedStringTests { #expect(a2.characters.elementsEqual(a3.characters)) } + @Test func attributedStringIdentical() { + let manyAttributesString = createManyAttributesString() + let manyAttributesString2 = createManyAttributesString() + #expect(manyAttributesString.isIdentical(to: manyAttributesString)) + #expect(manyAttributesString2.isIdentical(to: manyAttributesString2)) + #expect(!(manyAttributesString.isIdentical(to: manyAttributesString2))) + #expect(!(manyAttributesString2.isIdentical(to: manyAttributesString))) + } + @Test func attributedSubstringEquality() { let emptyStr = AttributedString("01234567890123456789") @@ -389,7 +406,19 @@ private struct AttributedStringTests { #expect(emptyStr[index0 ..< index5] == AttributedString("01234")) } - + + @Test func attributedSubstringIdentical() { + let manyAttributesString = createManyAttributesString() + let manyAttributesString2 = createManyAttributesString() + let manyAttributesStringRange = manyAttributesString.characters.index(manyAttributesString.startIndex, offsetBy: manyAttributesString.characters.count / 2)... + let manyAttributesSubstring = manyAttributesString[manyAttributesStringRange] + let manyAttributes2Substring = manyAttributesString2[manyAttributesStringRange] + #expect(manyAttributesSubstring.isIdentical(to: manyAttributesSubstring)) + #expect(manyAttributes2Substring.isIdentical(to: manyAttributes2Substring)) + #expect(!(manyAttributesSubstring.isIdentical(to: manyAttributes2Substring))) + #expect(!(manyAttributes2Substring.isIdentical(to: manyAttributesSubstring))) + } + @Test func runEquality() { var attrStr = AttributedString("Hello", attributes: AttributeContainer().testInt(1)) attrStr += AttributedString(" ")