Skip to content

Add interspersed(with:) #35

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

Merged
merged 17 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
737aca5
Add interspersed(with:) method
danielctull Oct 12, 2020
4440add
Conform Intersperse to Collection when the base sequence is a Collection
danielctull Oct 13, 2020
c4647c9
Conform Intersperse to BidirectionalCollection when the base sequence…
danielctull Oct 13, 2020
206bc1e
Use tuple comparison to simplify the Comparable implementation for In…
danielctull Oct 26, 2020
14629c8
Prevent the double calculation of the base’s next index
danielctull Oct 26, 2020
b2b6673
Remove the index property for Intersperse.Index because the separator…
danielctull Oct 26, 2020
1b5e3ec
Improve readability by using static functions to create Index values
danielctull Oct 26, 2020
f455043
Add failing test for ordered indices
danielctull Oct 28, 2020
eb28264
Fix issues when comparing values of Intersperse.Index
danielctull Oct 28, 2020
5cdbf8b
Add custom implementation for Intersperse.distance(from:to:)
danielctull Oct 29, 2020
fe6d16e
Add conformance to RandomAccessCollection on Intersperse
danielctull Oct 29, 2020
ca1a82d
Remove the need for a second iteration by adding the endIndex to the …
danielctull Oct 29, 2020
172332e
Use validateIndexTraversals to fix issues with the handling of the en…
danielctull Oct 31, 2020
ac9848c
Add implementation of Intersperse.index(_:offsetBy:)
danielctull Oct 31, 2020
faf4755
Use the separator case as the endIndex to improve the calculations of…
danielctull Nov 1, 2020
6f6768b
Add a precondition to prevent returning an index after the endIndex o…
danielctull Nov 2, 2020
d4bdbc4
Add intersperse test for empty and non-empty sequences
danielctull Nov 2, 2020
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
57 changes: 57 additions & 0 deletions Guides/Intersperse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Intersperse

