Skip to content

Instantly share code, notes, and snippets.

@benjaminmayo
Last active March 1, 2020 19:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjaminmayo/734812a8f1e437f98d08381817bc38a2 to your computer and use it in GitHub Desktop.
Save benjaminmayo/734812a8f1e437f98d08381817bc38a2 to your computer and use it in GitHub Desktop.
This is an example implementation of the zip sparse pairs algorithm in Swift. The sequence iterates through two sorted sequences of elements, which are then paired according to some supplied `predicate`. The sequence returns a stream of `PossiblePair` instances.
struct ZipSparsePairsSequence<A: Sequence, B: Sequence> : Sequence {
private var primary: A
private var subset: B
private let predicate: PairingPredicate
fileprivate init(primary: A, subset: B, pairingWith predicate: @escaping PairingPredicate) {
self.primary = primary
self.subset = subset
self.predicate = predicate
}
func makeIterator() -> ZipSparsePairsSequence<A, B>.Iterator {
return Iterator(
primaryIterator: self.primary.makeIterator(),
subsetIterator: self.subset.makeIterator(),
pairingWith: self.predicate
)
}
typealias PairingPredicate = (A.Element, B.Element) -> Bool
enum PossiblePair {
case paired(A.Element, B.Element)
case missingPair(for: A.Element)
var elements: (A.Element, B.Element?) {
switch self {
case .paired(let a, let b):
return (a, b)
case .missingPair(for: let a):
return (a, nil)
}
}
}
}
extension ZipSparsePairsSequence {
struct Iterator : IteratorProtocol {
private var primaryIterator: A.Iterator
private var subsetIterator: B.Iterator
private let predicate: PairingPredicate
private var currentSubsetElement: B.Element?
fileprivate init(primaryIterator: A.Iterator, subsetIterator: B.Iterator, pairingWith predicate: @escaping PairingPredicate) {
self.primaryIterator = primaryIterator
self.subsetIterator = subsetIterator
self.predicate = predicate
}
mutating func next() -> PossiblePair? {
// This algorithm looks complicated at a glance, but it can be reasoned about by thinking about two stacks of playing cards, Hearts and Spades, in order. Each stack is ordered but there may be gaps. The values of Spades must be a subset of the values of Hearts.
// Place the two stacks face down. Turning over a card is analogous to calling `next()` on the iterator.
// Make a space to the side which can store a single Spades card, or nothing. This is analogous to `self.currentSubsetElement`.
// Repeat the algorithm like so:
// If there is a card set aside (which there will never be on the first iteration) turn over the top card of the Hearts stack.
// Compare the two cards. If their values match, pair them and set them down together into a pile. This pile is analogous to returning a `PossiblePair`.
// If the cards do not match, place the Heart-suited card down into the pile and keep the Spades card aside.
// If there was not a card set aside, turn over a new Hearts and Spades cards from the top of each stack.
// Compare the two cards. If the values match, pair them and put them into the return pile.
// If the values do not match, set the Spades card aside for later, and place the Heart-suited into the pile.
// Repeat the above until there are no more cards in the Spades stack.
// If there are Hearts left in the stack, then set them into the return pile as if there was not a match.
// With the Hearts stack now empty, the sequence terminates.
// I didn't invent the underlying procedure but I can't remember what it is called and Google searches failed me.
// I implemented into Swift by playing with two physical stacks of Hearts and Spades and then translating those actions into code.
// The visual characteristic and immediate repeatability of the physical demonstration helped to ensure I was covering every case appropriately. Once I started programming, I implemented the algorithm correctly — first time.
if let currentSubsetElement = self.currentSubsetElement {
if let primaryElement = self.primaryIterator.next() {
if self.predicate(primaryElement, currentSubsetElement) {
self.currentSubsetElement = nil
return .paired(primaryElement, currentSubsetElement)
} else {
return .missingPair(for: primaryElement)
}
}
} else if let subsetElement = self.subsetIterator.next() {
if let primaryElement = self.primaryIterator.next() {
if self.predicate(primaryElement, subsetElement) {
return .paired(primaryElement, subsetElement)
} else {
self.currentSubsetElement = subsetElement
return .missingPair(for: primaryElement)
}
}
} else if let primaryElement = self.primaryIterator.next() {
return .missingPair(for: primaryElement)
}
return nil
}
}
}
/// A sequence composed of two sorted sequences, where elements in both sequences can be paired together according to some property. The `first` sequence must contain every element of the `second`; that is to say, the `second` sequence is a subset of `first`.
/// - Parameters:
/// - first: The first sequence or collection to pair and zip.
/// - second: The second sequence or collection to pair and zip. The `second` sequence is a subset of the `first` sequence.
/// - predicate: A comparator that determines whether a given `A.Element` should be paired with a given `B.Element`.
/// - Returns: A sequence of `PossiblePair` values, where the elements of the `first` sequence are always vended, and accordingly paired with a corresponding element from the `second` sequence if it exists.
func zipSparsePairs<A: Sequence, B: Sequence>(pairingSorted first: A, withSortedSubset second: B, using predicate: @escaping ZipSparsePairsSequence<A, B>.PairingPredicate) -> ZipSparsePairsSequence<A, B> {
return ZipSparsePairsSequence(primary: first, subset: second, pairingWith: predicate)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment