-
Notifications
You must be signed in to change notification settings - Fork 446
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
Conversation
Sources/Algorithms/Intersperse.swift
Outdated
public static func < (lhs: Index, rhs: Index) -> Bool { | ||
if lhs.index < rhs.index { return true } | ||
if lhs.index > rhs.index { return false } | ||
if lhs.kind == .element, rhs.kind == .separator { return true } | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks correct, but if you conform Kind
to Comparable
(which will just correctly use the case order I think?) then this becomes as simple as (lhs.index, lhs.kind) < (rhs.index, rhs.kind)
Sources/Algorithms/Intersperse.swift
Outdated
public func index(after i: Index) -> Index { | ||
switch i.kind { | ||
case .element where base.index(after: i.index) == base.endIndex: | ||
return endIndex | ||
case .element: | ||
return Index(index: i.index, kind: .separator) | ||
case .separator: | ||
return Index(index: base.index(after: i.index), kind: .element) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When traversing the Intersperse
indices, each index from the base collection is computed twice: first when the kind of i
is .element
, and then when the kind is .separator
. Maybe we should have the .separator
case store the next index, similar to how you solved this problem in Iterator.State
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although your explanation is totally coherent, I'm not sure I follow completely. Is the computation you're describing base.index(after: i.index)
? Given that i
is Intersperse.Index
, its index
property it holds is not recomputed each time, so should be fine?
In my head, I can vaguely see what you're suggesting. Perhaps I just need to sit with a piece of paper for a few minutes!
ebe8e23
to
0b7a16d
Compare
These last couple of commits break |
I think the index comparison can be like: 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(lr), .element(rr)),
let (.separator(lr), .separator(rr)),
let (.element(lr), .separator(rr)):
return lr < rr
case let (.separator(lr), .element(rr)):
return lr <= rr
}
}
} The inner index usually cascades, except when comparing an element and separator at the same inner index. Then you need to remember that the separator version is ordered less. |
I'm guesstimating, but you probably can make the |
Here's another possibility, letting tuple comparison + synthesized public struct Index: Comparable {
enum Kind: Comparable {
case separator
case element
}
let base: Base.Index
let kind: Kind
public static func < (lhs: Index, rhs: Index) -> Bool {
(lhs.base, lhs.kind) < (rhs.base, rhs.kind)
}
} Putting the |
806341a
to
65e2698
Compare
I added a test utility to check the order of a |
65e2698
to
7b2301b
Compare
@CTMacUser: I've added the conformance to This was tricky, so I also added another assert function to make sure the distances are calculated correctly and I've put this in the TestUtilities for tests on other collections to make use of. |
Both |
I'll add them as TODOs, cheers. Just taking a gander at your test function and indeed it looks to do the same thing as I was trying to achieve with those two asserts and a little more which is great! One thing I found in writing mine was that it was useful to take into account the |
It does take into account |
… is a BidirectionalCollection
This happens when you come back around to a separator, it recalculates the base sequence’s next index. Storing it in the separator enum allows it to be cached for use next time around. Cheers to Tim Vermeulen for pointing this out.
… case doesn’t use it This moves the property to be an associated value on the element case so that we can provide different names when we switch over the values.
This tests shows how the implementation of Comparable for Intersperse.Index is incorrect.
…set of test indices
5d9aacf
to
ac9848c
Compare
Using I also implemented |
case let .element(index): | ||
return .separator(next: base.index(after: index)) | ||
case let .separator(next): | ||
return .element(next) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
let interspersed = "".interspersed(with: "-") | ||
XCTAssertEqualSequences(interspersed, "") | ||
validateIndexTraversals(interspersed) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 🙂
… index(_:offsetBy:) and distance(from:to:) Another suggestion from @timvermeulen. :)
…r before the startIndex
3affe04
to
6f6768b
Compare
It's worth considering what cross-over this implementation has with let a = [1, 2, 3, 4, 5]
a.slidingWindows(ofCount: 1).joined(separator: [0]) // 1, 0, 2, 0, 3, 0, 4, 0, 5 extension Collection {
public func interspersed(with separator: Element) -> JoinedSequence<SlidingWindows<Self>> {
return slidingWindows(ofCount: 1).joined(separator: [separator])
}
} |
@ollieatkinson: That's true yeah! One niggle is this is that |
You are correct Both |
@ollieatkinson You don't need the sliding windows adaptor to do this with composition: let a = [1, 2, 3, 4, 5]
a.lazy.map { CollectionOfOne($0) }.joined(separator: CollectionOfOne(0)) Neither of those would allow you to implement |
Given this, what is the next step to getting this merged in? Should I squash the commits down and rebase them back onto the main branch? |
@danielctull All set here, thanks! |
Description
This addition allows the caller to insert values between each element of a sequence. In this way you could have a string and insert a hyphen between each character or you could have an array of SwiftUI views and insert a separator view between each.
Detailed Design
To achieve this, a new sequence type called Intersperse is added to the library. It also conforms to Collection and BidirectionalCollection when the base sequence conforms to those respective protocols.
For ease of use, a new method is added to sequence to allow the interspersing of the separator element:
It is crucial that the separator is only inserted between elements, so before returning the separator element, a lookup is performed on the base sequence to make sure the next element exists.
Documentation Plan
As the
interspersed(with:)
method is the main entry point, this is documented with two examples of use which are taken from the included unit tests. TheIntersperse
sequence type itself is lightly documented, which I believe follows the trend of the current APIs in the library.Test Plan
Arrays of numbers and a String are used, calling
interspersed(with:)
on each, a value between the elements of each. I have also tested that the empty sequence case yields an empty sequence.Source Impact
This is a purely additive change.
Checklist