From 7eabb3ff171b76d704ac1c70d4f7eaf4d2b55544 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Fri, 12 Jul 2024 21:16:20 -0400 Subject: [PATCH 1/9] Add function to test the inclusion of one sorted sequence into another Add a function for a sequence that takes another sequence and a predicate, assumes that both sequences are sorted to that predicate, and checks if the argument sequence is a subset of the receiver. Add a function that takes the above function, but defaults the predicate to the less-than operator. Add tests and documentation. --- Guides/Inclusion.md | 74 ++++++++++++++++ .../Documentation.docc/Algorithms.md | 1 + .../Documentation.docc/Inclusion.md | 12 +++ Sources/Algorithms/Includes.swift | 87 +++++++++++++++++++ .../SwiftAlgorithmsTests/IncludesTests.swift | 60 +++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 Guides/Inclusion.md create mode 100644 Sources/Algorithms/Documentation.docc/Inclusion.md create mode 100644 Sources/Algorithms/Includes.swift create mode 100644 Tests/SwiftAlgorithmsTests/IncludesTests.swift diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md new file mode 100644 index 00000000..cdaae87c --- /dev/null +++ b/Guides/Inclusion.md @@ -0,0 +1,74 @@ +# Inclusion + +[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Includes.swift) | + [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/IncludesTests.swift)] + +Check if one sequence includes another. +Both sequences must be sorted along the same criteria, +which must provide a strict weak ordering. + +```swift +let first = [4, 3, 2, 1], second = [3, 2, 1, 0], third = [3, 2] +let firstIncludesSecond = first.includes(sorted: second, sortedBy: >) // false +let secondIncludesThird = second.includes(sorted: third, sortedBy: >) // true +let thirdIncludesFirst = third.includes(sorted: first, sortedBy: >) // false +let firstIncludesThird = first.includes(sorted: third, sortedBy: >) // true +let thirdIncludesSecond = third.includes(sorted: second, sortedBy: >) // false +let secondIncludesFirst = second.includes(sorted: first, sortedBy: >) // false +``` + +If a predicate is not supplied, +then the less-than operator is used, +but only if the `Element` type conforms to `Comparable`. + +```swift +(1...3).includes(sorted: 1..<3) // true +``` + +## Detailed Design + +Two new methods are added to `Sequence`: + +```swift +extension Sequence { + public func + includes( + sorted other: T, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Bool + where T : Sequence, Self.Element == T.Element +} + +extension Sequence where Self.Element : Comparable { + @inlinable public func + includes( + sorted other: T + ) -> Bool + where T : Sequence, Self.Element == T.Element +} +``` + +The `Sequence.includes(sorted:)` method calls the +`Sequence.includes(sorted:sortedBy:)` method with the less-than operator for +the latter's second argument. + +### Complexity + +Calling either method is O(_n_), +where *n* is the length of the shorter sequence. + +### Naming + +These methods' base name is inspired by the C++ function `std::includes`. + +### Comparison with Other Languages + +**[C++][C++]:** Has an `includes` function family. + +**[Haskell][Haskell]:** Has the `isInfixOf` function, plus the `isPrefixOf`, +`isSuffixOf`, and `isSubsequenceOf` functions. + + + +[C++]: https://en.cppreference.com/w/cpp/algorithm/includes +[Haskell]: https://hackage.haskell.org/package/base-4.20.0.1/docs/Data-List.html#v:isInfixOf diff --git a/Sources/Algorithms/Documentation.docc/Algorithms.md b/Sources/Algorithms/Documentation.docc/Algorithms.md index 3cfb2693..4bcab3e4 100644 --- a/Sources/Algorithms/Documentation.docc/Algorithms.md +++ b/Sources/Algorithms/Documentation.docc/Algorithms.md @@ -40,3 +40,4 @@ Explore more chunking methods and the remainder of the Algorithms package, group - - - +- diff --git a/Sources/Algorithms/Documentation.docc/Inclusion.md b/Sources/Algorithms/Documentation.docc/Inclusion.md new file mode 100644 index 00000000..9970cce7 --- /dev/null +++ b/Sources/Algorithms/Documentation.docc/Inclusion.md @@ -0,0 +1,12 @@ +# Inclusion + +Check if one sorted sequence is completely contained in another. +The sort criteria is a user-supplied predicate. +The predicate can be omitted to default to the less-than operator. + +## Topics + +### Inclusion + +- ``Swift/Sequence/includes(sorted:sortedBy:)`` +- ``Swift/Sequence/includes(sorted:)`` diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift new file mode 100644 index 00000000..ac9a8067 --- /dev/null +++ b/Sources/Algorithms/Includes.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2024 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// MARK: Sequence.includes(sorted:sortedBy:) +//-------------------------------------------------------------------------===// + +extension Sequence { + /// Assuming that this sequence and the given sequence are sorted according + /// to the given predicate, determine whether the given sequence is contained + /// within this one. + /// + /// - Precondition: Both the receiver and `other` must be sorted according to + /// `areInIncreasingOrder`, which must be a strict weak ordering over + /// its arguments. Either the receiver, `other`, or both must be finite. + /// + /// - Parameters: + /// - other: The sequence that is compared against the receiver. + /// - areInIncreasingOrder: The sorting criteria. + /// - Returns: Whether the entirety of `other` is contained within this + /// sequence. + /// + /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. + public func includes( + sorted other: T, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Bool + where T.Element == Element { + // Originally, there was a function that evaluated the two sequences' + // elements with respect to having elements exclusive to the receiver, + // elements exclusive to `other`, and shared elements. But this function + // only needs to know when an element that is exclusive to the `other` is + // found. So, that function's guts were ripped out and repurposed. + var firstElement, secondElement: Element? + var iterator = makeIterator(), otherIterator = other.makeIterator() + while true { + firstElement = firstElement ?? iterator.next() + secondElement = secondElement ?? otherIterator.next() + switch (firstElement, secondElement) { + case let (first?, second?) where try areInIncreasingOrder(first, second): + // Found an element exclusive to `self`, move on. + firstElement = nil + case let (first?, second?) where try areInIncreasingOrder(second, first): + // Found an element exclusive to `other`. + return false + case (_?, _?): + // Found a shared element, move on. + firstElement = nil + secondElement = nil + case (nil, _?): + // Found an element exclusive to `other`, and any remaining elements + // will be exclusive to `other`. + return false + default: + // The elements from `other` (and possibly `self` too) have been + // exhausted without disproving inclusion. + return true + } + } + } +} + +extension Sequence where Element: Comparable { + /// Assuming that this sequence and the given sequence are sorted, + /// determine whether the given sequence is contained within this one. + /// + /// - Precondition: Either the receiver, `other`, or both must be finite. + /// + /// - Parameter other: The sequence that is compared against the receiver. + /// - Returns: Whether the entirety of `other` is contained within this + /// sequence. + /// + /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. + @inlinable + public func includes(sorted other: T) -> Bool + where T.Element == Element { + return includes(sorted: other, sortedBy: <) + } +} diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift new file mode 100644 index 00000000..390859b6 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Algorithms open source project +// +// Copyright (c) 2024 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 IncludesTests: XCTestCase { + /// Check if one empty set includes another. + func testBothSetsEmpty() { + XCTAssertTrue(EmptyCollection().includes(sorted: EmptyCollection())) + } + + /// Check if a non-empty set contains an empty one. + func testNonemptyIncludesEmpty() { + XCTAssertTrue(CollectionOfOne(2).includes(sorted: EmptyCollection())) + } + + /// Check if an empty set contains a non-empty one. + func testEmptyIncludesNonempty() { + XCTAssertFalse(EmptyCollection().includes(sorted: CollectionOfOne(2))) + } + + /// Check for inclusion between disjoint (non-empty) sets. + func testDisjointSets() { + XCTAssertFalse("abc".includes(sorted: "DEF")) + } + + /// Check if a non-empty set includes an identical one. + func testIdenticalSets() { + XCTAssertTrue([0, 1, 2, 3].includes(sorted: 0..<4)) + } + + /// Check if a set includes a strict non-empty subset. + func testStrictSubset() { + XCTAssertTrue([0, 1, 2, 3].includes(sorted: 1..<3)) + XCTAssertTrue([0, 1, 2, 3].includes(sorted: 0..<2)) + XCTAssertTrue([0, 1, 2, 3].includes(sorted: 2..<4)) + } + + /// Check if a non-empty set incudes a strict superset. + func testStrictSuperset() { + XCTAssertFalse([0, 1, 2, 3].includes(sorted: -1..<5)) + XCTAssertFalse([0, 1, 2, 3].includes(sorted: -1..<4)) + XCTAssertFalse([0, 1, 2, 3].includes(sorted: 0..<5)) + } + + /// Check if a non-empty set includes another that shares just some elements. + func testOverlap() { + XCTAssertFalse([0, 1, 2, 3].includes(sorted: 2..<5)) + XCTAssertFalse([0, 1, 2, 3].includes(sorted: -1..<2)) + } +} From 8190b55f9cce77adfedc3665f9f4e940583d4c82 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Sat, 13 Jul 2024 14:09:28 -0400 Subject: [PATCH 2/9] Add missing documentation --- Sources/Algorithms/Includes.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index ac9a8067..6e76db07 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -72,7 +72,8 @@ extension Sequence where Element: Comparable { /// Assuming that this sequence and the given sequence are sorted, /// determine whether the given sequence is contained within this one. /// - /// - Precondition: Either the receiver, `other`, or both must be finite. + /// - Precondition: Both the receiver and `other` must be sorted. + /// At least one of the involved sequences must be finite. /// /// - Parameter other: The sequence that is compared against the receiver. /// - Returns: Whether the entirety of `other` is contained within this From 7597e5fee6706d83fce2a4b9cb6f96e4d9d22385 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Sat, 13 Jul 2024 14:37:22 -0400 Subject: [PATCH 3/9] Add code examples to the functions' docs --- Sources/Algorithms/Includes.swift | 12 ++++++++++++ Tests/SwiftAlgorithmsTests/IncludesTests.swift | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index 6e76db07..ed4e7ee8 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -18,6 +18,12 @@ extension Sequence { /// to the given predicate, determine whether the given sequence is contained /// within this one. /// + /// let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] + /// assert(base.includes(sorted: [8, 7, 6, 2, 1], sortedBy: >)) + /// assert(!base.includes(sorted: [8, 7, 5, 2, 1], sortedBy: >)) + /// + /// The elements of the argument need not be contiguous in the receiver. + /// /// - Precondition: Both the receiver and `other` must be sorted according to /// `areInIncreasingOrder`, which must be a strict weak ordering over /// its arguments. Either the receiver, `other`, or both must be finite. @@ -72,6 +78,12 @@ extension Sequence where Element: Comparable { /// Assuming that this sequence and the given sequence are sorted, /// determine whether the given sequence is contained within this one. /// + /// let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] + /// assert(base.includes(sorted: [1, 2, 6, 7, 8])) + /// assert(!base.includes(sorted: [1, 2, 5, 7, 8])) + /// + /// The elements of the argument need not be contiguous in the receiver. + /// /// - Precondition: Both the receiver and `other` must be sorted. /// At least one of the involved sequences must be finite. /// diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift index 390859b6..d0b7f474 100644 --- a/Tests/SwiftAlgorithmsTests/IncludesTests.swift +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -57,4 +57,18 @@ final class IncludesTests: XCTestCase { XCTAssertFalse([0, 1, 2, 3].includes(sorted: 2..<5)) XCTAssertFalse([0, 1, 2, 3].includes(sorted: -1..<2)) } + + /// Confirm the example code from `Sequence.includes(sorted:sortedBy:)`. + func testFirstDiscussionCode() { + let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] + XCTAssertTrue(base.includes(sorted: [8, 7, 6, 2, 1], sortedBy: >)) + XCTAssertFalse(base.includes(sorted: [8, 7, 5, 2, 1], sortedBy: >)) + } + + /// Confirm the example code from `Sequence.includes(sorted:)`. + func testSecondDiscussionCode() { + let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] + XCTAssertTrue(base.includes(sorted: [1, 2, 6, 7, 8])) + XCTAssertFalse(base.includes(sorted: [1, 2, 5, 7, 8])) + } } From e35561911194c9701e1a4c1b4e92895cafcfb306 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 19 Aug 2024 00:23:55 -0400 Subject: [PATCH 4/9] Add functions checking overlap of sorted sequences --- Guides/Inclusion.md | 58 ++++- .../Documentation.docc/Inclusion.md | 3 + Sources/Algorithms/Includes.swift | 208 +++++++++++++++--- .../SwiftAlgorithmsTests/IncludesTests.swift | 29 +++ 4 files changed, 265 insertions(+), 33 deletions(-) diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md index cdaae87c..adaf9d7e 100644 --- a/Guides/Inclusion.md +++ b/Guides/Inclusion.md @@ -3,7 +3,7 @@ [[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Includes.swift) | [Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/IncludesTests.swift)] -Check if one sequence includes another. +Check if one sequence includes another with the `includes` function. Both sequences must be sorted along the same criteria, which must provide a strict weak ordering. @@ -17,26 +17,66 @@ let thirdIncludesSecond = third.includes(sorted: second, sortedBy: >) // false let secondIncludesFirst = second.includes(sorted: first, sortedBy: >) // false ``` +For a more detailed computation of how much the two sequences intersect, +use the `overlap` function. + +```swift +let firstOverlapThird = first.overlap(withSorted: third, sortedBy: >) +assert(firstOverlapThird.elementsFromSelf == .some(true)) +assert(firstOverlapThird.sharedElements == .some(true)) +assert(firstOverlapThird.elementsFromOther == .some(false)) +``` + +By default, `overlap` returns its result after at least one of the sequences ends. +To immediately end comparisons as soon as an element for a particular category is found, +pass in the appropriate "`bail`" argument. + +```swift +let firstOverlapThirdAgain = first.overlap(withSorted: third, bailAfterShared: true, sortedBy: >) +assert(firstOverlapThirdAgain.sharedElements == .some(true)) +assert(firstOverlapThirdAgain.elementsFromSelf == .some(true)) +assert(firstOverlapThirdAgain.elementsFromOther == .none) +``` + +When `overlap` ends by a short-circut, +only the flag for the short-circuted category will be `true`. +The returned flags for the other categories may be `nil`. +If multiple categories are flagged for short-circuting, +only the first one found is guaranteed to be `true`. +When no bailing argument is supplied, +none of the returned flags will be `nil`. + If a predicate is not supplied, then the less-than operator is used, but only if the `Element` type conforms to `Comparable`. ```swift (1...3).includes(sorted: 1..<3) // true +(1...3).overlap(sorted: 1..<3).elementsFromOther // .some(false) ``` ## Detailed Design -Two new methods are added to `Sequence`: +Four new methods are added to `Sequence`: ```swift extension Sequence { - public func + @inlinable public func includes( sorted other: T, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool ) rethrows -> Bool where T : Sequence, Self.Element == T.Element + + public func + overlap( + withSorted other: T, + bailAfterSelfExclusive: Bool = false, + bailAfterShared: Bool = false, + bailAfterOtherExclusive: Bool = false, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> (elementsFromSelf: Bool?, sharedElements: Bool?, elementsFromOther: Bool?) + where T : Sequence, Self.Element == T.Element } extension Sequence where Self.Element : Comparable { @@ -45,16 +85,26 @@ extension Sequence where Self.Element : Comparable { sorted other: T ) -> Bool where T : Sequence, Self.Element == T.Element + + @inlinable public func + overlap( + withSorted other: T, + bailAfterSelfExclusive: Bool = false, + bailAfterShared: Bool = false, + bailAfterOtherExclusive: Bool = false + ) -> (elementsFromSelf: Bool?, sharedElements: Bool?, elementsFromOther: Bool?) + where T : Sequence, Self.Element == T.Element } ``` The `Sequence.includes(sorted:)` method calls the `Sequence.includes(sorted:sortedBy:)` method with the less-than operator for the latter's second argument. +The same relationship applies to both versions of `Sequence.overlap`. ### Complexity -Calling either method is O(_n_), +Calling any of these methods is O(_n_), where *n* is the length of the shorter sequence. ### Naming diff --git a/Sources/Algorithms/Documentation.docc/Inclusion.md b/Sources/Algorithms/Documentation.docc/Inclusion.md index 9970cce7..2b71e273 100644 --- a/Sources/Algorithms/Documentation.docc/Inclusion.md +++ b/Sources/Algorithms/Documentation.docc/Inclusion.md @@ -1,6 +1,7 @@ # Inclusion Check if one sorted sequence is completely contained in another. +Can also determine the degree overlap between two sorted sequences. The sort criteria is a user-supplied predicate. The predicate can be omitted to default to the less-than operator. @@ -10,3 +11,5 @@ The predicate can be omitted to default to the less-than operator. - ``Swift/Sequence/includes(sorted:sortedBy:)`` - ``Swift/Sequence/includes(sorted:)`` +- ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:sortedBy:)`` +- ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:)`` diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index ed4e7ee8..b7512770 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -35,66 +35,216 @@ extension Sequence { /// sequence. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. + @inlinable public func includes( sorted other: T, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool ) rethrows -> Bool where T.Element == Element { - // Originally, there was a function that evaluated the two sequences' - // elements with respect to having elements exclusive to the receiver, - // elements exclusive to `other`, and shared elements. But this function - // only needs to know when an element that is exclusive to the `other` is - // found. So, that function's guts were ripped out and repurposed. + return try !overlap(withSorted: other, bailAfterOtherExclusive: true, + sortedBy: areInIncreasingOrder).elementsFromOther! + } +} + +extension Sequence where Element: Comparable { + /// Assuming that this sequence and the given sequence are sorted, + /// determine whether the given sequence is contained within this one. + /// + /// let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] + /// assert(base.includes(sorted: [1, 2, 6, 7, 8])) + /// assert(!base.includes(sorted: [1, 2, 5, 7, 8])) + /// + /// The elements of the argument need not be contiguous in the receiver. + /// + /// - Precondition: Both the receiver and `other` must be sorted. + /// At least one of the involved sequences must be finite. + /// + /// - Parameter other: The sequence that is compared against the receiver. + /// - Returns: Whether the entirety of `other` is contained within this + /// sequence. + /// + /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. + @inlinable + public func includes(sorted other: T) -> Bool + where T.Element == Element { + return includes(sorted: other, sortedBy: <) + } +} + +//===----------------------------------------------------------------------===// +// MARK: - Sequence.overlap(withSorted:sortedBy:) +//-------------------------------------------------------------------------===// + +extension Sequence { + /// Assuming that this sequence and the given sequence are sorted according + /// to the given predicate, check if the sequences have overlap and/or + /// exclusive elements. + /// + /// let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] + /// let test1 = base.overlap(withSorted: [8, 7, 6, 2, 1], sortedBy: >) + /// let test2 = base.overlap(withSorted: [8, 7, 5, 2, 1], sortedBy: >) + /// assert(test1.elementsFromSelf!) + /// assert(test1.sharedElements!) + /// assert(!test1.elementsFromOther!) + /// assert(test2.elementsFromSelf!) + /// assert(test2.sharedElements!) + /// assert(test2.elementsFromOther!) + /// + /// - Precondition: Both the receiver and `other` must be sorted according to + /// `areInIncreasingOrder`, + /// which must be a strict weak ordering over its arguments. + /// Either the receiver, `other`, or both must be finite. + /// + /// - Parameters: + /// - other: The sequence that is compared against the receiver. + /// - areInIncreasingOrder: The sorting criteria. + /// - bailAfterSelfExclusive: Indicate that this function should abort as + /// soon as one element that is exclusive to this sequence is found. + /// If not given, defaults to `false`. + /// - bailAfterShared: Indicate that this function should abort as soon as + /// an element that both sequences share is found. + /// If not given, defaults to `false`. + /// - bailAfterOtherExclusive: Indicate that this function should abort as + /// soon as one element that is exclusive to `other` is found. + /// If not given, defaults to `false`. + /// - Returns: A tuple of three `Bool` members indicating whether there are + /// elements exclusive to `self`, + /// there are elements shared between the sequences, + /// and there are elements exclusive to `other`. + /// If a member is `true`, + /// then at least one element in that category exists. + /// If a member is `false`, + /// then there are no elements in that category. + /// If a member is `nil`, + /// then the function aborted before its category's status could be + /// determined. + /// + /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. + public func overlap( + withSorted other: T, + bailAfterSelfExclusive: Bool = false, + bailAfterShared: Bool = false, + bailAfterOtherExclusive: Bool = false, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> ( + elementsFromSelf: Bool?, + sharedElements: Bool?, + elementsFromOther: Bool? + ) + where T.Element == Element { var firstElement, secondElement: Element? var iterator = makeIterator(), otherIterator = other.makeIterator() - while true { + var result: (fromSelf: Bool?, shared: Bool?, fromOther: Bool?) + loop: + while result != (true, true, true) { firstElement = firstElement ?? iterator.next() secondElement = secondElement ?? otherIterator.next() switch (firstElement, secondElement) { - case let (first?, second?) where try areInIncreasingOrder(first, second): - // Found an element exclusive to `self`, move on. + case let (s?, o?) where try areInIncreasingOrder(s, o): + // Exclusive to self + result.fromSelf = true + guard !bailAfterSelfExclusive else { break loop } + + // Move to the next element in self. firstElement = nil - case let (first?, second?) where try areInIncreasingOrder(second, first): - // Found an element exclusive to `other`. - return false + case let (s?, o?) where try areInIncreasingOrder(o, s): + // Exclusive to other + result.fromOther = true + guard !bailAfterOtherExclusive else { break loop } + + // Move to the next element in other. + secondElement = nil case (_?, _?): - // Found a shared element, move on. + // Shared + result.shared = true + guard !bailAfterShared else { break loop } + + // Iterate to the next element for both sequences. firstElement = nil secondElement = nil + case (_?, nil): + // Never bail, just finalize after finding an exclusive to self. + result.fromSelf = true + result.shared = result.shared ?? false + result.fromOther = result.fromOther ?? false + break loop case (nil, _?): - // Found an element exclusive to `other`, and any remaining elements - // will be exclusive to `other`. - return false - default: - // The elements from `other` (and possibly `self` too) have been - // exhausted without disproving inclusion. - return true + // Never bail, just finalize after finding an exclusive to other. + result.fromSelf = result.fromSelf ?? false + result.shared = result.shared ?? false + result.fromOther = true + break loop + case (nil, nil): + // Finalize everything instead of bailing + result.fromSelf = result.fromSelf ?? false + result.shared = result.shared ?? false + result.fromOther = result.fromOther ?? false + break loop } } + return (result.fromSelf, result.shared, result.fromOther) } } extension Sequence where Element: Comparable { /// Assuming that this sequence and the given sequence are sorted, - /// determine whether the given sequence is contained within this one. + /// check if the sequences have overlap and/or exclusive elements. /// /// let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] - /// assert(base.includes(sorted: [1, 2, 6, 7, 8])) - /// assert(!base.includes(sorted: [1, 2, 5, 7, 8])) - /// - /// The elements of the argument need not be contiguous in the receiver. + /// let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) + /// let test2 = base.overlap(withSorted: [1, 2, 5, 7, 8]) + /// assert(test1.elementsFromSelf!) + /// assert(test1.sharedElements!) + /// assert(!test1.elementsFromOther!) + /// assert(test2.elementsFromSelf!) + /// assert(test2.sharedElements!) + /// assert(test2.elementsFromOther!) /// /// - Precondition: Both the receiver and `other` must be sorted. /// At least one of the involved sequences must be finite. /// - /// - Parameter other: The sequence that is compared against the receiver. - /// - Returns: Whether the entirety of `other` is contained within this - /// sequence. + /// - Parameters: + /// - other: The sequence that is compared against the receiver. + /// - bailAfterSelfExclusive: Indicate that this function should abort as + /// soon as one element that is exclusive to this sequence is found. + /// If not given, defaults to `false`. + /// - bailAfterShared: Indicate that this function should abort as soon as + /// an element that both sequences share is found. + /// If not given, defaults to `false`. + /// - bailAfterOtherExclusive: Indicate that this function should abort as + /// soon as one element that is exclusive to `other` is found. + /// If not given, defaults to `false`. + /// - Returns: A tuple of three `Bool` members indicating whether there are + /// elements exclusive to `self`, + /// elements shared between the sequences, + /// and elements exclusive to `other`. + /// If a member is `true`, + /// then at least one element in that category exists. + /// If a member is `false`, + /// then there are no elements in that category. + /// If a member is `nil`, + /// then the function aborted before its category's status could be + /// determined. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. @inlinable - public func includes(sorted other: T) -> Bool + public func overlap( + withSorted other: T, + bailAfterSelfExclusive: Bool = false, + bailAfterShared: Bool = false, + bailAfterOtherExclusive: Bool = false + ) -> ( + elementsFromSelf: Bool?, + sharedElements: Bool?, + elementsFromOther: Bool? + ) where T.Element == Element { - return includes(sorted: other, sortedBy: <) + return overlap( + withSorted: other, + bailAfterSelfExclusive: bailAfterSelfExclusive, + bailAfterShared: bailAfterShared, + bailAfterOtherExclusive: bailAfterOtherExclusive, + sortedBy: < + ) } } diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift index d0b7f474..56fc7873 100644 --- a/Tests/SwiftAlgorithmsTests/IncludesTests.swift +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -71,4 +71,33 @@ final class IncludesTests: XCTestCase { XCTAssertTrue(base.includes(sorted: [1, 2, 6, 7, 8])) XCTAssertFalse(base.includes(sorted: [1, 2, 5, 7, 8])) } + + /// Confirm the example code from `Sequence.overlap(withSorted:` + /// `bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:` + /// `sortedBy:)`. + func testThirdDiscussionCode() { + let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] + let test1 = base.overlap(withSorted: [8, 7, 6, 2, 1], sortedBy: >) + let test2 = base.overlap(withSorted: [8, 7, 5, 2, 1], sortedBy: >) + XCTAssertTrue(test1.elementsFromSelf!) + XCTAssertTrue(test1.sharedElements!) + XCTAssertFalse(test1.elementsFromOther!) + XCTAssertTrue(test2.elementsFromSelf!) + XCTAssertTrue(test2.sharedElements!) + XCTAssertTrue(test2.elementsFromOther!) + } + + /// Confirm the example code from `Sequence.overlap(withSorted:` + /// `bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:)`. + func testFourthDiscussionCode() { + let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] + let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) + let test2 = base.overlap(withSorted: [1, 2, 5, 7, 8]) + XCTAssertTrue(test1.elementsFromSelf!) + XCTAssertTrue(test1.sharedElements!) + XCTAssertFalse(test1.elementsFromOther!) + XCTAssertTrue(test2.elementsFromSelf!) + XCTAssertTrue(test2.sharedElements!) + XCTAssertTrue(test2.elementsFromOther!) + } } From ba38c7a293f3f00af349b0de3cd8e661cef78e76 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 19 Aug 2024 01:49:41 -0400 Subject: [PATCH 5/9] Change the return for the overlap functions to a custom type --- Guides/Inclusion.md | 37 ++-- .../Documentation.docc/Inclusion.md | 6 +- Sources/Algorithms/Includes.swift | 172 +++++++++++------- .../SwiftAlgorithmsTests/IncludesTests.swift | 49 +++-- 4 files changed, 177 insertions(+), 87 deletions(-) diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md index adaf9d7e..24d60550 100644 --- a/Guides/Inclusion.md +++ b/Guides/Inclusion.md @@ -22,9 +22,9 @@ use the `overlap` function. ```swift let firstOverlapThird = first.overlap(withSorted: third, sortedBy: >) -assert(firstOverlapThird.elementsFromSelf == .some(true)) -assert(firstOverlapThird.sharedElements == .some(true)) -assert(firstOverlapThird.elementsFromOther == .some(false)) +assert(firstOverlapThird.hasElementsExclusiveToFirst) +assert(firstOverlapThird.hasSharedElements) +assert(!firstOverlapThird.hasElementsExclusiveToSecond) ``` By default, `overlap` returns its result after at least one of the sequences ends. @@ -33,18 +33,16 @@ pass in the appropriate "`bail`" argument. ```swift let firstOverlapThirdAgain = first.overlap(withSorted: third, bailAfterShared: true, sortedBy: >) -assert(firstOverlapThirdAgain.sharedElements == .some(true)) -assert(firstOverlapThirdAgain.elementsFromSelf == .some(true)) -assert(firstOverlapThirdAgain.elementsFromOther == .none) +assert(firstOverlapThirdAgain.hasSharedElements) +// The other two flags may have random values. ``` When `overlap` ends by a short-circut, -only the flag for the short-circuted category will be `true`. -The returned flags for the other categories may be `nil`. -If multiple categories are flagged for short-circuting, -only the first one found is guaranteed to be `true`. -When no bailing argument is supplied, -none of the returned flags will be `nil`. +exactly one of the short-circut categories in the argument list will have its +flag in the return value set to `true`. +Otherwise all of the short-circut categories will have their flags as `false`. +Only in the latter case are the flags for categories not short-circuted would +be accurate. If a predicate is not supplied, then the less-than operator is used, @@ -97,6 +95,21 @@ extension Sequence where Self.Element : Comparable { } ``` +And one type: + +```swift +public enum OverlapDegree : UInt, CaseIterable { + case bothEmpty, onlyFirstNonempty, onlySecondNonempty, disjoint,identical, + firstIncludesNonemptySecond, secondIncludesNonemptyFirst, partialOverlap +} + +extension OverlapDegree { + @inlinable public var hasElementsExclusiveToFirst: Bool { get } + @inlinable public var hasElementsExclusiveToSecond: Bool { get } + @inlinable public var hasSharedElements: Bool { get } +} +``` + The `Sequence.includes(sorted:)` method calls the `Sequence.includes(sorted:sortedBy:)` method with the less-than operator for the latter's second argument. diff --git a/Sources/Algorithms/Documentation.docc/Inclusion.md b/Sources/Algorithms/Documentation.docc/Inclusion.md index 2b71e273..cc8a91de 100644 --- a/Sources/Algorithms/Documentation.docc/Inclusion.md +++ b/Sources/Algorithms/Documentation.docc/Inclusion.md @@ -1,7 +1,7 @@ # Inclusion Check if one sorted sequence is completely contained in another. -Can also determine the degree overlap between two sorted sequences. +Can also determine the degree of overlap between two sorted sequences. The sort criteria is a user-supplied predicate. The predicate can be omitted to default to the less-than operator. @@ -13,3 +13,7 @@ The predicate can be omitted to default to the less-than operator. - ``Swift/Sequence/includes(sorted:)`` - ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:sortedBy:)`` - ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:)`` + +### Supporting Types + +- ``OverlapDegree`` diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index b7512770..8249def5 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -10,7 +10,60 @@ //===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===// -// MARK: Sequence.includes(sorted:sortedBy:) +// MARK: OverlapDegree +//-------------------------------------------------------------------------===// + +/// The amount of overlap between two sets. +public enum OverlapDegree: UInt, CaseIterable { + /// Both sets are empty (degenerate). + case bothEmpty + /// Have a nonempty first set, empty second (degenerate). + case onlyFirstNonempty + /// Have an empty first set, nonempty second (degenerate). + case onlySecondNonempty + /// Have two nonempty sets with no overlap. + case disjoint + /// The two sets are equivalent and nonempty. + case identical + /// The first set is a strict superset of a nonempty second. + case firstIncludesNonemptySecond + /// The first set is a nonempty strict subset of the second. + case secondIncludesNonemptyFirst + /// The sets overlap but each still have exclusive elements. + case partialOverlap +} + +extension OverlapDegree { + /// The bit mask checking if there are elements exclusive to the first set. + @usableFromInline + static var firstOnlyMask: RawValue { 1 << 0 } + /// The bit mask checking if there are elements exclusive to the second set. + @usableFromInline + static var secondOnlyMask: RawValue { 1 << 1 } + /// The bit mask checking if there are elements shared by both sets. + @usableFromInline + static var sharedMask: RawValue { 1 << 2 } +} + +extension OverlapDegree { + /// Whether there are any elements in the first set that are not in + /// the second. + @inlinable + public var hasElementsExclusiveToFirst: Bool + { rawValue & Self.firstOnlyMask != 0 } + /// Whether there are any elements in the second set that are not in + /// the first. + @inlinable + public var hasElementsExclusiveToSecond: Bool + { rawValue & Self.secondOnlyMask != 0 } + /// Whether there are any elements that occur in both sets. + @inlinable + public var hasSharedElements: Bool + { rawValue & Self.sharedMask != 0 } +} + +//===----------------------------------------------------------------------===// +// MARK: - Sequence.includes(sorted:sortedBy:) //-------------------------------------------------------------------------===// extension Sequence { @@ -41,8 +94,11 @@ extension Sequence { sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool ) rethrows -> Bool where T.Element == Element { - return try !overlap(withSorted: other, bailAfterOtherExclusive: true, - sortedBy: areInIncreasingOrder).elementsFromOther! + return try !overlap( + withSorted: other, + bailAfterOtherExclusive: true, + sortedBy: areInIncreasingOrder + ).hasElementsExclusiveToSecond } } @@ -83,12 +139,12 @@ extension Sequence { /// let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] /// let test1 = base.overlap(withSorted: [8, 7, 6, 2, 1], sortedBy: >) /// let test2 = base.overlap(withSorted: [8, 7, 5, 2, 1], sortedBy: >) - /// assert(test1.elementsFromSelf!) - /// assert(test1.sharedElements!) - /// assert(!test1.elementsFromOther!) - /// assert(test2.elementsFromSelf!) - /// assert(test2.sharedElements!) - /// assert(test2.elementsFromOther!) + /// assert(test1.hasElementsExclusiveToFirst) + /// assert(test1.hasSharedElements) + /// assert(!test1.hasElementsExclusiveToSecond) + /// assert(test2.hasElementsExclusiveToFirst!) + /// assert(test2.hasSharedElements) + /// assert(test2.hasElementsExclusiveToSecond) /// /// - Precondition: Both the receiver and `other` must be sorted according to /// `areInIncreasingOrder`, @@ -107,17 +163,15 @@ extension Sequence { /// - bailAfterOtherExclusive: Indicate that this function should abort as /// soon as one element that is exclusive to `other` is found. /// If not given, defaults to `false`. - /// - Returns: A tuple of three `Bool` members indicating whether there are - /// elements exclusive to `self`, - /// there are elements shared between the sequences, - /// and there are elements exclusive to `other`. - /// If a member is `true`, - /// then at least one element in that category exists. - /// If a member is `false`, - /// then there are no elements in that category. - /// If a member is `nil`, - /// then the function aborted before its category's status could be - /// determined. + /// - Returns: A value representing the categories of overlap found. + /// If none of the abort arguments were `true`, + /// or otherwise none of their corresponding categories were found, + /// then all of the category flags from the returned value are accurate. + /// Otherwise, + /// the returned value has exactly one of the flags in the + /// short-circuit subset as `true`, + /// and the flags outside that set may have invalid values. + /// The receiver is considered the first set, and `other` as the second. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. public func overlap( @@ -126,37 +180,33 @@ extension Sequence { bailAfterShared: Bool = false, bailAfterOtherExclusive: Bool = false, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> ( - elementsFromSelf: Bool?, - sharedElements: Bool?, - elementsFromOther: Bool? - ) + ) rethrows -> OverlapDegree where T.Element == Element { var firstElement, secondElement: Element? var iterator = makeIterator(), otherIterator = other.makeIterator() - var result: (fromSelf: Bool?, shared: Bool?, fromOther: Bool?) + var fromSelf, shared, fromOther: Bool? loop: - while result != (true, true, true) { + while (fromSelf, shared, fromOther) != (true, true, true) { firstElement = firstElement ?? iterator.next() secondElement = secondElement ?? otherIterator.next() switch (firstElement, secondElement) { case let (s?, o?) where try areInIncreasingOrder(s, o): // Exclusive to self - result.fromSelf = true + fromSelf = true guard !bailAfterSelfExclusive else { break loop } // Move to the next element in self. firstElement = nil case let (s?, o?) where try areInIncreasingOrder(o, s): // Exclusive to other - result.fromOther = true + fromOther = true guard !bailAfterOtherExclusive else { break loop } // Move to the next element in other. secondElement = nil case (_?, _?): // Shared - result.shared = true + shared = true guard !bailAfterShared else { break loop } // Iterate to the next element for both sequences. @@ -164,25 +214,29 @@ extension Sequence { secondElement = nil case (_?, nil): // Never bail, just finalize after finding an exclusive to self. - result.fromSelf = true - result.shared = result.shared ?? false - result.fromOther = result.fromOther ?? false + fromSelf = true + shared = shared ?? false + fromOther = fromOther ?? false break loop case (nil, _?): // Never bail, just finalize after finding an exclusive to other. - result.fromSelf = result.fromSelf ?? false - result.shared = result.shared ?? false - result.fromOther = true + fromSelf = fromSelf ?? false + shared = shared ?? false + fromOther = true break loop case (nil, nil): // Finalize everything instead of bailing - result.fromSelf = result.fromSelf ?? false - result.shared = result.shared ?? false - result.fromOther = result.fromOther ?? false + fromSelf = fromSelf ?? false + shared = shared ?? false + fromOther = fromOther ?? false break loop } } - return (result.fromSelf, result.shared, result.fromOther) + + let selfBit = fromSelf == true ? OverlapDegree.firstOnlyMask : 0, + shareBit = shared == true ? OverlapDegree.sharedMask : 0, + otherBit = fromOther == true ? OverlapDegree.secondOnlyMask : 0 + return .init(rawValue: selfBit | shareBit | otherBit)! } } @@ -193,12 +247,12 @@ extension Sequence where Element: Comparable { /// let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] /// let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) /// let test2 = base.overlap(withSorted: [1, 2, 5, 7, 8]) - /// assert(test1.elementsFromSelf!) - /// assert(test1.sharedElements!) - /// assert(!test1.elementsFromOther!) - /// assert(test2.elementsFromSelf!) - /// assert(test2.sharedElements!) - /// assert(test2.elementsFromOther!) + /// assert(test1.hasElementsExclusiveToFirst) + /// assert(test1.hasSharedElements) + /// assert(!test1.hasElementsExclusiveToSecond) + /// assert(test2.hasElementsExclusiveToFirst) + /// assert(test2.hasSharedElements) + /// assert(test2.hasElementsExclusiveToSecond) /// /// - Precondition: Both the receiver and `other` must be sorted. /// At least one of the involved sequences must be finite. @@ -214,17 +268,15 @@ extension Sequence where Element: Comparable { /// - bailAfterOtherExclusive: Indicate that this function should abort as /// soon as one element that is exclusive to `other` is found. /// If not given, defaults to `false`. - /// - Returns: A tuple of three `Bool` members indicating whether there are - /// elements exclusive to `self`, - /// elements shared between the sequences, - /// and elements exclusive to `other`. - /// If a member is `true`, - /// then at least one element in that category exists. - /// If a member is `false`, - /// then there are no elements in that category. - /// If a member is `nil`, - /// then the function aborted before its category's status could be - /// determined. + /// - Returns: A value representing the categories of overlap found. + /// If none of the abort arguments were `true`, + /// or otherwise none of their corresponding categories were found, + /// then all of the category flags from the returned value are accurate. + /// Otherwise, + /// the returned value has exactly one of the flags in the + /// short-circuit subset as `true`, + /// and the flags outside that set may have invalid values. + /// The receiver is considered the first set, and `other` as the second. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. @inlinable @@ -233,11 +285,7 @@ extension Sequence where Element: Comparable { bailAfterSelfExclusive: Bool = false, bailAfterShared: Bool = false, bailAfterOtherExclusive: Bool = false - ) -> ( - elementsFromSelf: Bool?, - sharedElements: Bool?, - elementsFromOther: Bool? - ) + ) -> OverlapDegree where T.Element == Element { return overlap( withSorted: other, diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift index 56fc7873..7b498207 100644 --- a/Tests/SwiftAlgorithmsTests/IncludesTests.swift +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -13,6 +13,31 @@ import XCTest import Algorithms final class IncludesTests: XCTestCase { + /// Confirm the operations for `OverlapDegree`. + func testOverlapDegree() { + XCTAssertEqualSequences( + OverlapDegree.allCases, + [ + .bothEmpty, .onlyFirstNonempty, .onlySecondNonempty, .disjoint, + .identical, .firstIncludesNonemptySecond, .secondIncludesNonemptyFirst, + .partialOverlap + ] + ) + + XCTAssertEqualSequences( + OverlapDegree.allCases.map(\.hasElementsExclusiveToFirst), + [false, true, false, true, false, true, false, true] + ) + XCTAssertEqualSequences( + OverlapDegree.allCases.map(\.hasElementsExclusiveToSecond), + [false, false, true, true, false, false, true, true] + ) + XCTAssertEqualSequences( + OverlapDegree.allCases.map(\.hasSharedElements), + [false, false, false, false, true, true, true, true] + ) + } + /// Check if one empty set includes another. func testBothSetsEmpty() { XCTAssertTrue(EmptyCollection().includes(sorted: EmptyCollection())) @@ -79,12 +104,12 @@ final class IncludesTests: XCTestCase { let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] let test1 = base.overlap(withSorted: [8, 7, 6, 2, 1], sortedBy: >) let test2 = base.overlap(withSorted: [8, 7, 5, 2, 1], sortedBy: >) - XCTAssertTrue(test1.elementsFromSelf!) - XCTAssertTrue(test1.sharedElements!) - XCTAssertFalse(test1.elementsFromOther!) - XCTAssertTrue(test2.elementsFromSelf!) - XCTAssertTrue(test2.sharedElements!) - XCTAssertTrue(test2.elementsFromOther!) + XCTAssertTrue(test1.hasElementsExclusiveToFirst) + XCTAssertTrue(test1.hasSharedElements) + XCTAssertFalse(test1.hasElementsExclusiveToSecond) + XCTAssertTrue(test2.hasElementsExclusiveToFirst) + XCTAssertTrue(test2.hasSharedElements) + XCTAssertTrue(test2.hasElementsExclusiveToSecond) } /// Confirm the example code from `Sequence.overlap(withSorted:` @@ -93,11 +118,11 @@ final class IncludesTests: XCTestCase { let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) let test2 = base.overlap(withSorted: [1, 2, 5, 7, 8]) - XCTAssertTrue(test1.elementsFromSelf!) - XCTAssertTrue(test1.sharedElements!) - XCTAssertFalse(test1.elementsFromOther!) - XCTAssertTrue(test2.elementsFromSelf!) - XCTAssertTrue(test2.sharedElements!) - XCTAssertTrue(test2.elementsFromOther!) + XCTAssertTrue(test1.hasElementsExclusiveToFirst) + XCTAssertTrue(test1.hasSharedElements) + XCTAssertFalse(test1.hasElementsExclusiveToSecond) + XCTAssertTrue(test2.hasElementsExclusiveToFirst) + XCTAssertTrue(test2.hasSharedElements) + XCTAssertTrue(test2.hasElementsExclusiveToSecond) } } From 3e18e8830989b25a3a00c3efd9204ee27dbb6db5 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Mon, 19 Aug 2024 13:54:31 -0400 Subject: [PATCH 6/9] Improve implementation Change the overlap domain flags from a triplet of Boolean to a bit-masking integer. Make some variable names longer. --- Sources/Algorithms/Includes.swift | 45 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index 8249def5..3c0dc790 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -43,6 +43,11 @@ extension OverlapDegree { /// The bit mask checking if there are elements shared by both sets. @usableFromInline static var sharedMask: RawValue { 1 << 2 } + + /// The bit mask covering all potential mask values. + @usableFromInline + static var allMask: RawValue + { firstOnlyMask | secondOnlyMask | sharedMask } } extension OverlapDegree { @@ -183,60 +188,50 @@ extension Sequence { ) rethrows -> OverlapDegree where T.Element == Element { var firstElement, secondElement: Element? - var iterator = makeIterator(), otherIterator = other.makeIterator() - var fromSelf, shared, fromOther: Bool? + var selfIterator = makeIterator(), otherIterator = other.makeIterator() + var result: OverlapDegree.RawValue = 0 loop: - while (fromSelf, shared, fromOther) != (true, true, true) { - firstElement = firstElement ?? iterator.next() + while result & OverlapDegree.allMask != OverlapDegree.allMask { + firstElement = firstElement ?? selfIterator.next() secondElement = secondElement ?? otherIterator.next() switch (firstElement, secondElement) { - case let (s?, o?) where try areInIncreasingOrder(s, o): + case let (first?, second?) where try areInIncreasingOrder(first, second): // Exclusive to self - fromSelf = true + result |= OverlapDegree.firstOnlyMask guard !bailAfterSelfExclusive else { break loop } // Move to the next element in self. firstElement = nil - case let (s?, o?) where try areInIncreasingOrder(o, s): + case let (first?, second?) where try areInIncreasingOrder(second, first): // Exclusive to other - fromOther = true + result |= OverlapDegree.secondOnlyMask guard !bailAfterOtherExclusive else { break loop } // Move to the next element in other. secondElement = nil case (_?, _?): // Shared - shared = true + result |= OverlapDegree.sharedMask guard !bailAfterShared else { break loop } // Iterate to the next element for both sequences. firstElement = nil secondElement = nil case (_?, nil): - // Never bail, just finalize after finding an exclusive to self. - fromSelf = true - shared = shared ?? false - fromOther = fromOther ?? false + // First exclusive to self after other ended + result |= OverlapDegree.firstOnlyMask break loop case (nil, _?): - // Never bail, just finalize after finding an exclusive to other. - fromSelf = fromSelf ?? false - shared = shared ?? false - fromOther = true + // First exclusive to other after self ended + result |= OverlapDegree.secondOnlyMask break loop case (nil, nil): - // Finalize everything instead of bailing - fromSelf = fromSelf ?? false - shared = shared ?? false - fromOther = fromOther ?? false + // No exclusives since both sequences stopped break loop } } - let selfBit = fromSelf == true ? OverlapDegree.firstOnlyMask : 0, - shareBit = shared == true ? OverlapDegree.sharedMask : 0, - otherBit = fromOther == true ? OverlapDegree.secondOnlyMask : 0 - return .init(rawValue: selfBit | shareBit | otherBit)! + return .init(rawValue: result)! } } From a74944a3d7d8fd5d533a49f70a65151984297111 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Wed, 21 Aug 2024 00:20:43 -0400 Subject: [PATCH 7/9] Correct documentation The changes to the return type in commit ba38c7a293f3f00af349b0de3cd8e661cef78e76 were not updated in the documentation --- Guides/Inclusion.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md index 24d60550..aa33400c 100644 --- a/Guides/Inclusion.md +++ b/Guides/Inclusion.md @@ -50,7 +50,7 @@ but only if the `Element` type conforms to `Comparable`. ```swift (1...3).includes(sorted: 1..<3) // true -(1...3).overlap(sorted: 1..<3).elementsFromOther // .some(false) +(1...3).overlap(sorted: 1..<3).hasElementsExclusiveToSecond // false ``` ## Detailed Design @@ -73,7 +73,7 @@ extension Sequence { bailAfterShared: Bool = false, bailAfterOtherExclusive: Bool = false, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool - ) rethrows -> (elementsFromSelf: Bool?, sharedElements: Bool?, elementsFromOther: Bool?) + ) rethrows -> OverlapDegree where T : Sequence, Self.Element == T.Element } From b2615e61d90488910bf09969faa4fd2b5eb259ca Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Wed, 21 Aug 2024 00:47:46 -0400 Subject: [PATCH 8/9] Change the 3 early-exit parameters to a single one Change the 3 Boolean inputs in the `overlap` functions that control early exits to one value of a custom type. Add the custom type to the tests. Update the documentation. --- Guides/Inclusion.md | 51 ++-- .../Documentation.docc/Inclusion.md | 5 +- Sources/Algorithms/Includes.swift | 222 +++++++++++++----- .../SwiftAlgorithmsTests/IncludesTests.swift | 111 ++++++++- 4 files changed, 310 insertions(+), 79 deletions(-) diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md index aa33400c..d85241fa 100644 --- a/Guides/Inclusion.md +++ b/Guides/Inclusion.md @@ -28,21 +28,23 @@ assert(!firstOverlapThird.hasElementsExclusiveToSecond) ``` By default, `overlap` returns its result after at least one of the sequences ends. -To immediately end comparisons as soon as an element for a particular category is found, -pass in the appropriate "`bail`" argument. +To immediately end comparisons as soon as an element for a particular part is found, +pass in the appropriate flags for the optional stopping-point argument. ```swift -let firstOverlapThirdAgain = first.overlap(withSorted: third, bailAfterShared: true, sortedBy: >) +let firstOverlapThirdAgain = first.overlap(withSorted: third, stoppingFor: .anythingShared, sortedBy: >) assert(firstOverlapThirdAgain.hasSharedElements) -// The other two flags may have random values. ``` -When `overlap` ends by a short-circut, -exactly one of the short-circut categories in the argument list will have its -flag in the return value set to `true`. -Otherwise all of the short-circut categories will have their flags as `false`. -Only in the latter case are the flags for categories not short-circuted would -be accurate. +When `overlap` ends by a short-circuit, +exactly one of the stopping-condition flags will be set to `true`. +To avoid checking all the element category properties, +apply the overlap's' `canSatisfy(:)` function on the stopping conditions to +check if a short-circuit happened. + +```swift +assert(firstOverlapThirdAgain.canSatisfy(.anythingShared)) +``` If a predicate is not supplied, then the less-than operator is used, @@ -69,9 +71,7 @@ extension Sequence { public func overlap( withSorted other: T, - bailAfterSelfExclusive: Bool = false, - bailAfterShared: Bool = false, - bailAfterOtherExclusive: Bool = false, + stoppingFor condition: OverlapHaltCondition = .nothing, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool ) rethrows -> OverlapDegree where T : Sequence, Self.Element == T.Element @@ -86,16 +86,14 @@ extension Sequence where Self.Element : Comparable { @inlinable public func overlap( - withSorted other: T, - bailAfterSelfExclusive: Bool = false, - bailAfterShared: Bool = false, - bailAfterOtherExclusive: Bool = false - ) -> (elementsFromSelf: Bool?, sharedElements: Bool?, elementsFromOther: Bool?) + withSorted other: T, + stoppingFor condition: OverlapHaltCondition = .nothing + ) -> OverlapDegree where T : Sequence, Self.Element == T.Element } ``` -And one type: +And two types: ```swift public enum OverlapDegree : UInt, CaseIterable { @@ -108,6 +106,21 @@ extension OverlapDegree { @inlinable public var hasElementsExclusiveToSecond: Bool { get } @inlinable public var hasSharedElements: Bool { get } } + +extension OverlapDegree { + @inlinable public func canSatisfy(_ condition: OverlapHaltCondition) -> Bool +} + +public enum OverlapHaltCondition : UInt, CaseIterable { + case nothing, anyExclusiveToFirst, anyExclusiveToSecond, anyExclusive, + anythingShared, anyFromFirst, anyFromSecond, anything +} + +extension OverlapHaltCondition { + @inlinable public var stopsOnElementsExclusiveToFirst: Bool { get } + @inlinable public var stopsOnElementsExclusiveToSecond: Bool { get } + @inlinable public var stopsOnSharedElements: Bool { get } +} ``` The `Sequence.includes(sorted:)` method calls the diff --git a/Sources/Algorithms/Documentation.docc/Inclusion.md b/Sources/Algorithms/Documentation.docc/Inclusion.md index cc8a91de..62663555 100644 --- a/Sources/Algorithms/Documentation.docc/Inclusion.md +++ b/Sources/Algorithms/Documentation.docc/Inclusion.md @@ -11,9 +11,10 @@ The predicate can be omitted to default to the less-than operator. - ``Swift/Sequence/includes(sorted:sortedBy:)`` - ``Swift/Sequence/includes(sorted:)`` -- ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:sortedBy:)`` -- ``Swift/Sequence/overlap(withSorted:bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:)`` +- ``Swift/Sequence/overlap(withSorted:stoppingFor:sortedBy:)`` +- ``Swift/Sequence/overlap(withSorted:stoppingFor:)`` ### Supporting Types - ``OverlapDegree`` +- ``OverlapHaltCondition`` diff --git a/Sources/Algorithms/Includes.swift b/Sources/Algorithms/Includes.swift index 3c0dc790..ada2d20d 100644 --- a/Sources/Algorithms/Includes.swift +++ b/Sources/Algorithms/Includes.swift @@ -67,6 +67,82 @@ extension OverlapDegree { { rawValue & Self.sharedMask != 0 } } +extension OverlapDegree { + /// Using the given description for which parts of a set operation result need + /// to be detected, + /// return whether this degree covers at least one of those parts. + /// + /// A set operation result part is considered to exist if the result has at + /// least one element that can qualify to be within that part. + /// This means that a degree of `.bothEmpty` never matches any part detection, + /// and that a detection request of `.nothing` can never be found. + /// + /// - Parameter condition: The parts of a set operation result whose + /// existence needs to be tested for. + /// - Returns: Whether this degree includes at least one + /// set operation result part that can match the `condition`. + /// + /// - Complexity: O(1). + @inlinable + public func canSatisfy(_ condition: OverlapHaltCondition) -> Bool { + return rawValue & condition.rawValue != 0 + } +} + +//===----------------------------------------------------------------------===// +// MARK: - OverlapHaltCondition +//-------------------------------------------------------------------------===// + +/// The condition when determining overlap should stop early. +public enum OverlapHaltCondition: UInt, CaseIterable { + /// Never stop reading elements if necessary. + case nothing + /// Stop when an element exclusive to the first set is found. + case anyExclusiveToFirst + /// Stop when an element exclusive to the second set is found. + case anyExclusiveToSecond + /// Stop when finding an element from exactly one set. + case anyExclusive + /// Stop when finding an element present in both sets. + case anythingShared + /// Stop when an element from the first set is found. + case anyFromFirst + /// Stop when an element from the second set is found. + case anyFromSecond + /// Stop on the first element found. + case anything +} + +extension OverlapHaltCondition { + /// The bit mask checking if analysis stops at the first element exclusive to + /// the first set. + @usableFromInline + static var firstOnlyMask: RawValue { 1 << 0 } + /// The bit mask checking if analysis stops at the first element exclusive to + /// the second set. + @usableFromInline + static var secondOnlyMask: RawValue { 1 << 1 } + /// The bit mask checking if analysis stops at the first element shared by + /// both sets. + @usableFromInline + static var sharedMask: RawValue { 1 << 2 } +} + +extension OverlapHaltCondition { + /// Whether analysis can stop on finding an element exclusive to the first set. + @inlinable + public var stopsOnElementsExclusiveToFirst: Bool + { rawValue & Self.firstOnlyMask != 0 } + /// Whether analysis can stop on finding an element exclusive to the second set. + @inlinable + public var stopsOnElementsExclusiveToSecond: Bool + { rawValue & Self.secondOnlyMask != 0 } + /// Whether analysis can stop on finding an element shared by both sets. + @inlinable + public var stopsOnSharedElements: Bool + { rawValue & Self.sharedMask != 0 } +} + //===----------------------------------------------------------------------===// // MARK: - Sequence.includes(sorted:sortedBy:) //-------------------------------------------------------------------------===// @@ -101,7 +177,7 @@ extension Sequence { where T.Element == Element { return try !overlap( withSorted: other, - bailAfterOtherExclusive: true, + stoppingFor: .anyExclusiveToSecond, sortedBy: areInIncreasingOrder ).hasElementsExclusiveToSecond } @@ -133,7 +209,7 @@ extension Sequence where Element: Comparable { } //===----------------------------------------------------------------------===// -// MARK: - Sequence.overlap(withSorted:sortedBy:) +// MARK: - Sequence.overlap(withSorted:stoppingFor:sortedBy:) //-------------------------------------------------------------------------===// extension Sequence { @@ -151,6 +227,36 @@ extension Sequence { /// assert(test2.hasSharedElements) /// assert(test2.hasElementsExclusiveToSecond) /// + /// When only the existence of specific kinds of overlap needs to be checked, + /// an extra argument can be supplied to stop reading the sequences as + /// soon as one confirmation has been found. + /// + /// let test3 = base.overlap(withSorted: [8, 7, 5, 2, 1], + /// stoppingFor: .anythingShared, sortedBy: >) + /// assert(test3.hasSharedElements) + /// + /// As soon as the value `8` is read from both `base` and the argument, + /// a shared element has been detected, + /// so the call ends early. + /// With early returns, + /// at most one of the searched-for overlap properties will be `true`; + /// all others will be `false`, + /// since the call ended before any other criteria could be checked. + /// The status of overlap properties outside of the search set are not + /// reliable to check. + /// For this past example, only the `hasSharedElements` property is + /// guaranteed to supply a valid value. + /// + /// Since triggering an early-end condition sets exactly one of the + /// return value's flags among the potentially multiple ones that could + /// match the condition, + /// calling the return value's `canSatisfy(:)` function may be shorter than + /// checking each potential flag individually. + /// + /// For both the return value and any possible early-end conditions, + /// the receiver is considered the first sequence and `other` is + /// considered the second sequence. + /// /// - Precondition: Both the receiver and `other` must be sorted according to /// `areInIncreasingOrder`, /// which must be a strict weak ordering over its arguments. @@ -158,32 +264,24 @@ extension Sequence { /// /// - Parameters: /// - other: The sequence that is compared against the receiver. + /// - condition: A specification of set operation result parts that will end + /// this call early if found. + /// If not given, + /// defaults to `.nothing`. /// - areInIncreasingOrder: The sorting criteria. - /// - bailAfterSelfExclusive: Indicate that this function should abort as - /// soon as one element that is exclusive to this sequence is found. - /// If not given, defaults to `false`. - /// - bailAfterShared: Indicate that this function should abort as soon as - /// an element that both sequences share is found. - /// If not given, defaults to `false`. - /// - bailAfterOtherExclusive: Indicate that this function should abort as - /// soon as one element that is exclusive to `other` is found. - /// If not given, defaults to `false`. - /// - Returns: A value representing the categories of overlap found. - /// If none of the abort arguments were `true`, - /// or otherwise none of their corresponding categories were found, - /// then all of the category flags from the returned value are accurate. - /// Otherwise, - /// the returned value has exactly one of the flags in the - /// short-circuit subset as `true`, - /// and the flags outside that set may have invalid values. - /// The receiver is considered the first set, and `other` as the second. + /// - Returns: The set operation result parts that would be present if + /// these sequence operands were merged ino a single sorted sequence and + /// all the sequences were treated as sets. + /// If at least one of the parts is in the `condition` filter, + /// this function call will end early, + /// and the return value may be a proper subset of the actual result. + /// Call `.canSatisfy(condition)` function on the returned value to check if + /// an early finish happened. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. public func overlap( withSorted other: T, - bailAfterSelfExclusive: Bool = false, - bailAfterShared: Bool = false, - bailAfterOtherExclusive: Bool = false, + stoppingFor condition: OverlapHaltCondition = .nothing, sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool ) rethrows -> OverlapDegree where T.Element == Element { @@ -198,21 +296,21 @@ extension Sequence { case let (first?, second?) where try areInIncreasingOrder(first, second): // Exclusive to self result |= OverlapDegree.firstOnlyMask - guard !bailAfterSelfExclusive else { break loop } + guard !condition.stopsOnElementsExclusiveToFirst else { break loop } // Move to the next element in self. firstElement = nil case let (first?, second?) where try areInIncreasingOrder(second, first): // Exclusive to other result |= OverlapDegree.secondOnlyMask - guard !bailAfterOtherExclusive else { break loop } + guard !condition.stopsOnElementsExclusiveToSecond else { break loop } // Move to the next element in other. secondElement = nil case (_?, _?): // Shared result |= OverlapDegree.sharedMask - guard !bailAfterShared else { break loop } + guard !condition.stopsOnSharedElements else { break loop } // Iterate to the next element for both sequences. firstElement = nil @@ -249,45 +347,61 @@ extension Sequence where Element: Comparable { /// assert(test2.hasSharedElements) /// assert(test2.hasElementsExclusiveToSecond) /// + /// When only the existence of specific kinds of overlap needs to be checked, + /// an extra argument can be supplied to stop reading the sequences as + /// soon as one confirmation has been found. + /// + /// let test3 = base.overlap(withSorted: [-1, 1, 2, 4, 7, 8], + /// stoppingFor: .anythingShared) + /// assert(test3.hasSharedElements) + /// + /// As soon as the value `1` is read from both `base` and the argument, + /// a shared element has been detected, + /// so the call ends early. + /// With early returns, + /// at most one of the searched-for overlap properties will be `true`; + /// all others will be `false`, + /// since the call ended before any other criteria could be checked. + /// The status of overlap properties outside of the search set are not + /// reliable to check. + /// For this past example, only the `hasSharedElements` property is + /// guaranteed to supply a valid value. + /// + /// Since triggering an early-end condition sets exactly one of the + /// return value's flags among the potentially multiple ones that could + /// match the condition, + /// calling the return value's `canSatisfy(:)` function may be shorter than + /// checking each potential flag individually. + /// + /// For both the return value and any possible early-end conditions, + /// the receiver is considered the first sequence and `other` is + /// considered the second sequence. + /// /// - Precondition: Both the receiver and `other` must be sorted. /// At least one of the involved sequences must be finite. /// /// - Parameters: /// - other: The sequence that is compared against the receiver. - /// - bailAfterSelfExclusive: Indicate that this function should abort as - /// soon as one element that is exclusive to this sequence is found. - /// If not given, defaults to `false`. - /// - bailAfterShared: Indicate that this function should abort as soon as - /// an element that both sequences share is found. - /// If not given, defaults to `false`. - /// - bailAfterOtherExclusive: Indicate that this function should abort as - /// soon as one element that is exclusive to `other` is found. - /// If not given, defaults to `false`. - /// - Returns: A value representing the categories of overlap found. - /// If none of the abort arguments were `true`, - /// or otherwise none of their corresponding categories were found, - /// then all of the category flags from the returned value are accurate. - /// Otherwise, - /// the returned value has exactly one of the flags in the - /// short-circuit subset as `true`, - /// and the flags outside that set may have invalid values. - /// The receiver is considered the first set, and `other` as the second. + /// - condition: A specification of set operation result parts that will end + /// this call early if found. + /// If not given, + /// defaults to `.nothing`. + /// - Returns: The set operation result parts that would be present if + /// these sequence operands were merged ino a single sorted sequence and + /// all the sequences were treated as sets. + /// If at least one of the parts is in the `condition` filter, + /// this function call will end early, + /// and the return value may be a proper subset of the actual result. + /// Call `.canSatisfy(condition)` function on the returned value to check if + /// an early finish happened. /// /// - Complexity: O(*n*), where `n` is the length of the shorter sequence. @inlinable public func overlap( withSorted other: T, - bailAfterSelfExclusive: Bool = false, - bailAfterShared: Bool = false, - bailAfterOtherExclusive: Bool = false + stoppingFor condition: OverlapHaltCondition = .nothing ) -> OverlapDegree where T.Element == Element { - return overlap( - withSorted: other, - bailAfterSelfExclusive: bailAfterSelfExclusive, - bailAfterShared: bailAfterShared, - bailAfterOtherExclusive: bailAfterOtherExclusive, - sortedBy: < - ) + return overlap(withSorted: other, stoppingFor: condition, sortedBy: <) } } diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift index 7b498207..8fe19a0f 100644 --- a/Tests/SwiftAlgorithmsTests/IncludesTests.swift +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -38,6 +38,28 @@ final class IncludesTests: XCTestCase { ) } + /// Confirm the operations for `OverlapHaltCondition`. + func testOverlapHaltCondition() { + XCTAssertEqualSequences( + OverlapHaltCondition.allCases, + [.nothing, .anyExclusiveToFirst, .anyExclusiveToSecond, .anyExclusive, + .anythingShared, .anyFromFirst, .anyFromSecond, .anything] + ) + + XCTAssertEqualSequences( + OverlapHaltCondition.allCases.map(\.stopsOnElementsExclusiveToFirst), + [false, true, false, true, false, true, false, true] + ) + XCTAssertEqualSequences( + OverlapHaltCondition.allCases.map(\.stopsOnElementsExclusiveToSecond), + [false, false, true, true, false, false, true, true] + ) + XCTAssertEqualSequences( + OverlapHaltCondition.allCases.map(\.stopsOnSharedElements), + [false, false, false, false, true, true, true, true] + ) + } + /// Check if one empty set includes another. func testBothSetsEmpty() { XCTAssertTrue(EmptyCollection().includes(sorted: EmptyCollection())) @@ -83,6 +105,81 @@ final class IncludesTests: XCTestCase { XCTAssertFalse([0, 1, 2, 3].includes(sorted: -1..<2)) } + /// Check the comprehensive tests for short-circuit matching. + func testCanSatisfy() { + XCTAssertEqualSequences( + product(OverlapDegree.allCases, OverlapHaltCondition.allCases).map { + $0.0.canSatisfy($0.1) + }, + [ + false, // bothEmpty, nothing + false, // bothEmpty, anyExclusiveToFirst + false, // bothEmpty, anyExclusiveToSecond + false, // bothEmpty, anyExclusive + false, // bothEmpty, anythingShared + false, // bothEmpty, anyFromFirst + false, // bothEmpty, anyFromSecond + false, // bothEmpty, anything + false, // onlyFirstNonempty, nothing + true , // onlyFirstNonempty, anyExclusiveToFirst + false, // onlyFirstNonempty, anyExclusiveToSecond + true , // onlyFirstNonempty, anyExclusive + false, // onlyFirstNonempty, anythingShared + true , // onlyFirstNonempty, anyFromFirst + false, // onlyFirstNonempty, anyFromSecond + true , // onlyFirstNonempty, anything + false, // onlySecondNonempty, nothing + false, // onlySecondNonempty, anyExclusiveToFirst + true , // onlySecondNonempty, anyExclusiveToSecond + true , // onlySecondNonempty, anyExclusive + false, // onlySecondNonempty, anythingShared + false, // onlySecondNonempty, anyFromFirst + true , // onlySecondNonempty, anyFromSecond + true , // onlySecondNonempty, anything + false, // disjoint, nothing + true , // disjoint, anyExclusiveToFirst + true , // disjoint, anyExclusiveToSecond + true , // disjoint, anyExclusive + false, // disjoint, anythingShared + true , // disjoint, anyFromFirst + true , // disjoint, anyFromSecond + true , // disjoint, anything + false, // identical, nothing + false, // identical, anyExclusiveToFirst + false, // identical, anyExclusiveToSecond + false, // identical, anyExclusive + true , // identical, anythingShared + true , // identical, anyFromFirst + true , // identical, anyFromSecond + true , // identical, anything + false, // firstIncludesNonemptySecond, nothing + true , // firstIncludesNonemptySecond, anyExclusiveToFirst + false, // firstIncludesNonemptySecond, anyExclusiveToSecond + true , // firstIncludesNonemptySecond, anyExclusive + true , // firstIncludesNonemptySecond, anythingShared + true , // firstIncludesNonemptySecond, anyFromFirst + true , // firstIncludesNonemptySecond, anyFromSecond + true , // firstIncludesNonemptySecond, anything + false, // secondIncludesNonemptyFirst, nothing + false, // secondIncludesNonemptyFirst, anyExclusiveToFirst + true , // secondIncludesNonemptyFirst, anyExclusiveToSecond + true , // secondIncludesNonemptyFirst, anyExclusive + true , // secondIncludesNonemptyFirst, anythingShared + true , // secondIncludesNonemptyFirst, anyFromFirst + true , // secondIncludesNonemptyFirst, anyFromSecond + true , // secondIncludesNonemptyFirst, anything + false, // partialOverlap, nothing + true , // partialOverlap, anyExclusiveToFirst + true , // partialOverlap, anyExclusiveToSecond + true , // partialOverlap, anyExclusive + true , // partialOverlap, anythingShared + true , // partialOverlap, anyFromFirst + true , // partialOverlap, anyFromSecond + true , // partialOverlap, anything + ] + ) + } + /// Confirm the example code from `Sequence.includes(sorted:sortedBy:)`. func testFirstDiscussionCode() { let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] @@ -97,8 +194,7 @@ final class IncludesTests: XCTestCase { XCTAssertFalse(base.includes(sorted: [1, 2, 5, 7, 8])) } - /// Confirm the example code from `Sequence.overlap(withSorted:` - /// `bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:` + /// Confirm the example code from `Sequence.overlap(withSorted:stoppingFor:` /// `sortedBy:)`. func testThirdDiscussionCode() { let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] @@ -110,10 +206,13 @@ final class IncludesTests: XCTestCase { XCTAssertTrue(test2.hasElementsExclusiveToFirst) XCTAssertTrue(test2.hasSharedElements) XCTAssertTrue(test2.hasElementsExclusiveToSecond) + + let test3 = base.overlap(withSorted: [8, 7, 4, 2, 1], + stoppingFor: .anythingShared, sortedBy: >) + XCTAssertTrue(test3.hasSharedElements) } - /// Confirm the example code from `Sequence.overlap(withSorted:` - /// `bailAfterSelfExclusive:bailAfterShared:bailAfterOtherExclusive:)`. + /// Confirm the example code from `Sequence.overlap(withSorted:stoppingFor:)`. func testFourthDiscussionCode() { let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) @@ -124,5 +223,9 @@ final class IncludesTests: XCTestCase { XCTAssertTrue(test2.hasElementsExclusiveToFirst) XCTAssertTrue(test2.hasSharedElements) XCTAssertTrue(test2.hasElementsExclusiveToSecond) + + let test3 = base.overlap(withSorted: [-1, 1, 2, 4, 7, 8], + stoppingFor: .anythingShared) + XCTAssertTrue(test3.hasSharedElements) } } From f8f4e3c69a4b6c0fcc96f3b9abc8e95d6e1be8a3 Mon Sep 17 00:00:00 2001 From: Daryle Walker Date: Wed, 21 Aug 2024 00:51:17 -0400 Subject: [PATCH 9/9] Improve names of some tests --- Tests/SwiftAlgorithmsTests/IncludesTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift index 8fe19a0f..72e22038 100644 --- a/Tests/SwiftAlgorithmsTests/IncludesTests.swift +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -181,14 +181,14 @@ final class IncludesTests: XCTestCase { } /// Confirm the example code from `Sequence.includes(sorted:sortedBy:)`. - func testFirstDiscussionCode() { + func testIncludesWithCustomPredicate() { let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] XCTAssertTrue(base.includes(sorted: [8, 7, 6, 2, 1], sortedBy: >)) XCTAssertFalse(base.includes(sorted: [8, 7, 5, 2, 1], sortedBy: >)) } /// Confirm the example code from `Sequence.includes(sorted:)`. - func testSecondDiscussionCode() { + func testIncludesWithComparable() { let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] XCTAssertTrue(base.includes(sorted: [1, 2, 6, 7, 8])) XCTAssertFalse(base.includes(sorted: [1, 2, 5, 7, 8])) @@ -196,7 +196,7 @@ final class IncludesTests: XCTestCase { /// Confirm the example code from `Sequence.overlap(withSorted:stoppingFor:` /// `sortedBy:)`. - func testThirdDiscussionCode() { + func testOverlapWithCustomPredicate() { let base = [9, 8, 7, 6, 6, 3, 2, 1, 0] let test1 = base.overlap(withSorted: [8, 7, 6, 2, 1], sortedBy: >) let test2 = base.overlap(withSorted: [8, 7, 5, 2, 1], sortedBy: >) @@ -213,7 +213,7 @@ final class IncludesTests: XCTestCase { } /// Confirm the example code from `Sequence.overlap(withSorted:stoppingFor:)`. - func testFourthDiscussionCode() { + func testOverlapWithComparable() { let base = [0, 1, 2, 3, 6, 6, 7, 8, 9] let test1 = base.overlap(withSorted: [1, 2, 6, 7, 8]) let test2 = base.overlap(withSorted: [1, 2, 5, 7, 8])