diff --git a/CHANGELOG.md b/CHANGELOG.md index 84f0a457..1a40e14b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,14 @@ package updates, you can specify your package dependency using ## [Unreleased] -*No changes yet.* +### Additions + +- The `deltas(via:)` method on sequences uses a closure on each pair of + adjacent elements and vends those results as a new sequence, with eager and + lazy versions. The variants `differences()`, `wrappedDifferences()`, and + `strides()` call `deltas(via:)` with a closure defaulted to a metric method + provided by the element's protocol (`-`, `&-`, and `distance(to:)`, + respectively). --- diff --git a/Guides/Deltas.md b/Guides/Deltas.md new file mode 100644 index 00000000..355c1b15 --- /dev/null +++ b/Guides/Deltas.md @@ -0,0 +1,115 @@ +# Deltas + +[[Source](../Sources/Algorithms/Deltas.swift) | + [Tests](../Tests/SwiftAlgorithmsTests/DeltasTests.swift)] + +Generates a sequence mapping all non-last elements of the source sequence to +the distance between the element and its successor, using a given closure to +evaluate the metric. + +```swift +let numbers = [1, 3, 12, 60].deltas(via: /) +// numbers == [3, 4, 5] + +let letterSkips = "ABCDE".unicodeScalars.lazy.deltas { $0.value - $1.value } +// Array(letterSkips) == [1, 1, 1, 1] + +let empty = CollectionOfOne(3.3).deltas(via: -) +// empty == [] +``` + +To return any distances, the source sequence needs to be at least two elements +in length. + +## Detailed Design + +A new method is added to sequences, with an overload for laziness: + +```swift +extension Sequence { + func deltas( + via subtracter: (Element, Element) throws -> T + ) rethrows -> [T] +} + +extension LazySequenceProtocol { + func deltas( + via subtracter: @escaping (Element, Element) -> T + ) -> DeltasSequence +} +``` + +The eager version of `deltas(via:)` copies the distances to a standard-library +`Array`. The lazy version encapsulates the distances into a new +`DeltasSequence` generic value type, parameterized on the source sequence's +type and the distance type. This type conforms to `Sequence` and +`LazySequenceProtocol`, escalating to `Collection` and `LazyCollectionProtocol` +if the source sequence type is also a collection type, and to +`BidirectionalCollection` and `RandomAccessCollection` if the source type +conforms too. + +The standard library contains several protocols that can provide common metric +functions. Variants of `deltas(via:)` have been made that use the standard +library routines, assuming that the source sequence's element type conforms to +the prerequiste protocol. + +```swift +extension Sequence where Element: AdditiveArithmetic { + func differences() -> DeltasSequence +} + +extension Sequence where Element: SIMD, Element.Scalar: FloatingPoint { + func differences() -> DeltasSequence +} + +extension Sequence where Element: FixedWidthInteger { + func wrappedDifferences() -> DeltasSequence +} + +extension Sequence where Element: SIMD, Element.Scalar: FixedWidthInteger { + func wrappedDifferences() -> DeltasSequence +} + +extension Sequence where Element: Strideable { + func strides() -> DeltasSequence +} +``` + +The `differences()` methods use the `-` operator. The `wrappedDifferences()` +methods use the `&-` operator. And the `strides()` method uses the +`.distance(to:)` method. Note all of these methods return a lazily generated +sequence/collection, since the core methods are non-throwing. + +### Complexity + +Calling the eager version of `deltas(via:)` is O(_n_), where _n_ is the length +of the source sequence. The lazy version, and its protocol-based metric +variants, are all O(_1_) to initially call, but O(_n_) again for running +through a single pass of the results. + +### Naming + +The name (as of this writing) for the core method is original by the author, +inspired by the use of "delta" in other computer-science contexts related to +changes. If there are terms-of-art for the concept, suggestions will be +appreciated. The names for the protocol-based metric variants are based on the +descriptions of their return values or types. + +### Comparison with other languages + +**Swift:** The `deltas(via:)` method is the counter operation to the `scan(_:)` +method, which acts like `reduce(_:_:)` but provides not only the final +combination but the intermediate results, all packaged as a sequence. There is +a version of `scan` in the "Combine" library of Apple's SDK, but a non-Reactive +version has not (yet) appeared in the standard library. + +**[C++][C++]:** Has an `adjacent_difference` function which takes a bounding +input iterator pair, an output iterator, and optionally a metric function +(which defaults to subtraction when not given), returning the updated output +iterator. It restricts the metric function to use parameter and return types +that are compatible with the input iterator's element type. (The output +iterator's element type must also be compatible.) + + + +[C++]: https://en.cppreference.com/w/cpp/algorithm/adjacent_difference diff --git a/README.md b/README.md index 9033c90a..3b9ac39a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Read more about the package, and the intent behind it, in the [announcement on s - [`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. - [`indexed()`](https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md): Iterate over tuples of a collection's indices and elements. - [`trimming(where:)`](https://github.com/apple/swift-algorithms/blob/main/Guides/Trim.md): Returns a slice by trimming elements from a collection's start and end. +- [`deltas(via:)`, `differences()`, `wrappedDifferences()`, `strides()`](./Guides/Deltas.md): Eager and lazy operations that evaluate the differences between each element of a sequence using a binary closure and publishes the result as another sequence. There are specialized methods for sequences with element types that support: subtraction, vector and/or wrapping subtraction, and `Strideable`. ## Adding Swift Algorithms as a Dependency diff --git a/Sources/Algorithms/Deltas.swift b/Sources/Algorithms/Deltas.swift new file mode 100644 index 00000000..36675712 --- /dev/null +++ b/Sources/Algorithms/Deltas.swift @@ -0,0 +1,414 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// An iterator wrapper that vends the changes between each consecutive pair of +/// elements, as evaluated by some closure. +public struct DeltasIterator { + /// The source of the operands for the differentiating closure. + @usableFromInline + var base: Base + /// The closure that evaluates the difference between source elements. + @usableFromInline + let subtracter: (Base.Element, Base.Element) throws -> Element + /// The last element from `base` read. + var previous: Base.Element? + + /// Creates an iterator vending the differences between consecutive elements + /// from the given iterator using the given closure. + @inlinable + init( + _ base: Base, + via subtracter: @escaping (Base.Element, Base.Element) throws -> Element + ) { + self.base = base + self.subtracter = subtracter + } +} + +extension DeltasIterator { + /// Advances to the next element, possibly throwing in the attempt, and + /// returns it, or `nil` if no next element exists. + @usableFromInline + mutating func throwingNext() throws -> Element? { + guard let previous = previous else { + guard let first = base.next() else { return nil } + + self.previous = first + return try throwingNext() + } + guard let current = base.next() else { return nil } + defer { self.previous = current } + + return try subtracter(current, previous) + } +} + +extension DeltasIterator: IteratorProtocol { + @inlinable + public mutating func next() -> Element? { + return try! throwingNext() + } +} + +/// A sequence wrapper that vends the changes between each consecutive pair of +/// elements, as evaluated by some closure. +public struct DeltasSequence { + /// The source of the operands for the differentiating closure. + public let base: Base + /// The closure that evaluates the difference between source elements. + @usableFromInline + let subtracter: (Base.Element, Base.Element) throws -> Element + + /// Creates a sequence vending the differences between consecutive elements + /// of the given sequence using the given closure. + @inlinable + init( + _ base: Base, + via subtracter: @escaping (Base.Element, Base.Element) throws -> Element + ) { + self.base = base + self.subtracter = subtracter + } +} + +extension DeltasSequence: LazySequenceProtocol { + @inlinable + public var underestimatedCount: Int + { Swift.max(base.underestimatedCount - 1, 0) } + + @inlinable + public func makeIterator() -> DeltasIterator { + return DeltasIterator(base.makeIterator(), via: subtracter) + } +} + +/// A collection wrapper presenting the changes between each consecutive pair of +/// elements, as evaluated by some closure. +public typealias DeltasCollection = DeltasSequence + +extension DeltasSequence: Collection, LazyCollectionProtocol +where Base: Collection { + @inlinable + public var startIndex: Base.Index { + let start = base.startIndex, end = base.endIndex + guard let second = base.index(start, offsetBy: +1, limitedBy: end), + second < end else { + // Need at least two wrapped elements to start. + return end + } + + return start + } + @inlinable public var endIndex: Base.Index { base.endIndex } + + @inlinable + public subscript(position: Base.Index) -> Element { + // If position is either base.end or base.indices.last, we get a crash. + return try! subtracter(base[base.index(after: position)], base[position]) + } + @inlinable + public subscript(bounds: Range) + -> DeltasSequence { + guard bounds.upperBound < base.endIndex else { + return SubSequence(base[bounds.lowerBound...], via: subtracter) + } + + return SubSequence(base[bounds.lowerBound ... bounds.upperBound], + via: subtracter) + } + + @inlinable + public func index(_ i: Base.Index, offsetBy distance: Int) -> Base.Index { + let endPoint = distance < 0 ? startIndex : endIndex + return index(i, offsetBy: distance, limitedBy: endPoint)! + } + public func index( + _ i: Base.Index, offsetBy distance: Int, limitedBy limit: Base.Index + ) -> Base.Index? { + guard let result = base.index(i, offsetBy: distance, limitedBy: limit) + else { return nil } + + if case let end = base.endIndex, result < end, + base.index(after: result) == end { + // Landed on the forbidden last base element, skip past it in the + // direction of movement. + if distance > 0 { + return end + } else if distance < 0 { + return base.index(result, offsetBy: -1, + limitedBy: Swift.max(base.startIndex, limit)) + } else { + preconditionFailure("Used the forbidden base index value") + } + } else { + return result + } + } + @inlinable + public func distance(from start: Base.Index, to end: Base.Index) -> Int { + var rawResult = base.distance(from: start, to: end) + if case let baseEnd = base.endIndex, start == baseEnd || end == baseEnd { + // We went past the forbidden last element, so take it out of the distance + // calculation. + rawResult -= rawResult.signum() + } + return rawResult + } + + @inlinable + public func index(after i: Base.Index) -> Base.Index { + return index(i, offsetBy: +1) + } +} + +extension DeltasSequence: BidirectionalCollection +where Base: BidirectionalCollection { + @inlinable + public func index(before i: Base.Index) -> Base.Index { + return index(i, offsetBy: -1) + } +} + +extension DeltasSequence: RandomAccessCollection +where Base: RandomAccessCollection {} + +//===----------------------------------------------------------------------===// +// deltas(storingInto:via:) +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Differentiates the sequence by applying the given closure between each + /// pair of consecutive elements in order, copying the results into an + /// instance of the given collection type. + /// + /// When the closure is called with a pair of consecutive elements, the latter + /// element is used as the first argument and the former element is used as + /// the second argument. If your closure defines its parameters' order such + /// that the source occurs first and the destination second, wrap that closure + /// in another that swaps the arguments' positions first. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// let deltas1 = fib.deltas(storingInto: Array.self, via: -) + /// let deltas2 = fib.deltas(storingInto: Array.self) { $1.distance(to: $0) } + /// print(deltas1, deltas2) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13] [0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Precondition: The sequence must be finite. + /// + /// - Parameters: + /// - type: The metatype specifier for the collection to be returned. + /// - subtracter: The closure that computes a value needed to traverse from + /// the closure's second argument to its first argument. + /// - Returns: A collection containing the changes, starting with the delta + /// between the first and second elements, and ending with the delta between + /// the next-to-last and last elements. The collection is empty if the + /// receiver has less than two elements. + /// + /// - Complexity: O(*n*), where *n* is the length of the sequence. + @usableFromInline + internal func deltas( + storingInto type: T.Type, + via subtracter: (Element, Element) throws -> T.Element + ) rethrows -> T { + var result = T() + try withoutActuallyEscaping(subtracter) { + var sequence = DeltasSequence(self, via: $0), + iterator = sequence.makeIterator() + result.reserveCapacity(sequence.underestimatedCount) + while let delta = try iterator.throwingNext() { + result.append(delta) + } + } + return result + } +} + +//===----------------------------------------------------------------------===// +// deltas(via:) +//===----------------------------------------------------------------------===// + +extension Sequence { + /// Differentiates the sequence into an array, formed by applying the given + /// closure on each pair of consecutive elements in order. + /// + /// When the closure is called with a pair of consecutive elements, the latter + /// element is used as the first argument and the former element is used as + /// the second argument. If your closure defines its parameters' order such + /// that the source occurs first and the destination second, wrap that closure + /// in another that swaps the arguments' positions first. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// let deltas1 = fib.deltas(via: -) + /// let deltas2 = fib.deltas() { $1.distance(to: $0) } + /// print(deltas1, deltas2) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13] [0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Precondition: The sequence must be finite. + /// + /// - Parameters: + /// - subtracter: The closure that computes a value needed to traverse from + /// the closure's second argument to its first argument. + /// - Returns: An array containing the changes, starting with the delta + /// between the first and second elements, and ending with the delta between + /// the next-to-last and last elements. The array is empty if the receiver + /// has less than two elements. + /// + /// - Complexity: O(*n*), where *n* is the length of the sequence. + @inlinable + public func deltas(via subtracter: (Element, Element) throws -> T) + rethrows -> [T] { + return try deltas(storingInto: Array.self, via: subtracter) + } +} + +extension LazySequenceProtocol { + /// Differentiates this sequence into a lazily generated sequence, formed by + /// applying the given closure on each pair of consecutive elements in order. + /// + /// When the closure is called with a pair of consecutive elements, the latter + /// element is used as the first argument and the former element is used as + /// the second argument. If your closure defines its parameters' order such + /// that the source occurs first and the destination second, wrap that closure + /// in another that swaps the arguments' positions first. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// let deltas1 = fib.lazy.deltas(via: -) + /// let deltas2 = fib.lazy.deltas() { $1.distance(to: $0) } + /// print(Array(deltas1), Array(deltas2)) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13] [0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Parameters: + /// - subtracter: The closure that computes a value needed to traverse from + /// the closure's second argument to its first argument. + /// - Returns: A lazy sequence containing the changes, starting with the delta + /// between the first and second elements, and ending with the delta between + /// the next-to-last and last elements. The result is empty if the receiver + /// has less than two elements. + @inlinable + public func deltas(via subtracter: @escaping (Element, Element) -> T) + -> DeltasSequence { + return DeltasSequence(elements, via: subtracter) + } +} + +//===----------------------------------------------------------------------===// +// differences(), wrappedDifferences(), strides() +// +// (Note: Some Apple-SDK types use custom delta operations that share the same +// operator/method names needed below. They don't actually conform to the +// corresponding protocols, and as such can't use the following methods.) +//===----------------------------------------------------------------------===// + +extension Sequence where Element: AdditiveArithmetic { + /// Differentiates this sequence into a lazy sequence formed by the + /// differences between each pair of consecutive elements in order. + /// + /// This method uses `.lazy.deltas(via:)` with the closure being the `-` + /// operator. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// print(Array(fib.differences())) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Returns: A lazy sequence containing the differences, starting with the + /// difference between the first and second elements, and ending with the + /// difference between the next-to-last and last elements. The result is + /// empty if the receiver has less than two elements. + @inlinable + public func differences() -> DeltasSequence { + return lazy.deltas(via: -) + } +} + +extension Sequence where Element: SIMD, Element.Scalar: FloatingPoint { + /// Differentiates this sequence into a lazy sequence formed by the vector + /// differences between each pair of consecutive elements in order. + /// + /// This method uses `.lazy.deltas(via:)` with the closure being the `-` + /// operator. + /// + /// let fibPairs: [SIMD2] = [[1, 1], [1, 2], [2, 3], [3, 5]] + /// print(Array(fibPairs.differences())) + /// // Prints "[SIMD2(0.0, 1.0), SIMD2(1.0, 1.0), SIMD2(1.0, 2.0)]" + /// + /// - Returns: A lazy sequence containing the vector-differences, starting + /// with the difference between the first and second elements, and ending + /// with the difference between the next-to-last and last elements. The + /// result is empty if the receiver has less than two elements. + @inlinable + public func differences() -> DeltasSequence { + return lazy.deltas(via: -) + } +} + +extension Sequence where Element: FixedWidthInteger { + /// Differentiates this sequence into a lazy sequence formed by the wrapped + /// differences between each pair of consecutive elements in order. + /// + /// This method uses `.lazy.deltas(via:)` with the closure being the `&-` + /// operator. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// print(Array(fib.wrappedDifferences())) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Returns: A lazy sequence containing the differences, starting with the + /// wrapped-difference between the first and second elements, and ending + /// with the wrapped-difference between the next-to-last and last elements. + /// The result is empty if the receiver has less than two elements. + @inlinable + public func wrappedDifferences() -> DeltasSequence { + return lazy.deltas(via: &-) + } +} + +extension Sequence where Element: SIMD, Element.Scalar: FixedWidthInteger { + /// Differentiates this sequence into a lazy sequence formed by the vector + /// wrapped-differences between each pair of consecutive elements in order. + /// + /// This method uses `.lazy.deltas(via:)` with the closure being the `&-` + /// operator. + /// + /// let fibPairs: [SIMD2] = [[1, 1], [1, 2], [2, 3], [3, 5], [5, 8]] + /// print(Array(fibPairs.wrappedDifferences())) + /// // Prints "[SIMD2(0, 1), SIMD2(1, 1), SIMD2(1, 2), SIMD2(2, 3)]" + /// + /// - Returns: A lazy sequence containing the vector-differences, starting + /// with the wrapped-difference between the first and second elements, and + /// ending with the wrapped-difference between the next-to-last and last + /// elements. The result is empty if the receiver has less than two + /// elements. + @inlinable + public func wrappedDifferences() -> DeltasSequence { + return lazy.deltas(via: &-) + } +} + +extension Sequence where Element: Strideable { + /// Differentiates this sequence into a lazy sequence formed by the + /// strides between each pair of consecutive elements in order. + /// + /// This method uses `.lazy.deltas(via:)` with the closure being a call to the + /// `Strideable.distance(to:)` method. + /// + /// let fib = [1, 1, 2, 3, 5, 8, 13, 21, 34] + /// print(Array(fib.strides())) + /// // Prints "[0, 1, 1, 2, 3, 5, 8, 13]" + /// + /// - Returns: A lazy sequence containing the strides, starting with the + /// distance between the first and second elements, and ending with the + /// distance between the next-to-last and last elements. The result is + /// empty if the receiver has less than two elements. + @inlinable + public func strides() -> DeltasSequence { + return lazy.deltas() { $1.distance(to: $0) } + } +} diff --git a/Tests/SwiftAlgorithmsTests/DeltasTests.swift b/Tests/SwiftAlgorithmsTests/DeltasTests.swift new file mode 100644 index 00000000..2a41b356 --- /dev/null +++ b/Tests/SwiftAlgorithmsTests/DeltasTests.swift @@ -0,0 +1,145 @@ +//===----------------------------------------------------------------------===// +// +// 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 +@testable import Algorithms + +/// Unit tests for the `deltas()`, both eager and lazy; `differences()`; +/// `wrappedDifferences()`; and `strides()` methods. +final class DeltasTests: XCTestCase { + /// Check the differences for an empty source. + func testEmpty() { + let empty = EmptyCollection() + XCTAssertEqualSequences(empty.deltas(via: -), []) + + let emptyDeltas = empty.lazy.deltas(via: -) + XCTAssertEqual(emptyDeltas.underestimatedCount, 0) + XCTAssertEqualSequences(emptyDeltas, []) + + XCTAssertTrue(emptyDeltas.isEmpty) + } + + /// Check the differences for a single-element source. + func testSingle() { + let single = CollectionOfOne(1) + XCTAssertEqualSequences(single.deltas(via: -), []) + + let singleDeltas = single.lazy.deltas(via: -) + XCTAssertEqual(singleDeltas.underestimatedCount, 0) + XCTAssertEqualSequences(singleDeltas, []) + + XCTAssertTrue(singleDeltas.isEmpty) + } + + /// Check the differences for a two-element source. + func testDouble() { + let sample = [3, 12] + XCTAssertEqualSequences(sample.deltas(via: /), [4]) + + let sampleDeltas = sample.lazy.deltas(via: /) + XCTAssertEqual(sampleDeltas.underestimatedCount, 1) + XCTAssertEqualSequences(sampleDeltas, [4]) + + XCTAssertFalse(sampleDeltas.isEmpty) + XCTAssertEqual(sampleDeltas.count, 1) + XCTAssertEqualSequences(sampleDeltas.indices.lazy.map { sampleDeltas[$0] }, + [4]) + XCTAssertEqualSequences(sampleDeltas.indices.reversed().map { + sampleDeltas[$0] + }, [4]) + } + + /// Check the differences with longer sources. + func testMoreSequences() { + let repeats = repeatElement(5.0, count: 5) + XCTAssertEqualSequences(repeats.deltas(via: -), [0, 0, 0, 0]) + XCTAssertEqualSequences(repeats.deltas(via: /), [1, 1, 1, 1]) + + let repeatsSubDeltas = repeats.lazy.deltas(via: -), + repeatsDivDeltas = repeats.lazy.deltas(via: /) + XCTAssertEqual(repeatsSubDeltas.underestimatedCount, 4) + XCTAssertEqual(repeatsDivDeltas.underestimatedCount, 4) + XCTAssertEqualSequences(repeatsSubDeltas, [0, 0, 0, 0]) + XCTAssertEqualSequences(repeatsDivDeltas, [1, 1, 1, 1]) + + XCTAssertFalse(repeatsSubDeltas.isEmpty) + XCTAssertEqual(repeatsSubDeltas.count, 4) + XCTAssertEqualSequences(repeatsSubDeltas.indices.map { + repeatsSubDeltas[$0] + }, [0, 0, 0, 0]) + XCTAssertEqualSequences(repeatsSubDeltas.indices.reversed().map { + repeatsSubDeltas[$0] + }, [0, 0, 0, 0]) + + XCTAssertFalse(repeatsDivDeltas.isEmpty) + XCTAssertEqual(repeatsDivDeltas.count, 4) + XCTAssertEqualSequences(repeatsDivDeltas.indices.map { + repeatsDivDeltas[$0] + }, [1, 1, 1, 1]) + XCTAssertEqualSequences(repeatsDivDeltas.indices.reversed().map { + repeatsDivDeltas[$0] + }, [1, 1, 1, 1]) + + let fibonacci = [1, 1, 2, 3, 5, 8], + factorials = [1, 1, 2, 6, 24, 120, 720, 5040] + XCTAssertEqualSequences(fibonacci.deltas(via: -), [0, 1, 1, 2, 3]) + XCTAssertEqualSequences(factorials.deltas(via: /), 1...7) + + let fibonacciDeltas = fibonacci.lazy.deltas(via: -), + factorialDeltas = factorials.lazy.deltas(via: /) + XCTAssertEqual(fibonacciDeltas.underestimatedCount, 5) + XCTAssertEqual(factorialDeltas.underestimatedCount, 7) + XCTAssertEqualSequences(fibonacciDeltas, [0, 1, 1, 2, 3]) + XCTAssertEqualSequences(factorialDeltas, 1...7) + + XCTAssertFalse(factorialDeltas.isEmpty) + XCTAssertTrue(factorialDeltas[factorialDeltas.endIndex...].isEmpty) + XCTAssertEqualSequences(factorialDeltas.prefix(3), 1...3) + XCTAssertEqual(factorialDeltas.distance(from: 1, to: 4), 3) + XCTAssertNil(factorialDeltas.index(4, offsetBy: +100, + limitedBy: factorialDeltas.endIndex)) + XCTAssertNil(factorialDeltas.index(4, offsetBy: -100, + limitedBy: factorialDeltas.startIndex)) + } + + /// Check that `distance` works in both directions across the banned index. + func testMoreDistance() { + let sample = 0..<10, sampleDeltas = sample.lazy.deltas(via: -) + XCTAssertEqualSequences(sampleDeltas, repeatElement(1, count: 9)) + XCTAssertEqual(sampleDeltas.distance(from: 2, to: 10), +7) // Not +8 + XCTAssertEqual(sampleDeltas.distance(from: 10, to: 2), -7) // Not -8 + XCTAssertEqual(sampleDeltas.distance(from: 10, to: 10), 0) + XCTAssertEqual(sampleDeltas.distance(from: 3, to: 7), +4) + XCTAssertEqual(sampleDeltas.distance(from: 7, to: 3), -4) + } + + /// Check the protocol-customized overloads. + func testCustoms() { + let fibInt = [1, 1, 2, 3, 5, 8, 13] + XCTAssertEqualSequences(fibInt.differences(), [0, 1, 1, 2, 3, 5]) + XCTAssertEqualSequences(fibInt.wrappedDifferences(), [0, 1, 1, 2, 3, 5]) + XCTAssertEqualSequences(fibInt.strides(), [0, 1, 1, 2, 3, 5]) + + let fibDouble = fibInt.map(Double.init) + XCTAssertEqualSequences(fibDouble.differences(), [0, 1, 1, 2, 3, 5]) + XCTAssertEqualSequences(fibDouble.strides(), [0, 1, 1, 2, 3, 5]) + + let fibInts = zip(fibInt.dropLast(), fibInt.dropFirst()).map { + SIMD2($0.0, $0.1) + } + XCTAssertEqualSequences(fibInts.wrappedDifferences(), + [[0, 1], [1, 1], [1, 2], [2, 3], [3, 5]]) + + let fibDoubles = fibInts.map(SIMD2.init) + XCTAssertEqualSequences(fibDoubles.differences(), + [[0, 1], [1, 1], [1, 2], [2, 3], [3, 5]]) + } +}