diff --git a/Guides/Inclusion.md b/Guides/Inclusion.md new file mode 100644 index 00000000..d85241fa --- /dev/null +++ b/Guides/Inclusion.md @@ -0,0 +1,150 @@ +# 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 with the `includes` function. +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 +``` + +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.hasElementsExclusiveToFirst) +assert(firstOverlapThird.hasSharedElements) +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 part is found, +pass in the appropriate flags for the optional stopping-point argument. + +```swift +let firstOverlapThirdAgain = first.overlap(withSorted: third, stoppingFor: .anythingShared, sortedBy: >) +assert(firstOverlapThirdAgain.hasSharedElements) +``` + +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, +but only if the `Element` type conforms to `Comparable`. + +```swift +(1...3).includes(sorted: 1..<3) // true +(1...3).overlap(sorted: 1..<3).hasElementsExclusiveToSecond // false +``` + +## Detailed Design + +Four new methods are added to `Sequence`: + +```swift +extension Sequence { + @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, + stoppingFor condition: OverlapHaltCondition = .nothing, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> OverlapDegree + 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 + + @inlinable public func + overlap( + withSorted other: T, + stoppingFor condition: OverlapHaltCondition = .nothing + ) -> OverlapDegree + where T : Sequence, Self.Element == T.Element +} +``` + +And two types: + +```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 } +} + +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 +`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 any of these methods 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..62663555 --- /dev/null +++ b/Sources/Algorithms/Documentation.docc/Inclusion.md @@ -0,0 +1,20 @@ +# Inclusion + +Check if one sorted sequence is completely contained in another. +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. + +## Topics + +### Inclusion + +- ``Swift/Sequence/includes(sorted:sortedBy:)`` +- ``Swift/Sequence/includes(sorted:)`` +- ``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 new file mode 100644 index 00000000..ada2d20d --- /dev/null +++ b/Sources/Algorithms/Includes.swift @@ -0,0 +1,407 @@ +//===----------------------------------------------------------------------===// +// +// 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: 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 } + + /// The bit mask covering all potential mask values. + @usableFromInline + static var allMask: RawValue + { firstOnlyMask | secondOnlyMask | sharedMask } +} + +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 } +} + +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:) +//-------------------------------------------------------------------------===// + +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. + /// + /// 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. + /// + /// - 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. + @inlinable + public func includes( + sorted other: T, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Bool + where T.Element == Element { + return try !overlap( + withSorted: other, + stoppingFor: .anyExclusiveToSecond, + sortedBy: areInIncreasingOrder + ).hasElementsExclusiveToSecond + } +} + +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:stoppingFor: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.hasElementsExclusiveToFirst) + /// assert(test1.hasSharedElements) + /// assert(!test1.hasElementsExclusiveToSecond) + /// assert(test2.hasElementsExclusiveToFirst!) + /// 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. + /// Either the receiver, `other`, or both must be finite. + /// + /// - 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. + /// - 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, + stoppingFor condition: OverlapHaltCondition = .nothing, + sortedBy areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> OverlapDegree + where T.Element == Element { + var firstElement, secondElement: Element? + var selfIterator = makeIterator(), otherIterator = other.makeIterator() + var result: OverlapDegree.RawValue = 0 + loop: + while result & OverlapDegree.allMask != OverlapDegree.allMask { + firstElement = firstElement ?? selfIterator.next() + secondElement = secondElement ?? otherIterator.next() + switch (firstElement, secondElement) { + case let (first?, second?) where try areInIncreasingOrder(first, second): + // Exclusive to self + result |= OverlapDegree.firstOnlyMask + 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 !condition.stopsOnElementsExclusiveToSecond else { break loop } + + // Move to the next element in other. + secondElement = nil + case (_?, _?): + // Shared + result |= OverlapDegree.sharedMask + guard !condition.stopsOnSharedElements else { break loop } + + // Iterate to the next element for both sequences. + firstElement = nil + secondElement = nil + case (_?, nil): + // First exclusive to self after other ended + result |= OverlapDegree.firstOnlyMask + break loop + case (nil, _?): + // First exclusive to other after self ended + result |= OverlapDegree.secondOnlyMask + break loop + case (nil, nil): + // No exclusives since both sequences stopped + break loop + } + } + + return .init(rawValue: result)! + } +} + +extension Sequence where Element: Comparable { + /// Assuming that this sequence and the given sequence are sorted, + /// check if the sequences have overlap and/or exclusive elements. + /// + /// 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.hasElementsExclusiveToFirst) + /// assert(test1.hasSharedElements) + /// assert(!test1.hasElementsExclusiveToSecond) + /// assert(test2.hasElementsExclusiveToFirst) + /// 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. + /// - 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, + stoppingFor condition: OverlapHaltCondition = .nothing + ) -> OverlapDegree + where T.Element == Element { + return overlap(withSorted: other, stoppingFor: condition, sortedBy: <) + } +} diff --git a/Tests/SwiftAlgorithmsTests/IncludesTests.swift b/Tests/SwiftAlgorithmsTests/IncludesTests.swift new file mode 100644 index 00000000..72e22038 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/IncludesTests.swift @@ -0,0 +1,231 @@ +//===----------------------------------------------------------------------===// +// +// 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 { + /// 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] + ) + } + + /// 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())) + } + + /// 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)) + } + + /// 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 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 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])) + } + + /// Confirm the example code from `Sequence.overlap(withSorted:stoppingFor:` + /// `sortedBy:)`. + 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: >) + XCTAssertTrue(test1.hasElementsExclusiveToFirst) + XCTAssertTrue(test1.hasSharedElements) + XCTAssertFalse(test1.hasElementsExclusiveToSecond) + 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:stoppingFor:)`. + 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]) + XCTAssertTrue(test1.hasElementsExclusiveToFirst) + XCTAssertTrue(test1.hasSharedElements) + XCTAssertFalse(test1.hasElementsExclusiveToSecond) + 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) + } +}