[[Source](https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Intersperse.swift) |
[Tests](https://github.com/apple/swift-algorithms/blob/main/Tests/SwiftAlgorithmsTests/IntersperseTests.swift)]

Place a given value in between each element of the sequence.

```swift
let numbers = [1, 2, 3].interspersed(with: 0)
// Array(numbers) == [1, 0, 2, 0, 3]

let letters = "ABCDE".interspersed(with: "-")
// String(letters) == "A-B-C-D-E"

let empty = [].interspersed(with: 0)
// Array(empty) == []
```

`interspersed(with:)` takes a separator value and inserts it in between every
element in the sequence.

## Detailed Design

A new method is added to sequence:

```swift
extension Sequence {
func interspersed(with separator: Element) -> Intersperse<Self>
}
```

The new `Intersperse` type represents the sequence when the separator is
inserted between each element. Intersperse conforms to Collection and
BidirectionalCollection when the base sequence conforms to Collection and
BidirectionalCollection respectively.

### Complexity

Calling these methods is O(_1_).

### Naming

This method’s and type’s name match the term of art used in other languages
and libraries.

### Comparison with other languages

**[Haskell][Haskell]:** Has an `intersperse` function which takes an element
and a list and 'intersperses' that element between the elements of the list.

**[Rust][Rust]:** Has a function called `intersperse` to insert a particular
value between each element.

<!-- Link references for other languages -->

[Haskell]: https://hackage.haskell.org/package/base-4.14.0.0/docs/Data-List.html#v:intersperse
[Rust]: https://docs.rs/itertools/0.9.0/itertools/trait.Itertools.html#method.intersperse
188 changes: 188 additions & 0 deletions Sources/Algorithms/Intersperse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A sequence that presents the elements of a base sequence of elements
/// with a separator between each of those elements.
public struct Intersperse<Base: Sequence> {
let base: Base
let separator: Base.Element
}

extension Intersperse: Sequence {
/// The iterator for an `Intersperse` sequence.
public struct Iterator: IteratorProtocol {
var iterator: Base.Iterator
let separator: Base.Element
var state = State.start

enum State {
case start
case element(Base.Element)
case separator
}

public mutating func next() -> Base.Element? {
// After the start, the state flips between element and separator. Before
// returning a separator, a check is made for the next element as a
// separator is only returned between two elements. The next element is
// stored to allow it to be returned in the next iteration.
switch state {
case .start:
state = .separator
return iterator.next()
case .separator:
guard let next = iterator.next() else { return nil }
state = .element(next)
return separator
case .element(let element):
state = .separator
return element
}
}
}

public func makeIterator() -> Intersperse<Base>.Iterator {
Iterator(iterator: base.makeIterator(), separator: separator)
}
}

extension Intersperse: Collection where Base: Collection {
public struct Index: Comparable {
enum Representation: Equatable {
case element(Base.Index)
case separator(next: Base.Index)
}
let representation: Representation

public static func < (lhs: Index, rhs: Index) -> Bool {
switch (lhs.representation, rhs.representation) {
case let (.element(li), .element(ri)),
let (.separator(next: li), .separator(next: ri)),
let (.element(li), .separator(next: ri)):
return li < ri
case let (.separator(next: li), .element(ri)):
return li <= ri
}
}

static func element(_ index: Base.Index) -> Self {
Self(representation: .element(index))
}

static func separator(next: Base.Index) -> Self {
Self(representation: .separator(next: next))
}
}

public var startIndex: Index {
base.startIndex == base.endIndex ? endIndex : .element(base.startIndex)
}

public var endIndex: Index {
.separator(next: base.endIndex)
}

public func index(after i: Index) -> Index {
precondition(i != endIndex, "Can't advance past endIndex")
switch i.representation {
case let .element(index):
return .separator(next: base.index(after: index))
case let .separator(next):
return .element(next)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns an invalid index for c.index(after: c.endIndex) — can you add a precondition for that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, index(before:) also had the same issue. Implemented in 6f6768b.

}
}

public subscript(position: Index) -> Element {
switch position.representation {
case .element(let index): return base[index]
case .separator: return separator
}
}

public func index(_ i: Index, offsetBy distance: Int) -> Index {
switch (i.representation, distance.isMultiple(of: 2)) {
case (let .element(index), true):
return .element(base.index(index, offsetBy: distance / 2))
case (let .element(index), false):
return .separator(next: base.index(index, offsetBy: (distance + 1) / 2))
case (let .separator(next: index), true):
return .separator(next: base.index(index, offsetBy: distance / 2))
case (let .separator(next: index), false):
return .element(base.index(index, offsetBy: (distance - 1) / 2))
}
}

// TODO: Implement index(_:offsetBy:limitedBy:)

public func distance(from start: Index, to end: Index) -> Int {
switch (start.representation, end.representation) {
case let (.element(element), .separator(next: separator)):
return 2 * base.distance(from: element, to: separator) - 1
case let (.separator(next: separator), .element(element)):
return 2 * base.distance(from: separator, to: element) + 1
case let (.element(start), .element(end)),
let (.separator(start), .separator(end)):
return 2 * base.distance(from: start, to: end)
}
}
}

extension Intersperse: BidirectionalCollection
where Base: BidirectionalCollection
{
public func index(before i: Index) -> Index {
precondition(i != startIndex, "Can't move before startIndex")
switch i.representation {
case let .element(index):
return .separator(next: index)
case let .separator(next):
return .element(base.index(before: next))
}
}
}

extension Intersperse: RandomAccessCollection
where Base: RandomAccessCollection {}

extension Sequence {

/// Returns a sequence containing elements of this sequence with the given
/// separator inserted in between each element.
///
/// Any value of the sequence's element type can be used as the separator.
///
/// ```
/// for value in [1,2,3].interspersed(with: 0) {
/// print(value)
/// }
/// // 1
/// // 0
/// // 2
/// // 0
/// // 3
/// ```
///
/// The following shows a String being interspersed with a Character:
/// ```
/// let result = "ABCDE".interspersed(with: "-")
/// print(String(result))
/// // "A-B-C-D-E"
/// ```
///
/// - Parameter separator: Value to insert in between each of this sequence’s
/// elements.
/// - Returns: The interspersed sequence of elements.
///
/// - Complexity: O(1)
public func interspersed(with separator: Element) -> Intersperse<Self> {
Intersperse(base: self, separator: separator)
}
}
60 changes: 60 additions & 0 deletions Tests/SwiftAlgorithmsTests/IntersperseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
//
// 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

final class IntersperseTests: XCTestCase {
func testSequence() {
let interspersed = (1...).prefix(5).interspersed(with: 0)
XCTAssertEqualSequences(interspersed, [1,0,2,0,3,0,4,0,5])
}

func testSequenceEmpty() {
let interspersed = (1...).prefix(0).interspersed(with: 0)
XCTAssertEqualSequences(interspersed, [])
}

func testString() {
let interspersed = "ABCDE".interspersed(with: "-")
XCTAssertEqualSequences(interspersed, "A-B-C-D-E")
validateIndexTraversals(interspersed)
}

func testStringEmpty() {
let interspersed = "".interspersed(with: "-")
XCTAssertEqualSequences(interspersed, "")
validateIndexTraversals(interspersed)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test that validates against empty and non-empty sequences? (0...).prefix(_) is an easy way to create something that will call through to sequence-based iteration.

Copy link
Contributor Author

@danielctull danielctull Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah that makes sense, so that it doesn't go through any of the Collection code? If I've understood correctly, this is in d4bdbc4. 🙂


func testArray() {
let interspersed = [1,2,3,4].interspersed(with: 0)
XCTAssertEqualSequences(interspersed, [1,0,2,0,3,0,4])
validateIndexTraversals(interspersed)
}

func testArrayEmpty() {
let interspersed = [].interspersed(with: 0)
XCTAssertEqualSequences(interspersed, [])
validateIndexTraversals(interspersed)
}

func testCollection() {
let interspersed = ["A","B","C","D"].interspersed(with: "-")
XCTAssertEqual(interspersed.count, 7)
}

func testBidirectionalCollection() {
let reversed = "ABCDE".interspersed(with: "-").reversed()
XCTAssertEqualSequences(reversed, "E-D-C-B-A")
validateIndexTraversals(reversed)
}
}