From 4f3525eb5e133d5b2e67e465f872da3d7a412d6a Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Fri, 23 Jul 2021 16:24:44 +0200 Subject: [PATCH 1/7] Add commonPrefix(with:), commonSuffix(with:) --- Sources/Algorithms/CommonPrefix.swift | 350 ++++++++++++++++++ .../CommonPrefixTests.swift | 63 ++++ 2 files changed, 413 insertions(+) create mode 100644 Sources/Algorithms/CommonPrefix.swift create mode 100644 Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift new file mode 100644 index 00000000..02d42991 --- /dev/null +++ b/Sources/Algorithms/CommonPrefix.swift @@ -0,0 +1,350 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +public struct CommonPrefix { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let other: Other + + @usableFromInline + internal let areEquivalent: (Base.Element, Other.Element) -> Bool + + @inlinable + internal init( + base: Base, + other: Other, + areEquivalent: @escaping (Base.Element, Other.Element) -> Bool + ) { + self.base = base + self.other = other + self.areEquivalent = areEquivalent + } +} + +extension CommonPrefix: Sequence { + public struct Iterator: IteratorProtocol { + @usableFromInline + internal var base: Base.Iterator + + @usableFromInline + internal var other: Other.Iterator + + @usableFromInline + internal let areEquivalent: (Base.Element, Other.Element) -> Bool + + @inlinable + internal init( + base: Base.Iterator, + other: Other.Iterator, + areEquivalent: @escaping (Base.Element, Other.Element) -> Bool + ) { + self.base = base + self.other = other + self.areEquivalent = areEquivalent + } + + public mutating func next() -> Base.Element? { + if let next = base.next(), + let otherNext = other.next(), + areEquivalent(next, otherNext) { + return next + } else { + return nil + } + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator( + base: base.makeIterator(), + other: other.makeIterator(), + areEquivalent: areEquivalent) + } +} + +extension CommonPrefix: Collection where Base: Collection, Other: Collection { + public struct Index { + @usableFromInline + internal let base: Base.Index + + @usableFromInline + internal let other: Other.Index + + @inlinable + internal init(base: Base.Index, other: Other.Index) { + self.base = base + self.other = other + } + } + + @inlinable + internal func normalizeIndex(base: Base.Index, other: Other.Index) -> Index { + if base != self.base.endIndex + && other != self.other.endIndex + && areEquivalent(self.base[base], self.other[other]) + { + return Index(base: base, other: other) + } else { + return endIndex + } + } + + @inlinable + public var startIndex: Index { + normalizeIndex(base: base.startIndex, other: other.startIndex) + } + + @inlinable + public var endIndex: Index { + Index(base: base.endIndex, other: other.endIndex) + } + + @inlinable + public func index(after index: Index) -> Index { + normalizeIndex( + base: base.index(after: index.base), + other: other.index(after: index.other)) + } + + @inlinable + public subscript(index: Index) -> Base.Element { + base[index.base] + } +} + +extension CommonPrefix.Index: Comparable { + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.base == rhs.base + } + + @inlinable + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.base < rhs.base + } +} + +extension CommonPrefix.Index: Hashable where Base.Index: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(base) + } +} + +extension CommonPrefix: LazySequenceProtocol where Base: LazySequenceProtocol {} +extension CommonPrefix: LazyCollectionProtocol + where Base: LazyCollectionProtocol, Other: Collection {} + +//===----------------------------------------------------------------------===// +// Sequence.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension Sequence { + @inlinable + public func commonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> [Element] { + var iterator = makeIterator() + var otherIterator = other.makeIterator() + var result: [Element] = [] + + while let next = iterator.next(), + let otherNext = otherIterator.next(), + try areEquivalent(next, otherNext) + { + result.append(next) + } + + return result + } +} + +extension Sequence where Element: Equatable { + @inlinable + public func commonPrefix( + with other: Other + ) -> CommonPrefix where Other.Element == Element { + CommonPrefix(base: self, other: other, areEquivalent: ==) + } +} + +//===----------------------------------------------------------------------===// +// LazySequenceProtocol.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension LazySequenceProtocol { + @inlinable + public func commonPrefix( + with other: Other, + by areEquivalent: @escaping (Element, Other.Element) -> Bool + ) -> CommonPrefix { + CommonPrefix(base: self, other: other, areEquivalent: areEquivalent) + } +} + +//===----------------------------------------------------------------------===// +// Collection.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension Collection { + @inlinable + public func commonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> SubSequence { + let endIndex = endIndex + + var index = startIndex + var iterator = other.makeIterator() + + while index != endIndex, + let next = iterator.next(), + try areEquivalent(self[index], next) + { + formIndex(after: &index) + } + + return self[..( + with other: Other + ) -> SubSequence where Other.Element == Element { + commonPrefix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// LazyCollectionProtocol.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension LazyCollectionProtocol { + @inlinable + public func commonPrefix( + with other: Other, + by areEquivalent: @escaping (Element, Other.Element) -> Bool + ) -> CommonPrefix { + CommonPrefix(base: self, other: other, areEquivalent: areEquivalent) + } +} + +extension LazyCollectionProtocol where Element: Equatable { + @inlinable + public func commonPrefix( + with other: Other + ) -> CommonPrefix where Other.Element == Element { + commonPrefix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// BidirectionalCollection.commonSuffix(with:) +//===----------------------------------------------------------------------===// + +extension BidirectionalCollection { + @inlinable + public func commonSuffix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> SubSequence { + let (index, _) = try startsOfCommonSuffix(with: other, by: areEquivalent) + return self[index...] + } +} + +extension BidirectionalCollection where Element: Equatable { + @inlinable + public func commonSuffix( + with other: Other + ) -> SubSequence where Other.Element == Element { + commonSuffix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// Collection.endsOfCommonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension Collection { + @inlinable + public func endsOfCommonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> (Index, Other.Index) { + var index = startIndex + var otherIndex = other.startIndex + + while index != endIndex && otherIndex != other.endIndex, + try areEquivalent(self[index], other[otherIndex]) + { + formIndex(after: &index) + other.formIndex(after: &otherIndex) + } + + return (index, otherIndex) + } +} + +extension Collection where Element: Equatable { + @inlinable + public func endsOfCommonPrefix( + with other: Other + ) -> (Index, Other.Index) where Other.Element == Element { + endsOfCommonPrefix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// BidirectionalCollection.startsOfCommonPrefix(with:) +//===----------------------------------------------------------------------===// + +extension BidirectionalCollection { + @inlinable + public func startsOfCommonSuffix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> (Index, Other.Index) { + let startIndex = startIndex + let otherStartIndex = other.startIndex + + var index = endIndex + var otherIndex = other.endIndex + + while index != startIndex && otherIndex != otherStartIndex { + let prev = self.index(before: index) + let otherPrev = other.index(before: otherIndex) + + if try !areEquivalent(self[prev], other[otherPrev]) { + break + } + + index = prev + otherIndex = otherPrev + } + + return (index, otherIndex) + } +} + +extension BidirectionalCollection where Element: Equatable { + @inlinable + public func startsOfCommonSuffix( + with other: Other + ) -> (Index, Other.Index) where Other.Element == Element { + startsOfCommonSuffix(with: other, by: ==) + } +} diff --git a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift new file mode 100644 index 00000000..4d97ef06 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift @@ -0,0 +1,63 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import Algorithms + +final class CommonPrefixTests: XCTestCase { + func testCommonPrefix() { + func testCommonPrefix(of a: String, and b: String, equals c: String) { + // eager sequence + XCTAssertEqualSequences(AnySequence(a).commonPrefix(with: AnySequence(b), by: ==), c) + + // lazy sequence + XCTAssertEqualSequences(AnySequence(a).lazy.commonPrefix(with: AnySequence(b), by: ==), c) + + // eager collection + XCTAssertEqualSequences(a.commonPrefix(with: b, by: ==), c) + + // lazy collection + XCTAssertEqualSequences(a.lazy.commonPrefix(with: b, by: ==), c) + } + + testCommonPrefix(of: "abcdef", and: "abcxyz", equals: "abc") + testCommonPrefix(of: "abc", and: "abcxyz", equals: "abc") + testCommonPrefix(of: "abcdef", and: "abc", equals: "abc") + testCommonPrefix(of: "abc", and: "abc", equals: "abc") + + testCommonPrefix(of: "abc", and: "xyz", equals: "") + testCommonPrefix(of: "", and: "xyz", equals: "") + testCommonPrefix(of: "abc", and: "", equals: "") + testCommonPrefix(of: "", and: "", equals: "") + + XCTAssertLazySequence( + AnySequence([1, 2, 3]).lazy.commonPrefix(with: AnySequence([4, 5, 6]))) + XCTAssertLazyCollection([1, 2, 3].lazy.commonPrefix(with: [4, 5, 6])) + } + + func testCommonSuffix() { + func testCommonSuffix(of a: String, and b: String, equals c: String) { + XCTAssertEqualSequences(a.commonSuffix(with: b, by: ==), c) + } + + testCommonSuffix(of: "abcxyz", and: "uvwxyz", equals: "xyz") + testCommonSuffix(of: "xyz", and: "uvwxyz", equals: "xyz") + testCommonSuffix(of: "abcxyz", and: "xyz", equals: "xyz") + testCommonSuffix(of: "xyz", and: "xyz", equals: "xyz") + + testCommonSuffix(of: "abc", and: "xyz", equals: "") + testCommonSuffix(of: "", and: "xyz", equals: "") + testCommonSuffix(of: "abc", and: "", equals: "") + testCommonSuffix(of: "", and: "", equals: "") + + XCTAssertLazySequence([1, 2, 3].lazy.commonSuffix(with: [4, 5, 6])) + } +} From 4c72bd9e51485e17ddbd50593b3bd08e34e486ee Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Fri, 23 Jul 2021 17:45:40 +0200 Subject: [PATCH 2/7] Rename `endsOfCommonPrefix` -> `endOfCommonPrefix`, `startsOfCommonSuffix` -> `startOfCommonSuffix` --- Sources/Algorithms/CommonPrefix.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift index 02d42991..e0b177ce 100644 --- a/Sources/Algorithms/CommonPrefix.swift +++ b/Sources/Algorithms/CommonPrefix.swift @@ -261,7 +261,7 @@ extension BidirectionalCollection { with other: Other, by areEquivalent: (Element, Other.Element) throws -> Bool ) rethrows -> SubSequence { - let (index, _) = try startsOfCommonSuffix(with: other, by: areEquivalent) + let (index, _) = try startOfCommonSuffix(with: other, by: areEquivalent) return self[index...] } } @@ -276,12 +276,12 @@ extension BidirectionalCollection where Element: Equatable { } //===----------------------------------------------------------------------===// -// Collection.endsOfCommonPrefix(with:) +// Collection.endOfCommonPrefix(with:) //===----------------------------------------------------------------------===// extension Collection { @inlinable - public func endsOfCommonPrefix( + public func endOfCommonPrefix( with other: Other, by areEquivalent: (Element, Other.Element) throws -> Bool ) rethrows -> (Index, Other.Index) { @@ -301,20 +301,20 @@ extension Collection { extension Collection where Element: Equatable { @inlinable - public func endsOfCommonPrefix( + public func endOfCommonPrefix( with other: Other ) -> (Index, Other.Index) where Other.Element == Element { - endsOfCommonPrefix(with: other, by: ==) + endOfCommonPrefix(with: other, by: ==) } } //===----------------------------------------------------------------------===// -// BidirectionalCollection.startsOfCommonPrefix(with:) +// BidirectionalCollection.startOfCommonPrefix(with:) //===----------------------------------------------------------------------===// extension BidirectionalCollection { @inlinable - public func startsOfCommonSuffix( + public func startOfCommonSuffix( with other: Other, by areEquivalent: (Element, Other.Element) throws -> Bool ) rethrows -> (Index, Other.Index) { @@ -342,9 +342,9 @@ extension BidirectionalCollection { extension BidirectionalCollection where Element: Equatable { @inlinable - public func startsOfCommonSuffix( + public func startOfCommonSuffix( with other: Other ) -> (Index, Other.Index) where Other.Element == Element { - startsOfCommonSuffix(with: other, by: ==) + startOfCommonSuffix(with: other, by: ==) } } From 506f2aaf0e3cb1713c6adeff88e363ea032a5893 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Wed, 4 Aug 2021 19:10:44 +0200 Subject: [PATCH 3/7] Add documentation --- Sources/Algorithms/CommonPrefix.swift | 180 ++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 24 deletions(-) diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift index e0b177ce..54897849 100644 --- a/Sources/Algorithms/CommonPrefix.swift +++ b/Sources/Algorithms/CommonPrefix.swift @@ -9,6 +9,7 @@ // //===----------------------------------------------------------------------===// +/// A sequence that produces the longest common prefix of two sequences. public struct CommonPrefix { @usableFromInline internal let base: Base @@ -32,6 +33,7 @@ public struct CommonPrefix { } extension CommonPrefix: Sequence { + /// The iterator for a `CommonPrefix` sequence. public struct Iterator: IteratorProtocol { @usableFromInline internal var base: Base.Iterator @@ -74,6 +76,7 @@ extension CommonPrefix: Sequence { } extension CommonPrefix: Collection where Base: Collection, Other: Collection { + /// The index for a `CommonPrefix` collection. public struct Index { @usableFromInline internal let base: Base.Index @@ -150,6 +153,21 @@ extension CommonPrefix: LazyCollectionProtocol //===----------------------------------------------------------------------===// extension Sequence { + /// Returns an array of the longest common prefix of this sequence and the + /// other sequence, according to the given equivalence function. + /// + /// let characters = AnySequence("abcde") + /// characters.commonPrefix(with: "abce", by: ==) // ["a", "b", "c"] + /// characters.commonPrefix(with: "bcde", by: ==) // [] + /// + /// - Parameters: + /// - other: The other sequence. + /// - areEquivalent: The equivalence function. + /// - Returns: An array containing the elements in the longest common prefix + /// of `self` and `other`, according to `areEquivalent`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// prefix. @inlinable public func commonPrefix( with other: Other, @@ -171,6 +189,18 @@ extension Sequence { } extension Sequence where Element: Equatable { + /// Returns an array of the longest common prefix of this sequence and the + /// other sequence. + /// + /// let characters = AnySequence("abcde") + /// characters.commonPrefix(with: "abce") // ["a", "b", "c"] + /// characters.commonPrefix(with: "bcde") // [] + /// + /// - Parameter other: The other sequence. + /// - Returns: An array containing the elements in the longest common prefix + /// of `self` and `other`. + /// + /// - Complexity: O(1) @inlinable public func commonPrefix( with other: Other @@ -184,6 +214,8 @@ extension Sequence where Element: Equatable { //===----------------------------------------------------------------------===// extension LazySequenceProtocol { + /// Returns a lazy sequence of the longest common prefix of this sequence and + /// another sequence, according to the given equivalence function. @inlinable public func commonPrefix( with other: Other, @@ -198,6 +230,20 @@ extension LazySequenceProtocol { //===----------------------------------------------------------------------===// extension Collection { + /// Returns the longest prefix of this collection that it has in common with + /// another sequence, according to the given equivalence function. + /// + /// let string = "abcde" + /// string.commonPrefix(with: "abce", by: ==) // "abc" + /// string.commonPrefix(with: "bcde", by: ==) // "" + /// + /// - Parameters: + /// - other: The other sequence. + /// - areEquivalent: The equivalence function. + /// - Returns: The longest prefix of `self` that it has in common with + /// `other`, according to `areEquivalent`. + /// + /// - Complexity: O(*n*), where *n* is the length of the common prefix. @inlinable public func commonPrefix( with other: Other, @@ -220,6 +266,19 @@ extension Collection { } extension Collection where Element: Equatable { + /// Returns the longest prefix of this collection that it has in common with + /// another sequence. + /// + /// let string = "abcde" + /// string.commonPrefix(with: "abce") // "abc" + /// string.commonPrefix(with: "bcde") // "" + /// + /// - Parameter other: The other sequence. + /// - Returns: The longest prefix of `self` that it has in common with + /// `other`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// prefix. @inlinable public func commonPrefix( with other: Other @@ -228,34 +287,26 @@ extension Collection where Element: Equatable { } } -//===----------------------------------------------------------------------===// -// LazyCollectionProtocol.commonPrefix(with:) -//===----------------------------------------------------------------------===// - -extension LazyCollectionProtocol { - @inlinable - public func commonPrefix( - with other: Other, - by areEquivalent: @escaping (Element, Other.Element) -> Bool - ) -> CommonPrefix { - CommonPrefix(base: self, other: other, areEquivalent: areEquivalent) - } -} - -extension LazyCollectionProtocol where Element: Equatable { - @inlinable - public func commonPrefix( - with other: Other - ) -> CommonPrefix where Other.Element == Element { - commonPrefix(with: other, by: ==) - } -} - //===----------------------------------------------------------------------===// // BidirectionalCollection.commonSuffix(with:) //===----------------------------------------------------------------------===// extension BidirectionalCollection { + /// Returns the longest suffix of this collection that it has in common with + /// another collection, according to the given equivalence function. + /// + /// let string = "abcde" + /// string.commonSuffix(with: "acde", by: ==) // "acde" + /// string.commonSuffix(with: "abcd", by: ==) // "" + /// + /// - Parameters: + /// - other: The other collection. + /// - areEquivalent: The equivalence function. + /// - Returns: The longest suffix of `self` that it has in common with + /// `other`, according to `areEquivalent`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// suffix. @inlinable public func commonSuffix( with other: Other, @@ -267,6 +318,19 @@ extension BidirectionalCollection { } extension BidirectionalCollection where Element: Equatable { + /// Returns the longest suffix of this collection that it has in common with + /// another collection. + /// + /// let string = "abcde" + /// string.commonSuffix(with: "acde") // "cde" + /// string.commonSuffix(with: "abcd") // "" + /// + /// - Parameter other: The other collection. + /// - Returns: The longest suffix of `self` that it has in common with + /// `other`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// suffix. @inlinable public func commonSuffix( with other: Other @@ -280,6 +344,24 @@ extension BidirectionalCollection where Element: Equatable { //===----------------------------------------------------------------------===// extension Collection { + /// Finds the longest common prefix of this collection and another collection, + /// according to the given equivalence function, and returns the index from + /// each collection that marks the end of this prefix. + /// + /// let string1 = "abcde" + /// let string2 = "abce" + /// let (i1, i2) = string1.endOfCommonPrefix(with: string2, by: ==) + /// print(string1[..( with other: Other, @@ -300,6 +382,22 @@ extension Collection { } extension Collection where Element: Equatable { + /// Finds the longest common prefix of this collection and another collection, + /// and returns the index from each collection that marks the end of this + /// prefix. + /// + /// let string1 = "abcde" + /// let string2 = "abce" + /// let (i1, i2) = string1.endOfCommonPrefix(with: string2) + /// print(string1[..( with other: Other @@ -309,10 +407,28 @@ extension Collection where Element: Equatable { } //===----------------------------------------------------------------------===// -// BidirectionalCollection.startOfCommonPrefix(with:) +// BidirectionalCollection.startOfCommonSuffix(with:) //===----------------------------------------------------------------------===// extension BidirectionalCollection { + /// Finds the longest common suffix of this collection and another collection, + /// according to the given equivalence function, and returns the index from + /// each collection that marks the start of this suffix. + /// + /// let string1 = "abcde" + /// let string2 = "acde" + /// let (i1, i2) = string1.startOfCommonSuffix(with: string2, by: ==) + /// print(string1[..( with other: Other, @@ -341,6 +457,22 @@ extension BidirectionalCollection { } extension BidirectionalCollection where Element: Equatable { + /// Finds the longest common suffix of this collection and another collection, + /// and returns the index from each collection that marks the start of this + /// suffix. + /// + /// let string1 = "abcde" + /// let string2 = "acde" + /// let (i1, i2) = string1.startOfCommonSuffix(with: string2) + /// print(string1[..( with other: Other From 3c60204d033a4b97971b941c551e5c0ba3dfd6b8 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Wed, 4 Aug 2021 19:21:00 +0200 Subject: [PATCH 4/7] Ensure `CommonPrefix.Iterator` keeps returning `nil` after it has done so once --- Sources/Algorithms/CommonPrefix.swift | 7 ++++++- Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift index 54897849..5e188467 100644 --- a/Sources/Algorithms/CommonPrefix.swift +++ b/Sources/Algorithms/CommonPrefix.swift @@ -44,6 +44,9 @@ extension CommonPrefix: Sequence { @usableFromInline internal let areEquivalent: (Base.Element, Other.Element) -> Bool + @usableFromInline + internal var isDone = false + @inlinable internal init( base: Base.Iterator, @@ -56,11 +59,13 @@ extension CommonPrefix: Sequence { } public mutating func next() -> Base.Element? { - if let next = base.next(), + if !isDone, + let next = base.next(), let otherNext = other.next(), areEquivalent(next, otherNext) { return next } else { + isDone = true return nil } } diff --git a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift index 4d97ef06..f590f13e 100644 --- a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift +++ b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift @@ -43,6 +43,15 @@ final class CommonPrefixTests: XCTestCase { XCTAssertLazyCollection([1, 2, 3].lazy.commonPrefix(with: [4, 5, 6])) } + func testCommonPrefixIteratorKeepsReturningNil() { + var iter = AnySequence("12A34").commonPrefix(with: "12B34").makeIterator() + XCTAssertEqual(iter.next(), "1") + XCTAssertEqual(iter.next(), "2") + XCTAssertEqual(iter.next(), nil) + XCTAssertEqual(iter.next(), nil) + XCTAssertEqual(iter.next(), nil) + } + func testCommonSuffix() { func testCommonSuffix(of a: String, and b: String, equals c: String) { XCTAssertEqualSequences(a.commonSuffix(with: b, by: ==), c) From 1f7ec21b81c0f278ad2e26831efdd3b913919f1d Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 5 Aug 2021 18:25:42 +0200 Subject: [PATCH 5/7] Add the guide --- Guides/CommonPrefix.md | 166 ++++++++++++++++++++++++++ Sources/Algorithms/CommonPrefix.swift | 18 +++ 2 files changed, 184 insertions(+) create mode 100644 Guides/CommonPrefix.md diff --git a/Guides/CommonPrefix.md b/Guides/CommonPrefix.md new file mode 100644 index 00000000..68bb929b --- /dev/null +++ b/Guides/CommonPrefix.md @@ -0,0 +1,166 @@ +# Common Prefix + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/CommonPrefix.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift)] + +Methods for finding the longest common prefix or suffix of two sequences or +collections. + +```swift +let string1 = "The quick brown fox jumps over the lazy dog." +let string2 = "The quick brown fox does not jump over the lazy dog." + +string1.commonPrefix(with: string2) // "The quick brown fox " +string1.commonSuffix(with: string2) // " over the lazy dog." +``` + +Use `endOfCommonPrefix(with:)` to find the end of the longest common prefix of +two collections in both collections: + +```swift +let (i1, i2) = string1.endOfCommonPrefix(with: string2) +string1[i1...] // "jumps over the lazy dog." +string2[i2...] // "does not jump over the lazy dog." +``` + +Similarly, `startOfCommonSuffix(with:)` finds the start of the longest common +suffix of two collections in both collections: + +```swift +let (i1, i2) = string1.startOfCommonSuffix(with: string2) +string1[..` | `CommonPrefix` | +| **`Collection`** | `Self.SubSequence` | `Self.SubSequence` | +| **`Sequence`** | `CommonPrefix` | `[Self.Element]` | + +```swift +extension Sequence { + public func commonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> [Element] +} + +extension Sequence where Element: Equatable { + public func commonPrefix( + with other: Other + ) -> CommonPrefix where Other.Element == Element +} + +extension LazySequenceProtocol { + public func commonPrefix( + with other: Other, + by areEquivalent: @escaping (Element, Other.Element) -> Bool + ) -> CommonPrefix +} + +extension Collection { + public func commonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> SubSequence +} + +extension Collection where Element: Equatable { + public func commonPrefix( + with other: Other + ) -> SubSequence where Other.Element == Element +} + +extension LazyCollectionProtocol where Element: Equatable { + public func commonPrefix( + with other: Other + ) -> CommonPrefix where Other.Element == Element +} +``` + +The `commonSuffix(with:)` method is available on bidirectional collections and +takes a second bidirectional collection as its argument. It returns a +`SubSequence`. + +```swift +extension BidirectionalCollection { + public func commonSuffix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> SubSequence +} + +extension BidirectionalCollection where Element: Equatable { + public func commonSuffix( + with other: Other + ) -> SubSequence where Other.Element == Element +} +``` + +The `endOfCommonPrefix(with:)` method is available on collections and takes a +second collection as its argument, returning a pair of indices that correspond +to the end of the longest common prefix in either collection. + +```swift +extension Collection { + public func endOfCommonPrefix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> (Index, Other.Index) +} + +extension Collection where Element: Equatable { + public func endOfCommonPrefix( + with other: Other + ) -> (Index, Other.Index) where Other.Element == Element +} +``` + +The `startOfCommonSuffix(with:)` method is available on bidirectional +collections and takes a second bidirectional collection as its argument, +returning a pair of indices that correspond to the start of the longest common +suffix in either collection. + +```swift +extension BidirectionalCollection { + public func startOfCommonSuffix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> (Index, Other.Index) +} + +extension BidirectionalCollection where Element: Equatable { + public func startOfCommonSuffix( + with other: Other + ) -> (Index, Other.Index) where Other.Element == Element +} +``` + +### Complexity + +Calling these methods is O(_n_) where _n_ is the length of the corresponding +prefix or suffix, unless a `CommonSequence` is returned, in which case it's +O(1). + +### Naming + +The names `endsOfCommonPrefix` and `startsOfCommonSuffix` were considered +because a pair of indices is returned, but these were decided against in favor +of `endOfCommonPrefix` and `startOfCommonSuffix` to match the plurality of +"prefix" and "suffix". diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift index 5e188467..39caffb8 100644 --- a/Sources/Algorithms/CommonPrefix.swift +++ b/Sources/Algorithms/CommonPrefix.swift @@ -292,6 +292,24 @@ extension Collection where Element: Equatable { } } +//===----------------------------------------------------------------------===// +// LazyCollectionProtocol.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +// This overload exists in the same form on `Sequence` but is necessary to +// ensure a `CommonPrefix` is returned and not a `SubSequence`. + +extension LazyCollectionProtocol where Element: Equatable { + /// Returns a lazy collection of the longest common prefix of this collection + /// and another sequence. + @inlinable + public func commonPrefix( + with other: Other + ) -> CommonPrefix where Other.Element == Element { + commonPrefix(with: other, by: ==) + } +} + //===----------------------------------------------------------------------===// // BidirectionalCollection.commonSuffix(with:) //===----------------------------------------------------------------------===// From a4c5e7355f1a73e9b7761c85005793ada8ce174b Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 5 Aug 2021 18:29:54 +0200 Subject: [PATCH 6/7] Add missing tests --- .../CommonPrefixTests.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift index f590f13e..660a22a4 100644 --- a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift +++ b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift @@ -16,16 +16,16 @@ final class CommonPrefixTests: XCTestCase { func testCommonPrefix() { func testCommonPrefix(of a: String, and b: String, equals c: String) { // eager sequence - XCTAssertEqualSequences(AnySequence(a).commonPrefix(with: AnySequence(b), by: ==), c) + XCTAssertEqualSequences(AnySequence(a).commonPrefix(with: AnySequence(b)), c) // lazy sequence - XCTAssertEqualSequences(AnySequence(a).lazy.commonPrefix(with: AnySequence(b), by: ==), c) + XCTAssertEqualSequences(AnySequence(a).lazy.commonPrefix(with: AnySequence(b)), c) // eager collection - XCTAssertEqualSequences(a.commonPrefix(with: b, by: ==), c) + XCTAssertEqualSequences(a.commonPrefix(with: b), c) // lazy collection - XCTAssertEqualSequences(a.lazy.commonPrefix(with: b, by: ==), c) + XCTAssertEqualSequences(a.lazy.commonPrefix(with: b), c) } testCommonPrefix(of: "abcdef", and: "abcxyz", equals: "abc") @@ -69,4 +69,20 @@ final class CommonPrefixTests: XCTestCase { XCTAssertLazySequence([1, 2, 3].lazy.commonSuffix(with: [4, 5, 6])) } + + func testEndOfCommonPrefix() { + XCTAssert([1, 2, 3].endOfCommonPrefix(with: [1, 2, 3]) == (3, 3)) + XCTAssert([1, 2, 3].endOfCommonPrefix(with: [4, 5]) == (0, 0)) + XCTAssert([1, 2, 3].endOfCommonPrefix(with: [1, 2]) == (2, 2)) + XCTAssert([1, 2, 3].endOfCommonPrefix(with: [1, 2, 4]) == (2, 2)) + XCTAssert([1, 2].endOfCommonPrefix(with: [1, 2, 3]) == (2, 2)) + } + + func testStartOfCommonSuffix() { + XCTAssert([1, 2, 3].startOfCommonSuffix(with: [1, 2, 3]) == (0, 0)) + XCTAssert([1, 2, 3].startOfCommonSuffix(with: [4, 5]) == (3, 2)) + XCTAssert([1, 2, 3].startOfCommonSuffix(with: [2, 3]) == (1, 0)) + XCTAssert([0, 1, 2, 3].startOfCommonSuffix(with: [0, 2, 3]) == (2, 1)) + XCTAssert([2, 3].startOfCommonSuffix(with: [1, 2, 3]) == (0, 1)) + } } From f63b9a491404047e265b2af18fc4396e0050ff2f Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Thu, 5 Aug 2021 20:54:57 +0200 Subject: [PATCH 7/7] Finishing touches --- Guides/CommonPrefix.md | 10 +++++----- Sources/Algorithms/CommonPrefix.swift | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Guides/CommonPrefix.md b/Guides/CommonPrefix.md index 68bb929b..ae6b6972 100644 --- a/Guides/CommonPrefix.md +++ b/Guides/CommonPrefix.md @@ -47,11 +47,11 @@ function is provided and a `CommonPrefix` otherwise. both base sequences conform. It also conforms to `LazySequenceProtocol` and `LazyCollectionProtocol` when the first base sequence conforms. -| | `commonPrefix(with:)` | `commonPrefix(with:by:)` -|----------------------------|----------------------------------|------------------------------| -| **`LazySequenceProtocol`** | `CommonPrefix` | `CommonPrefix` | -| **`Collection`** | `Self.SubSequence` | `Self.SubSequence` | -| **`Sequence`** | `CommonPrefix` | `[Self.Element]` | +| | `commonPrefix(with:)` | `commonPrefix(with:by:)` +|------------------------------|------------------------------|------------------------------| +| **`LazySequenceProtocol`** | `CommonPrefix` | `CommonPrefix` | +| **`Collection`** | `Self.SubSequence` | `Self.SubSequence` | +| **`Sequence`** | `CommonPrefix` | `[Self.Element]` | ```swift extension Sequence { diff --git a/Sources/Algorithms/CommonPrefix.swift b/Sources/Algorithms/CommonPrefix.swift index 39caffb8..ff32a56e 100644 --- a/Sources/Algorithms/CommonPrefix.swift +++ b/Sources/Algorithms/CommonPrefix.swift @@ -158,8 +158,8 @@ extension CommonPrefix: LazyCollectionProtocol //===----------------------------------------------------------------------===// extension Sequence { - /// Returns an array of the longest common prefix of this sequence and the - /// other sequence, according to the given equivalence function. + /// Returns an array of the longest common prefix of this sequence and another + /// sequence, according to the given equivalence function. /// /// let characters = AnySequence("abcde") /// characters.commonPrefix(with: "abce", by: ==) // ["a", "b", "c"] @@ -167,7 +167,8 @@ extension Sequence { /// /// - Parameters: /// - other: The other sequence. - /// - areEquivalent: The equivalence function. + /// - areEquivalent: A predicate that returns true if its two arguments are + /// equivalent; otherwise, false. /// - Returns: An array containing the elements in the longest common prefix /// of `self` and `other`, according to `areEquivalent`. /// @@ -194,8 +195,8 @@ extension Sequence { } extension Sequence where Element: Equatable { - /// Returns an array of the longest common prefix of this sequence and the - /// other sequence. + /// Returns an array of the longest common prefix of this sequence and another + /// sequence. /// /// let characters = AnySequence("abcde") /// characters.commonPrefix(with: "abce") // ["a", "b", "c"] @@ -244,7 +245,8 @@ extension Collection { /// /// - Parameters: /// - other: The other sequence. - /// - areEquivalent: The equivalence function. + /// - areEquivalent: A predicate that returns true if its two arguments are + /// equivalent; otherwise, false. /// - Returns: The longest prefix of `self` that it has in common with /// `other`, according to `areEquivalent`. /// @@ -324,7 +326,8 @@ extension BidirectionalCollection { /// /// - Parameters: /// - other: The other collection. - /// - areEquivalent: The equivalence function. + /// - areEquivalent: A predicate that returns true if its two arguments are + /// equivalent; otherwise, false. /// - Returns: The longest suffix of `self` that it has in common with /// `other`, according to `areEquivalent`. /// @@ -379,7 +382,8 @@ extension Collection { /// /// - Parameters: /// - other: The other collection. - /// - areEquivalent: The equivalence function. + /// - areEquivalent: A predicate that returns true if its two arguments are + /// equivalent; otherwise, false. /// - Returns: A pair of indices from `self` and `other` that mark the end of /// their longest common prefix according to `areEquivalent`. /// @@ -446,7 +450,8 @@ extension BidirectionalCollection { /// /// - Parameters: /// - other: The other collection. - /// - areEquivalent: The equivalence function. + /// - areEquivalent: A predicate that returns true if its two arguments are + /// equivalent; otherwise, false. /// - Returns: A pair of indices from `self` and `other` that mark the start /// of their longest common suffix according to `areEquivalent`. ///