diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0a457..eedf93c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,21 @@ package updates, you can specify your package dependency using ## [Unreleased] -*No changes yet.* +### Additions + +- The `sortedEndIndex(by:)` and `sortedEndIndex()` methods check when a + collection stops being sorted. The `rampedEndIndex(by:)` and + `rampedEndIndex()` methods are variants that check for strict increases in + rank, instead of non-decreases. The `firstVariance(by:)` and + `firstVariance()` are variants in the other direction, checking for a run + with no changes in value. +- The `sortedRange(for: by:)` and `sortedRange(for:)` methods perform a binary + search for a value within an already-sorted collection. To optimize time in + some circumstances, an isolated phase of the binary-search procedure can be + done via the `someSortedPosition(of: by:)`, `lowerSortedBound(around: by:)`, + and `upperSortedBound(around: by:)` methods, each of which has a + defaulted-comparison overload (`someSortedPosition(of:)`, + `lowerSortedBound(around:)`, and `upperSortedBound(around:)`). --- diff --git a/Guides/BinarySearch.md b/Guides/BinarySearch.md new file mode 100644 index 00000000..263a8f99 --- /dev/null +++ b/Guides/BinarySearch.md @@ -0,0 +1,67 @@ +# Binary Search + +[[Source](../Sources/Algorithms/BinarySearch.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/BinarySearchTests.swift)] + +Methods that locate a given value within a collection, narrowing the location by half in each round. The collection already has to be sorted along the given predicate, or simple non-decreasing order if the predicate is defaulted to the standard less-than operator. + +As many data structures need to internally store their elements in order, the pre-sorted requirement usually isn't onerous. + +(To-Do: put better explanation here.) + +## Detailed Design + +The core methods are declared as extensions to `Collection`. The versions that default comparison to the less-than operator are constrained to collections where the element type conforms to `Comparable`. + +```swift +extension Collection { + func someSortedPosition( + of target: Element, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> (index: Index, isMatch: Bool) + + func lowerSortedBound( + around match: Index, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index + + func upperSortedBound( + around match: Index, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index + + func sortedRange( + for target: Element, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Range +} + +extension Collection where Element: Comparable { + func someSortedPosition(of target: Element) -> (index: Index, isMatch: Bool) + func lowerSortedBound(around match: Index) -> Index + func upperSortedBound(around match: Index) -> Index + func sortedRange(for target: Element) -> Range +} +``` + +Generally, only `sortedRange(for:)`, or `sortedRange(for: by:)`, is needed to perform a binary search. These methods are wrappers to calls to the other three. Use those other methods if you need only one phase of the search process and you want to save time. + +Note that while `sortedRange` and `someSortedPosition` work with a target value, and therefore may not be actually present in the collection, the `lowerSortedBound` and `upperSortedBound` methods work with a target index; that index must point to a known-good match, such as the first result from `someSortedPosition` (if the second result from that same call is `true`). + +### Complexity + +The search process narrows down the range in half each time, leading the search to work in O(log _n_) rounds, where _n_ is the length of the collection. When the collection supports O(1) traversal, _i.e._ random access, the search will then work in O(log _n_) operations. Search is permitted for collections with sub-random-access traversal, but this worsens the time for search to O(_n_). + +### Comparison with other languages + +**C++:** The `` library defines `binary_search` as an analog to `someSortedPosition`. The C++ function returns only an existence check; you cannot exploit the result, either success or failure, without calling a related method. Since the computation ends up with the location anyway, the Swift method bundles the existence check along with where the qualifying element was found. The returned index helps even during failure, as it's the best place to insert a matching element. + +Of course, immediately using only the `isMatch` member from a call to `someSortedPosition` acts as a direct counterpart to `binary_search`. + +Some implementations of `binary_search` may punt to `lower_bound`, but `someSortedPosition` stops at the first discovered match, without unnecessarily taking extra time searching for the border. The trade-off is that `someSortedPosition` needs to do up to two comparisons per round instead of one. + +The same library defines `lower_bound` and `upper_bound` as analogs to `lowerSortedBound` and `upperSortedBound`. The C++ functions match `binary_search` in that they search for a target value, while the Swift methods take a known-good target index. This difference in the Swift methods is meant to segregate functionality. + +The same C++ library defines `equal_range` as an analog to `sortedRange`. + +(To-Do: Put other languages here.) diff --git a/Guides/SortedPrefix.md b/Guides/SortedPrefix.md new file mode 100644 index 00000000..d4957b6b --- /dev/null +++ b/Guides/SortedPrefix.md @@ -0,0 +1,49 @@ +# Sorted Prefix + +[[Source](../Sources/Algorithms/SortedPrefix.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/SortedPrefixTests.swift)] + +Methods to measure how long a collection maintains being sorted, either along a given predicate or defaulting to the standard less-than operator, with variants for strictly-increasing and steady-state sequences. + +(To-Do: put better explanation here.) + +## Detailed Design + +The core methods are declared as extensions to `Collection`. The versions that default comparison to the less-than operator are constrained to collections where the element type conforms to `Comparable`. + +```swift +extension Collection { + func sortedEndIndex( + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index + + func rampedEndIndex( + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index + + func firstVariance( + by areEquivalent: (Element, Element) throws -> Bool + ) rethrows -> Index +} + +extension Collection where Element: Comparable { + func sortedEndIndex() -> Index + func rampedEndIndex() -> Index +} + +extension Collection where Element: Equatable { + func firstVariance() -> Index +} +``` + +Checking if the entire collection is sorted (or strictly increasing, or steady-state) can be done by comparing the result of a showcased method to `endIndex`. + +### Complexity + +These methods have to walk their entire collection until a non-match is found, so they all work in O(_n_) operations, where _n_ is the length of the collection. + +### Comparison with other languages + +**C++:** The `` library defines `is_sorted` and `is_sorted_until`, the latter of which functions like `sortedEndPrefix`. + +(To-Do: Put other languages here.) diff --git a/README.md b/README.md index f73b351c..1da5cc75 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,16 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`randomStableSample(count:)`, `randomStableSample(count:using:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/RandomSampling.md): Randomly selects a specific number of elements from a collection, preserving their original relative order. - [`uniqued()`, `uniqued(on:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Unique.md): The unique elements of a collection, preserving their order. +#### Sorted-Collection operations + +- [`sortedEndIndex(by:)`, `sortedEndIndex()`](./Guides/SortedPrefix.md): Reports when a collection stops being sorted. +- [`rampedEndIndex(by:)`, `rampedEndIndex()`](./Guides/SortedPrefix.md): Reports when a collection stops being strictly increasing. +- [`firstVariance(by:)`, `firstVariance()`](./Guides/SortedPrefix.md): Reports when a collection stops being at a constant value. +- [`someSortedPosition(of: by:)`, `someSortedPosition(of:)`](./Guides/BinarySearch.md): Locates if and where a target value is within a sorted collection. +- [`lowerSortedBound(around: by:)`, `lowerSortedBound(around:)`](./Guides/BinarySearch.md): Reports the lower bound for the equal-valued subsequence within a sorted collection that covers the targeted element. +- [`upperSortedBound(around: by:)`, `upperSortedBound(around:)`](./Guides/BinarySearch.md): Reports the upper bound for the equal-valued subsequence within a sorted collection that covers the targeted element. +- [`sortedRange(for: by:)`, `sortedRange(for:)`](./Guides/BinarySearch.md): Locates the subsequence within a sorted collection that contains all the elements matching a target value. + #### Other useful operations - [`chunked(by:)`, `chunked(on:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Chunked.md): Eager and lazy operations that break a collection into chunks based on either a binary predicate or when the result of a projection changes. diff --git a/Sources/Algorithms/BinarySearch.swift b/Sources/Algorithms/BinarySearch.swift new file mode 100644 index 00000000..ee60506f --- /dev/null +++ b/Sources/Algorithms/BinarySearch.swift @@ -0,0 +1,359 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// someSortedPosition(of: by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns where the given value may appear in the collection, assuming the + /// collection is (at least) partitioned along the given predicate used for + /// comparing between elements. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied in + /// the precondition. + /// + /// If the return value flags a non-match, the emitted index is the best + /// position where `target` may be inserted and let the collection maintain + /// its values' (semi-)sort. + /// + /// - Precondition: + /// - All of the elements equivalent to `target` are in a contiguous + /// subsequence. + /// - All of the elements ordered less than `target` form a (possibly empty) + /// prefix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// - All of the elements ordered greater than `target` form a (possibly + /// empty) suffix of the collection. These elements are not necessarily + /// sorted within this subsequence. + /// + /// - Parameters: + /// - target: An element to search for in the collection. + /// - areInIncreasingOrder: A predicate that returns `true` if its first + /// argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: A two-element tuple. The first member is the first possible of + /// the following: an index into this collection for an element that is + /// equivalent to `target`, the index for the first element that is ordered + /// greater than `target`, or `endIndex`. The second member indicates + /// whether the first member points to an equivalent element. + /// + /// - Complexity: O(log *n*), where *n* is the length of this collection if + /// the collection conforms to `RandomAccessCollection`, otherwise O(*n*). + public func someSortedPosition( + of target: Element, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> (index: Index, isMatch: Bool) { + var span = count, start = startIndex, end = endIndex + while start < end { + let semispan = span / 2, pivot = index(start, offsetBy: semispan) + let candidate = self[pivot] + if try areInIncreasingOrder(candidate, target) { + // Too small; check the larger values in the suffix. + start = index(after: pivot) + span -= semispan + 1 + } else if try areInIncreasingOrder(target, candidate) { + // Too large; check the smaller values in the prefix. + end = pivot + span = semispan + } else { + // Just right! + return (pivot, true) + } + } + assert(span == 0) + return (end, false) + } +} + +//===----------------------------------------------------------------------===// +// lowerSortedBound(around: by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Assuming the collection is (at least) partitioned along the given + /// predicate used for comparing between elements, returns the starting index + /// for the contiguous subsequence containing the elements equivalent to the + /// one at the given index. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied + /// within the precondition. + /// + /// - Precondition: + /// - `match` is a valid index of the collection, but less than `endIndex`. + /// - All of the elements equivalent to `self[match]` are in a contiguous + /// subsequence. + /// - All of the elements ordered less than `self[match]` form a (possibly + /// empty) prefix of the collection. These elements are not necessarily + /// sorted within this subsequence. + /// - All of the elements ordered greater than `self[match]` form a + /// (possibly empty) suffix of the collection. These elements are not + /// necessarily sorted within this subsequence. + /// + /// - Parameters: + /// - match: An index for the value to be bound within the collection. + /// - areInIncreasingOrder: A predicate that returns `true` if its first + /// argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: The index for the first element of the collection equivalent to + /// `self[match]`. May be `match` itself. + /// + /// - Complexity: O(log *m*), where *m* is the distance between `startIndex` + /// and `match` if the collection conforms to `RandomAccessCollection`, + /// otherwise O(*m*). + @inlinable + public func lowerSortedBound( + around match: Index, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index { + let target = self[match] + return try self[...match].partitioningIndex { try !areInIncreasingOrder($0, target) } + } +} + +//===----------------------------------------------------------------------===// +// upperSortedBound(around: by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Assuming the collection is (at least) partitioned along the given + /// predicate used for comparing between elements, returns the past-the-end + /// index for the contiguous subsequence containing the elements equivalent to + /// the one at the given index. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied + /// within the precondition. + /// + /// - Precondition: + /// - `match` is a valid index of the collection, but less than `endIndex`. + /// - All of the elements equivalent to `self[match]` are in a contiguous + /// subsequence. + /// - All of the elements ordered less than `self[match]` form a (possibly + /// empty) prefix of the collection. These elements are not necessarily + /// sorted within this subsequence. + /// - All of the elements ordered greater than `self[match]` form a + /// (possibly empty) suffix of the collection. These elements are not + /// necessarily sorted within this subsequence. + /// + /// - Parameters: + /// - match: An index for the value to be bound within the collection. + /// - areInIncreasingOrder: A predicate that returns `true` if its first + /// argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: The index for the first element that is ordered greater than + /// `self[match]`. If there is no such element, `endIndex` is returned + /// instead. + /// + /// - Complexity: O(log *m*), where *m* is the distance between `match` and + /// `endIndex` if the collection conforms to `RandomAccessCollection`, + /// otherwise O(*m*). + @inlinable + public func upperSortedBound( + around match: Index, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index { + let target = self[match] + return try self[match...].partitioningIndex { try areInIncreasingOrder(target, $0) } + } +} + +//===----------------------------------------------------------------------===// +// sortedRange(for: by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns the bounds for the contiguous subsequence of all the elements + /// equivalent to the given value, assuming the collection is (at least) + /// partitioned along the given predicate used for comparing between elements. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied in + /// the precondition. + /// + /// If no matches are found, the returned empty range hovers over the best + /// position where `target` may be inserted and let the collection maintain + /// its values' (semi-)sort. + /// + /// - Precondition: + /// - All of the elements equivalent to `target` are in a contiguous + /// subsequence. + /// - All of the elements ordered less than `target` form a (possibly empty) + /// prefix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// - All of the elements ordered greater than `target` form a (possibly + /// empty) suffix of the collection. These elements are not necessarily + /// sorted within this subsequence. + /// + /// - Parameters: + /// - target: An element to search for in the collection. + /// - areInIncreasingOrder: A predicate that returns `true` if its first + /// argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: A range for the shortest subsequence containing all the + /// elements equivalent to `target`. The range always ends at the first + /// possible of the following: the first element ordered greater than + /// `target`, or `endIndex`. The returned range will be empty if there are + /// no matches. + /// + /// - Complexity: O(log *n*), where *n* is the length of this collection if + /// the collection conforms to `RandomAccessCollection`, otherwise O(*n*). + public func sortedRange( + for target: Element, + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Range { + let (match, success) = try someSortedPosition(of: target, by: areInIncreasingOrder) + guard success else { return match.. (index: Index, isMatch: Bool) { + return someSortedPosition(of: target, by: <) + } + + /// Assuming the collection is (at least) partitioned along the relative order + /// each element has to the value at the given index, returns the starting + /// index for the contiguous subsequence containing the elements equal to that + /// value. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied + /// within the precondition. + /// + /// - Precondition: + /// - `match` is a valid index of the collection, but less than `endIndex`. + /// - All of the elements equal to `self[match]` are in a contiguous + /// subsequence. + /// - All of the elements less than `self[match]` form a (possibly empty) + /// prefix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// - All of the elements greater than `self[match]` form a (possible empty) + /// suffix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// + /// - Parameters: + /// - match: An index for the value to be bound within the collection. + /// - Returns: The index for the first element of the collection equal to + /// `self[match]`. May be `match` itself. + /// + /// - Complexity: O(log *m*), where *m* is the distance between `startIndex` + /// and `match` if the collection conforms to `RandomAccessCollection`, + /// otherwise O(*m*). + @inlinable public func lowerSortedBound(around match: Index) -> Index { + return lowerSortedBound(around: match, by: <) + } + + /// Assuming the collection is (at least) partitioned along the relative order + /// each element has to the value at the given index, returns the past-the-end + /// index for the contiguous subsequence containing the elements equal to that + /// value. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied + /// within the precondition. + /// + /// - Precondition: + /// - `match` is a valid index of the collection, but less than `endIndex`. + /// - All of the elements equal to `self[match]` are in a contiguous + /// subsequence. + /// - All of the elements less than `self[match]` form a (possibly empty) + /// prefix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// - All of the elements greater than `self[match]` form a (possibly empty) + /// suffix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// + /// - Parameters: + /// - match: An index for the value to be bound within the collection. + /// - Returns: The index for the first element greater than `self[match]`. If + /// there is no such element, `endIndex` is returned instead. + /// + /// - Complexity: O(log *m*), where *m* is the distance between `match` and + /// `endIndex` if the collection conforms to `RandomAccessCollection`, + /// otherwise O(*m*). + @inlinable public func upperSortedBound(around match: Index) -> Index { + return upperSortedBound(around: match, by: <) + } + + /// Returns the bounds for the contiguous subsequence of all the elements + /// equal to the given value, assuming the collection is (at least) + /// partitioned along the relative order each element has to that value. + /// + /// A fully sorted sequence meets the criteria of the partitioning implied in + /// the precondition. + /// + /// If no matches are found, the returned empty range hovers over the best + /// position where `target` may be inserted and let the collection maintain + /// its values' (semi-)sort. + /// + /// - Precondition: + /// - All of the elements equal to `target` are in a contiguous subsequence. + /// - All of the elements less than `target` form a (possibly empty) prefix + /// of the collection. These elements are not necessarily sorted within + /// this subsequence. + /// - All of the elements greater than `target` form a (possibly empty) + /// suffix of the collection. These elements are not necessarily sorted + /// within this subsequence. + /// + /// - Parameters: + /// - target: An element to search for in the collection. + /// - Returns: A range for the shortest subsequence containing all the + /// elements equal to `target`. The range always ends at the first possible + /// of the following: the first element greater than `target`, or + /// `endIndex`. The returned range will be empty if there are no matches. + /// + /// - Complexity: O(log *n*), where *n* is the length of this collection if + /// the collection conforms to `RandomAccessCollection`, otherwise O(*n*). + @inlinable public func sortedRange(for target: Element) -> Range { + return sortedRange(for: target, by: <) + } +} diff --git a/Sources/Algorithms/SortedPrefix.swift b/Sources/Algorithms/SortedPrefix.swift new file mode 100644 index 00000000..cf46367a --- /dev/null +++ b/Sources/Algorithms/SortedPrefix.swift @@ -0,0 +1,179 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// sortedEndIndex(by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns the past-the-end index for the longest prefix of the collection + /// that is sorted according to the given predicate used for comparisons + /// between elements. + /// + /// If `endIndex` is returned, then the entire collection is sorted. + /// Sequences shorter than two elements in length are always completely + /// sorted. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its + /// first argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: The index of the first element that is in decreasing order + /// relative to its predecessor. If there is no such element, `endIndex` is + /// returned instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + public func sortedEndIndex( + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index { + let end = endIndex + guard var previousValue = first else { return end } + + var currentIndex = index(after: startIndex) + while currentIndex < end, + case let currentValue = self[currentIndex], + try !areInIncreasingOrder(currentValue, previousValue) { + previousValue = currentValue + formIndex(after: ¤tIndex) + } + return currentIndex + } +} + +//===----------------------------------------------------------------------===// +// rampedEndIndex(by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns the past-the-end index for the longest prefix of the collection + /// with strictly increasing values according to the given predicate used for + /// comparisons between elements. + /// + /// If `endIndex` is returned, then the entire collection is strictly + /// increasing (and sorted). Sequences shorter than two elements in length + /// always have complete coverage in being strictly increasing. + /// + /// - Parameter areInIncreasingOrder: A predicate that returns `true` if its + /// first argument should be ordered before its second argument; otherwise, + /// `false`. + /// - Returns: The index of the first element that is not in increasing order + /// relative to its predecessor. If there is no such element, `endIndex` is + /// returned instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + public func rampedEndIndex( + by areInIncreasingOrder: (Element, Element) throws -> Bool + ) rethrows -> Index { + let end = endIndex + guard var previousValue = first else { return end } + + var currentIndex = index(after: startIndex) + while currentIndex < end, + case let currentValue = self[currentIndex], + try areInIncreasingOrder(previousValue, currentValue) { + previousValue = currentValue + formIndex(after: ¤tIndex) + } + return currentIndex + } +} + +//===----------------------------------------------------------------------===// +// firstVariance(by:) +//===----------------------------------------------------------------------===// + +extension Collection { + /// Returns the index of the earliest element not equivalent to the first + /// element, using the given predicate as the equivalence test. + /// + /// The predicate must be a *equivalence relation* over the elements. That + /// is, for any elements `a`, `b`, and `c`, the following conditions must + /// hold: + /// + /// - `areEquivalent(a, a)` is always `true`. (Reflexivity) + /// - `areEquivalent(a, b)` implies `areEquivalent(b, a)`. (Symmetry) + /// - If `areEquivalent(a, b)` and `areEquivalent(b, c)` are both `true`, then + /// `areEquivalent(a, c)` is also `true`. (Transitivity) + /// + /// Returns `endIndex` if the collection is shorter than two elements. + /// + /// - Parameter areEquivalent: A predicate that returns `true` if its two + /// arguments are equivalent; otherwise, `false`. + /// - Returns: The index for the first element *x* in `dropFirst()` such that + /// `areEquivalent(first!, x)` is `false`. If there is no such element, + /// `endIndex` is returned instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func firstVariance( + by areEquivalent: (Element, Element) throws -> Bool + ) rethrows -> Index { + guard let target = first else { return endIndex } + + return try dropFirst().indexed().first(where: { + try !areEquivalent(target, $0.element) + })?.index ?? endIndex + } +} + +//===----------------------------------------------------------------------===// +// sortedEndIndex() +// rampedEndIndex() +//===----------------------------------------------------------------------===// + +extension Collection where Element: Comparable { + /// Returns the past-the-end index for the longest prefix of the collection + /// that is sorted. + /// + /// If `endIndex` is returned, then the entire collection is sorted. + /// Sequences shorter than two elements in length are always completely + /// sorted. + /// + /// - Returns: The index of the first element that is less than its + /// predecessor. If there is no such element, `endIndex` is returned + /// instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func sortedEndIndex() -> Index { return sortedEndIndex(by: <) } + + /// Returns the past-the-end index for the longest prefix of the collection + /// with strictly increasing elements. + /// + /// If `endIndex` is returned, then the entire collection is strictly + /// increasing (and sorted). Sequences shorter than two elements in length + /// always have complete coverage in being strictly increasing. + /// + /// - Returns: The index of the first element that is less than or equal to + /// its predecessor. If there is no such element, `endIndex` is returned + /// instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func rampedEndIndex() -> Index { return rampedEndIndex(by: <) } +} + +//===----------------------------------------------------------------------===// +// firstVariance() +//===----------------------------------------------------------------------===// + +extension Collection where Element: Equatable { + /// Returns the index of the earliest element not equal to the first one. + /// + /// Returns `endIndex` if the collection is shorter than two elements. + /// + /// - Returns: The index for the first element *x* in `dropFirst()` such that + /// `first! == x` is `false`. If there is no such element, `endIndex` is + /// returned instead. + /// + /// - Complexity: O(*n*), where *n* is the length of the collection. + @inlinable + public func firstVariance() -> Index { return firstVariance(by: ==) } +} diff --git a/Tests/SwiftAlgorithmsTests/BinarySearchTests.swift b/Tests/SwiftAlgorithmsTests/BinarySearchTests.swift new file mode 100644 index 00000000..719605b8 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/BinarySearchTests.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 `sortedRange(for:)` and related binary search methods. +final class BinarySearchTests: XCTestCase { + /// Check for empty results from an empty source. + func testEmpty() { + let empty = EmptyCollection() + XCTAssertEqual(empty.sortedRange(for: 3.14), 0..<0) + } + + /// Check when the target is too small for a single-element source. + func testBelowSourceValue() { + let single = CollectionOfOne(0.0) + XCTAssertEqual(single.sortedRange(for: -1.1), 0..<0) + } + + /// Check when the target is too large for a single-element source. + func testAboveSourceValue() { + let single = CollectionOfOne(0.0) + XCTAssertEqual(single.sortedRange(for: +1.1), 1..<1) + } + + /// Check when the target matches the sole element. + func testOneElementOneMatch() { + let single = CollectionOfOne(0.0) + XCTAssertEqual(single.sortedRange(for: 0), 0..<1) + } + + /// Check when the target matches all the elements. + func testMultipleElementsAllMatch() { + let repeated = repeatElement(5.5, count: 5) + XCTAssertEqual(repeated.sortedRange(for: 5.5), 0..<5) + } + + /// Check when the target is too small for a multiple-element source. + func testBelowSourceValues() { + let sample = [2.2, 3.3, 4.4] + XCTAssertEqual(sample.sortedRange(for: 1.1), 0..<0) + } + + /// Check when the target is too large for a multiple-element source. + func testAboveSourceValues() { + let sample = [2.2, 3.3, 4.4] + XCTAssertEqual(sample.sortedRange(for: 5.5), 3..<3) + } + + /// Check when the target fails in the middle of some values. + func testInternalMiss() { + let sample = [2.2, 3.3, 4.4] + XCTAssertEqual(sample.sortedRange(for: 3.14), 1..<1) + } + + /// Check when the target succeeds the first time within values. + func testMultipleElementsFirstCheck() { + let sample = [1.1, 2.2, 3.3, 4.4, 5.5] + XCTAssertEqual(sample.sortedRange(for: 3.3), 2..<3) + } + + /// Check when the target succeeds after some lowering. + func testMultipleElementsNarrowDownPrefixes() { + let sample = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10, 11.11, + 12.12, 13.13, 14.14, 15.15, 16.16, 17.17] + XCTAssertEqual(sample.sortedRange(for: 3.3), 2..<3) + } + + /// Check when the target succeeds after some raising. + func testMultipleElementsNarrowDownSuffixes() { + let sample = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10, 11.11, + 12.12, 13.13, 14.14, 15.15, 16.16, 17.17] + XCTAssertEqual(sample.sortedRange(for: 15.15), 14..<15) + } + + /// Check when the target succeeds after some lowering and raising. + func testMultipleElementsJumpingAround() { + let sample = [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10, 11.11, + 12.12, 13.13, 14.14, 15.15, 16.16, 17.17] + XCTAssertEqual(sample.sortedRange(for: 7.7), 6..<7) + XCTAssertEqual(sample.sortedRange(for: 11.11), 10..<11) + } +} diff --git a/Tests/SwiftAlgorithmsTests/SortedPrefixTests.swift b/Tests/SwiftAlgorithmsTests/SortedPrefixTests.swift new file mode 100644 index 00000000..01c23cb8 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/SortedPrefixTests.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// 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 `sortedEndIndex` and `rampedEndIndex`. +final class SortedPrefixTests: XCTestCase { + /// Check that empty sequences are always increasing. + func testEmpty() { + let empty = EmptyCollection() + XCTAssertEqual(empty.sortedEndIndex(), empty.endIndex) + XCTAssertEqual(empty.rampedEndIndex(), empty.endIndex) + XCTAssertEqual(empty.firstVariance(), empty.endIndex) + } + + /// Check that single-element sequences are always increasing. + func testSingleElement() { + let single = CollectionOfOne(1.1) + XCTAssertEqual(single.sortedEndIndex(), single.endIndex) + XCTAssertEqual(single.rampedEndIndex(), single.endIndex) + XCTAssertEqual(single.firstVariance(), single.endIndex) + } + + /// Test for failures at second element. + func testFailSecond() { + let sample = [4.4, -2.2, 0, 5.5] + XCTAssertEqual(sample.sortedEndIndex(), sample.dropFirst().startIndex) + XCTAssertEqual(sample.rampedEndIndex(), sample.dropFirst().startIndex) + XCTAssertEqual(sample.firstVariance(), sample.dropFirst().startIndex) + } + + /// Test for failures after the second element. + func testFailAfterElementIteration() { + let sample = [-2.2, 4.4, 0, 5.5] + XCTAssertEqual(sample.sortedEndIndex(), sample.dropFirst(2).startIndex) + XCTAssertEqual(sample.rampedEndIndex(), sample.dropFirst(2).startIndex) + } + + /// Check that unchanging sequences are always sorted, but never increase. + func testSteadyState() { + let repeated = repeatElement(5.5, count: 5) + XCTAssertEqual(repeated.sortedEndIndex(), repeated.endIndex) + XCTAssertEqual(repeated.rampedEndIndex(), repeated.dropFirst().startIndex) + XCTAssertEqual(repeated.firstVariance(), repeated.endIndex) + } + + /// Check that a range is always increasing. + func testRange() { + let range = -10...10 + XCTAssertEqual(range.sortedEndIndex(), range.endIndex) + XCTAssertEqual(range.rampedEndIndex(), range.endIndex) + } +}