How to extend String.Iterator in Swift

Viewed 140

I have a String like LINNIIBDDDN, basically a series of tokens. I'd like to use multiple iterators, one for each token type. I'd like to have each iterator ignore the tokens that don't belong to it. That is, I want to call something like next_ish(), to advance the iterator to the next element of its particular token. So if the Niterator is at index 3, and I call next_ish(), I want it to go to index 10, the next N, not the I at index 4. I have some code that already works, but it's a lot of code, it makes the String into an array, and I have subclassed iterators, basically hand-written, with no help from Swift, although I'm sure the Swift iterators are more stable and thoroughly tested. I'd rather use their code than mine, where possible.

It seems like it should be easy just to extend String.Iterator and add next_ish(), but I'm at a loss. My first naive attempt was to extend String.Iterator. I get the error Constrained extension must be declared on the unspecialized generic type 'IndexingIterator' with constraints specified by a 'where' clause. I went looking to figure out what kind of where clause to use, and I haven't found anything.

There are a lot of answers here on SO, about extending arrays and generics, pulling all the elements of a certain type into an array of their own, even some answers about specialized for...in loops, but I can't find anything about extending iterators. I've read through Collections.swift and haven't found anything helpful. Is it possible to extend String.Iterator? That would make my life a lot easier. If not, is there some sort of built-in Swift mechanism for doing this sort of thing?

1 Answers

String.Iterator is (implicitly) defined as

typealias Iterator = IndexingIterator<String>

and the error message

Constrained extension must be declared on the unspecialized generic type 'IndexingIterator' with constraints specified by a 'where' clause

means that we must define extension methods as

extension IndexingIterator where Elements == String { }

Alternatively (with increasing generality):

extension IndexingIterator where Elements: StringProtocol { }
extension IndexingIterator where Elements.Element == Character { }

I haven't found a way to access the underlying collection (or position) from within an extension method, the corresponding members are defined as “internal”:

public struct IndexingIterator<Elements : Collection> {
  internal let _elements: Elements
  internal var _position: Elements.Index
  // ...
}

What you can do is to pass the wanted element to your “next-ish” method, either as the element itself, or as a predicate:

extension IndexingIterator where Elements.Element == Character {

    mutating func next(_ wanted: Character) -> Character? {
        while let c = next() {
            if c == wanted { return c }
        }
        return nil
    }

    mutating func next(where predicate: ((Character) -> Bool)) -> Character? {
        while let c = next() {
            if predicate(c) { return c }
        }
        return nil
    }
}

Example usage:

var it1 = "ABCDABCE".makeIterator()
print(it1.next("C") as Any) // Optional("C")
print(it1.next() as Any)    // Optional("D")
print(it1.next("C") as Any) // Optional("C")
print(it1.next() as Any)    // Optional("E")
print(it1.next("C") as Any) // nil

var it2 = "LINnIIBDDDN".makeIterator()
while let c = it2.next(where: { "Nn".contains($0) }) {
    print(c, terminator: ", ")
}
print()
// N, n, N,

But actually I would consider String.Iterator being an IndexingIterator an implementation detail, and extend the IteratorProtocol instead:

extension IteratorProtocol where Element: Equatable {
    mutating func next(_ wanted: Element) -> Element? {
        while let e = next() {
            if e == wanted { return e }
        }
        return nil
    }
}

extension IteratorProtocol  {
    mutating func next(where predicate: ((Element) -> Bool)) -> Element? {
        while let e = next() {
            if predicate(e) { return e }
        }
        return nil
    }
}

That makes it usable for arbitrary sequences. Example:

var it3 = [1, 1, 2, 3, 5, 8, 13, 21, 34].makeIterator()
while let e = it3.next(where: { $0 % 2 == 0} ) {
    print(e, terminator: ", ")
}
print()
// 2, 8, 34,
Related