Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detection of How One Sorted Sequence Includes Another #38

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ This project follows semantic versioning.

## [Unreleased]

*No new changes.*
### Additions

- The `degreeOfInclusion(with:by:)` and `degreeOfInclusion(with:)` methods have
been added. They report how much overlap two sorted sequences have, expressed
by the `SetInclusion` type. ([#38])

---

Expand Down Expand Up @@ -288,6 +292,7 @@ This changelog's format is based on [Keep a Changelog](https://keepachangelog.co
[#24]: https://github.com/apple/swift-algorithms/pull/24
[#31]: https://github.com/apple/swift-algorithms/pull/31
[#35]: https://github.com/apple/swift-algorithms/pull/35
[#38]: https://github.com/apple/swift-algorithms/pull/38
[#46]: https://github.com/apple/swift-algorithms/pull/46
[#51]: https://github.com/apple/swift-algorithms/pull/51
[#54]: https://github.com/apple/swift-algorithms/pull/54
Expand Down
77 changes: 77 additions & 0 deletions Guides/DegreeOfInclusion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Sorted Sequence Inclusion

[[Source](../Sources/Algorithms/DegreeOfInclusion.swift) |
[Tests](../Tests/SwiftAlgorithmsTests/DegreeOfInclusionTests.swift)]

Methods to find how much two sorted sequences overlap.

```swift
if (1...7).degreeOfInclusion(with: [1, 5, 6]).doesFirstIncludeSecond {
print("The range is a superset of the array.")
}
```

The result is an enumeration detailing the precise containment relationship, if
any. If the result was `Bool`, then the method would need to be called again
(with swapped arguments) to confirm the actual inclusion degree.

## Detailed Design

The inclusion-detection methods are declared as extensions to `Sequence`. The
overload that defaults comparisons to the standard less-than operator is
constrained to when the `Element` type conforms to `Comparable`.

A reported inclusion state is expressed with the `SetInclusion` type. This state
is based on the existence of elements that are shared, exclusive to the first
sequence, and exclusive to the second sequence. This includes all the
degenerate combinations, which are the ones where at least one source is empty.
Use the convenience properties `doesFirstIncludeSecond` and
`doesSecondIncludeFirst` (and `areIdentical`) to actually check if one source is
a superset of the other.

```swift
enum SetInclusion {
case bothUninhabited, onlyFirstInhabited, onlySecondInhabited,
dualExclusivesOnly, sharedOnly, firstExtendsSecond,
secondExtendsFirst, dualExclusivesAndShared
}

extension SetInclusion {
var hasExclusivesToFirst: Bool { get }
var hasExclusivesToSecond: Bool { get }
var hasSharedElements: Bool { get }
var areIdentical: Bool { get }
var doesFirstIncludeSecond: Bool { get }
var doesSecondIncludeFirst: Bool { get }
}

extension Sequence {
func degreeOfInclusion<S: Sequence>(
with other: S,
by areInIncreasingOrder: (Element, Element) throws -> Bool
) rethrows -> SetInclusion where S.Element == Element
}

extension Sequence where Element: Comparable {
func degreeOfInclusion<S: Sequence>(
with other: S
) -> SetInclusion where S.Element == Element
}
```

### Complexity

All of these methods have to walk the entirety of both sources, so they work in
O(_n_) operations, where _n_ is the length of the shorter source.

### Comparison with other languages

**C++:** The `<algorithm>` library defines the `includes` function, whose
functionality is part of the semantics of `degreeOfInclusion`. The `includes`
function only detects of the second sequence is included within the first; it
doesn't notify if the inclusion is degenerate, or if inclusion fails because
it's actually reversed, both of which `degreeOfInclusion` can do. To get the
direct functionality of `includes`, check the `doesFirstIncludeSecond` property
of the return value from `degreeOfInclusion`.

(To-do: add other languages.)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Read more about the package, and the intent behind it, in the [announcement on s
- [`reductions(_:)`, `reductions(_:_:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Reductions.md): Returns all the intermediate states of reducing the elements of a sequence or collection.
- [`split(maxSplits:omittingEmptySubsequences:whereSeparator)`, `split(separator:maxSplits:omittingEmptySubsequences)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Split.md): Lazy versions of the Standard Library's eager operations that split sequences and collections into subsequences separated by the specified separator element.
- [`windows(ofCount:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Windows.md): Breaks a collection into overlapping subsequences where elements are slices from the original collection.
- [`degreeOfInclusion(with:by:)`, `degreeOfInclusion(with:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/DegreeOfInclusion.md): Reports the degree two sorted sequences overlap.

## Adding Swift Algorithms as a Dependency

Expand Down
142 changes: 142 additions & 0 deletions Sources/Algorithms/DegreeOfInclusion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Algorithms open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

/// The manner two (multi-)sets may overlap, including degenerate cases.
public enum SetInclusion: UInt, CaseIterable {
/// Neither source had any elements.
case bothUninhabited
/// Only the first source had any elements.
case onlyFirstInhabited
/// Only the second source had any elements.
case onlySecondInhabited
/// Each source has its own elements, without any shared.
case dualExclusivesOnly
/// Each source has elements, all of them shared.
case sharedOnly
/// The second source has elements, but the first has those and some more.
case firstExtendsSecond
/// The first source has elements, but the second has those and some more.
case secondExtendsFirst
/// Each source has exclusive elements, and there are some shared ones.
case dualExclusivesAndShared
}

extension SetInclusion {
/// Whether there are elements exclusive to the first source.
@inlinable public var hasExclusivesToFirst: Bool { rawValue & 0x01 != 0 }
/// Whether there are elements exclusive to the second source.
@inlinable public var hasExclusivesToSecond: Bool { rawValue & 0x02 != 0 }
/// Whether there are elements shared by both sources.
@inlinable public var hasSharedElements: Bool { rawValue & 0x04 != 0 }

/// Whether the sources are identical.
@inlinable public var areIdentical: Bool { rawValue & 0x03 == 0 }
/// Whether the first source contains everything from the second.
@inlinable public var doesFirstIncludeSecond: Bool { !hasExclusivesToSecond }
/// Whether the second source contains everything from the first.
@inlinable public var doesSecondIncludeFirst: Bool { !hasExclusivesToFirst }
}

//===----------------------------------------------------------------------===//
// degreeOfInclusion(with:by:)
//===----------------------------------------------------------------------===//

extension Sequence {
/// Returns how this sequence and the given sequence overlap, assuming both
/// are sorted according to the given predicate that can compare elements.
///
/// The predicate must be a *strict weak ordering* over the elements. That
/// is, for any elements `a`, `b`, and `c`, the following conditions must
/// hold:
///
/// - `areInIncreasingOrder(a, a)` is always `false`. (Irreflexivity)
/// - If `areInIncreasingOrder(a, b)` and `areInIncreasingOrder(b, c)` are
/// both `true`, then `areInIncreasingOrder(a, c)` is also
/// `true`. (Transitive comparability)
/// - Two elements are *incomparable* if neither is ordered before the other
/// according to the predicate. If `a` and `b` are incomparable, and `b`
/// and `c` are incomparable, then `a` and `c` are also incomparable.
/// (Transitive incomparability)
///
/// - Precondition: Both the receiver and `other` are sorted according to
/// `areInIncreasingOrder`; and both should be finite.
///
/// - Parameters:
/// - other: A sequence to compare to this sequence.
/// - areInIncreasingOrder: A predicate that returns `true` if its first
/// argument should be ordered before its second argument; otherwise,
/// `false`.
/// - Returns: The degree of inclusion between the sequences. The receiver is
/// considered the first source, and `other` second.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of the
/// sequence and the length of `other`.
public func degreeOfInclusion<S: Sequence>(
with other: S,
by areInIncreasingOrder: (Element, Element) throws -> Bool
) rethrows -> SetInclusion where S.Element == Element {
var rawResult: UInt = 0, cache, otherCache: Element?, isDone = false
var iterator = makeIterator(), otherIterator = other.makeIterator()
while !isDone {
cache = cache ?? iterator.next()
otherCache = otherCache ?? otherIterator.next()
switch (cache, otherCache) {
case (nil, nil):
isDone = true
case (_?, nil):
rawResult |= 0x01
isDone = true
case (nil, _?):
rawResult |= 0x02
isDone = true
case let (first?, second?):
if try areInIncreasingOrder(first, second) {
rawResult |= 0x01
cache = nil
} else if try areInIncreasingOrder(second, first) {
rawResult |= 0x02
otherCache = nil
} else {
rawResult |= 0x04
cache = nil
otherCache = nil
}
isDone = rawResult == 0x07
}
}
return SetInclusion(rawValue: rawResult)!
}
}

//===----------------------------------------------------------------------===//
// degreeOfInclusion(with:)
//===----------------------------------------------------------------------===//

extension Sequence where Element: Comparable {
/// Returns how this sequence and the given sequence overlap, assuming both
/// are sorted.
///
/// - Precondition: Both the receiver and `other` are sorted; and both should
/// be finite.
///
/// - Parameters:
/// - other: A sequence to compare to this sequence.
/// - Returns: The degree of inclusion between the sequences. The receiver is
/// considered the first source, and `other` second.
///
/// - Complexity: O(*m*), where *m* is the lesser of the length of the
/// sequence and the length of `other`.
@inlinable
public func degreeOfInclusion<S: Sequence>(with other: S) -> SetInclusion
where S.Element == Element {
return degreeOfInclusion(with: other, by: <)
}
}
105 changes: 105 additions & 0 deletions Tests/SwiftAlgorithmsTests/DegreeOfInclusion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Algorithms open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//

import XCTest
import Algorithms

/// Unit tests for the `sortedOverlap` method and `SetInclusion` type.
final class SortedInclusionTests: XCTestCase {
/// Check the `SetInclusion` type's properties.
func testInclusion() {
XCTAssertEqualSequences(SetInclusion.allCases, [
.bothUninhabited, .onlyFirstInhabited, .onlySecondInhabited,
.dualExclusivesOnly, .sharedOnly, .firstExtendsSecond,
.secondExtendsFirst, .dualExclusivesAndShared
])

XCTAssertEqualSequences(SetInclusion.allCases.map(\.hasExclusivesToFirst), [
false, true, false, true, false, true, false, true
])
XCTAssertEqualSequences(SetInclusion.allCases.map(\.hasExclusivesToSecond), [
false, false, true, true, false, false, true, true
])
XCTAssertEqualSequences(SetInclusion.allCases.map(\.hasSharedElements), [
false, false, false, false, true, true, true, true
])

XCTAssertEqualSequences(SetInclusion.allCases.map(\.areIdentical), [
true, false, false, false, true, false, false, false
])
XCTAssertEqualSequences(SetInclusion.allCases.map(\.doesFirstIncludeSecond), [
true, true, false, false, true, true, false, false
])
XCTAssertEqualSequences(SetInclusion.allCases.map(\.doesSecondIncludeFirst), [
true, false, true, false, true, false, true, false
])
}

/// Check when both sources are empty.
func testEmpty() {
let empty = EmptyCollection<Int>()
XCTAssertEqual(empty.degreeOfInclusion(with: empty), .bothUninhabited)
}

/// Check when exactly one source is empty.
func testOnlyOneEmpty() {
let empty = EmptyCollection<Int>(), single = CollectionOfOne(1)
XCTAssertEqual(single.degreeOfInclusion(with: empty), .onlyFirstInhabited)
XCTAssertEqual(empty.degreeOfInclusion(with: single), .onlySecondInhabited)
}

/// Check when there are no common elements.
func testDisjoint() {
let one = CollectionOfOne(1), two = CollectionOfOne(2)
XCTAssertEqual(one.degreeOfInclusion(with: two), .dualExclusivesOnly)
XCTAssertEqual(two.degreeOfInclusion(with: one), .dualExclusivesOnly)
// The order changes which comparison branch is used and which versus-nil
// case is used.
}

/// Check when there are only common elements.
func testIdentical() {
let single = CollectionOfOne(1)
XCTAssertEqual(single.degreeOfInclusion(with: single), .sharedOnly)
}

/// Check when the first source is a superset of the second.
func testFirstIncludesSecond() {
XCTAssertEqual([1, 2, 3, 5, 7].degreeOfInclusion(with: [1, 3, 5, 7]),
.firstExtendsSecond)
XCTAssertEqual([2, 4, 6, 8].degreeOfInclusion(with: [2, 4, 6]),
.firstExtendsSecond)
// The logic path differs if the last elements tie, or the first source's
// last element is bigger. (The second's last element can't be biggest.)
}

/// Check when the second source is a superset of the first.
func testSecondIncludesFirst() {
XCTAssertEqual([1, 3, 5, 7].degreeOfInclusion(with: [1, 2, 3, 5, 7]),
.secondExtendsFirst)
XCTAssertEqual([2, 4, 6].degreeOfInclusion(with: [2, 4, 6, 8]),
.secondExtendsFirst)
// The logic path differs if the last elements tie, or the second source's
// last element is bigger. (The first's last element can't be biggest.)
}

/// Check when there are shared and two-way exclusive elements.
func testPartialOverlap() {
XCTAssertEqual([3, 6, 9].degreeOfInclusion(with: [2, 4, 6, 8]),
.dualExclusivesAndShared)
XCTAssertEqual([1, 2, 4].degreeOfInclusion(with: [1, 4, 16]),
.dualExclusivesAndShared)
// For the three categories; exclusive to first, exclusive to second, and
// shared; if the third one encountered isn't from the last element(s) from
// a sequence(s), then the iteration will end early. The first example
// uses the short-circuit condition.
}
}