diff --git a/Guides/CommonPrefix.md b/Guides/CommonPrefix.md new file mode 100644 index 00000000..ae6b6972 --- /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 new file mode 100644 index 00000000..ff32a56e --- /dev/null +++ b/Sources/Algorithms/CommonPrefix.swift @@ -0,0 +1,510 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A sequence that produces the longest common prefix of two sequences. +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 { + /// The iterator for a `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 + + @usableFromInline + internal var isDone = false + + @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 !isDone, + let next = base.next(), + let otherNext = other.next(), + areEquivalent(next, otherNext) { + return next + } else { + isDone = true + return nil + } + } + } + + @inlinable + public func makeIterator() -> Iterator { + Iterator( + base: base.makeIterator(), + other: other.makeIterator(), + areEquivalent: areEquivalent) + } +} + +extension CommonPrefix: Collection where Base: Collection, Other: Collection { + /// The index for a `CommonPrefix` 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 { + /// 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"] + /// characters.commonPrefix(with: "bcde", by: ==) // [] + /// + /// - Parameters: + /// - other: The other sequence. + /// - 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`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// prefix. + @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 { + /// 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"] + /// 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 + ) -> CommonPrefix where Other.Element == Element { + CommonPrefix(base: self, other: other, areEquivalent: ==) + } +} + +//===----------------------------------------------------------------------===// +// LazySequenceProtocol.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +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, + by areEquivalent: @escaping (Element, Other.Element) -> Bool + ) -> CommonPrefix { + CommonPrefix(base: self, other: other, areEquivalent: areEquivalent) + } +} + +//===----------------------------------------------------------------------===// +// Collection.commonPrefix(with:) +//===----------------------------------------------------------------------===// + +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: 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`. + /// + /// - Complexity: O(*n*), where *n* is the length of the common prefix. + @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:) +//===----------------------------------------------------------------------===// + +// 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:) +//===----------------------------------------------------------------------===// + +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: 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`. + /// + /// - Complexity: O(*n*), where *n* is the length of the longest common + /// suffix. + @inlinable + public func commonSuffix( + with other: Other, + by areEquivalent: (Element, Other.Element) throws -> Bool + ) rethrows -> SubSequence { + let (index, _) = try startOfCommonSuffix(with: other, by: areEquivalent) + return self[index...] + } +} + +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 + ) -> SubSequence where Other.Element == Element { + commonSuffix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// Collection.endOfCommonPrefix(with:) +//===----------------------------------------------------------------------===// + +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, + 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 { + /// 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 + ) -> (Index, Other.Index) where Other.Element == Element { + endOfCommonPrefix(with: other, by: ==) + } +} + +//===----------------------------------------------------------------------===// +// 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, + 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 { + /// 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 + ) -> (Index, Other.Index) where Other.Element == Element { + startOfCommonSuffix(with: other, by: ==) + } +} diff --git a/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift new file mode 100644 index 00000000..660a22a4 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/CommonPrefixTests.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// 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)), c) + + // lazy sequence + XCTAssertEqualSequences(AnySequence(a).lazy.commonPrefix(with: AnySequence(b)), c) + + // eager collection + XCTAssertEqualSequences(a.commonPrefix(with: b), c) + + // lazy collection + XCTAssertEqualSequences(a.lazy.commonPrefix(with: b), 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 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) + } + + 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])) + } + + 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)) + } +}