🐈

Swiftで要素数が異なる2つの配列を漏れなくzipする

2022/08/30に公開

モチベーション

以下のような2つの文字列が用意されていて、Pairの要素毎に結合した一つの文字列の生成をしたいケースがありました。

let a = "1234567"
let b = "890"

...

>> 1829304567

今回はSwift上で同様の処理が行いたいので、どのように実装するか検証してみました。

Swiftのzip仕様

Swiftにはzipメソッドが用意されていて、これはPairのSequenceを生成してくれます。

https://developer.apple.com/documentation/swift/zip(::)

しかしこの機能には一つ落とし穴があり、要素数が異なる場合少ない方に合わせるという仕様が存在します。
そのため、そのままzipを使うと一部の要素が抜け落ちるといった問題が発生します。

zip(a, b).reduce(into: "") { $0 += String($1.0) + String($1.1) }
>> 182930

ドキュメントにもきっちり記載があります。

If the two sequences passed to zip(::) are different lengths, the resulting sequence is the same length as the shorter sequence. In this example, the resulting array is the same length as words

解決策

現状では標準メソッドだけの対応では難しそうなので、zipをベースに新たにSequence/Iterator Protocolに準拠した Zip2SequenceAllzipAll を作ることで解決しました。

主に実装が必要な部分は、次の要素を返す next() メソッドです。

このメソッドの中で何らかの値を返却するとループの処理が進み、nilを返却すると配列の終端に到達したものとして処理が終了する仕組みです。
今回はa, bどちらかの値が存在していれば処理を続行し、どちらの値ものnilの場合はnilを返して処理を終了させることで要素数が異なる場合でも最後までループできるような仕組みにしました。

func zipAll<A: Sequence, B: Sequence>(_ a: A, _ b: B) -> Zip2SequenceAll<A, B> {
    Zip2SequenceAll(a, b)
}

struct Zip2SequenceAll<A: Sequence, B: Sequence>: Sequence, IteratorProtocol {
    private var a: A.Iterator
    private var b: B.Iterator
    
    init (_ a: A, _ b: B) {
        self.a = a.makeIterator()
        self.b = b.makeIterator()
    }
    
    mutating func next() -> (A.Element?, B.Element?)? {
        let a = a.next()
        let b = b.next()
        
        guard a != nil || b != nil else {
            return nil
        }
        
        return (a, b)
    }
}
zipAll(a, b).reduce(into: "") { (result, char) in
    if let charA = char.0 {
        result += String(charA)
    }
    
    if let charB = char.1 {
        result += String(charB)
    }
}

>> 1829304567

FAQ

2つの配列で同じ位置にnilを含む場合、要素の途中で処理が終了してしまわないのか?

具体的にいうと以下のようなケース。
現状の実装ではa, bそれぞれnilなら処理を終了するという実装になっているので、nil以降の3, 6, 7が出力されないのではという疑問点がありました。

let a = ["1", "2", nil, "3"]
let b = ["4", "5", nil, "6", "7"]

これに関しては問題なく動作します。というのも、a.next()/b.next() の返す値を確認するとわかるのですが、要素がnil・存在する場合は Optional(nil) とOptionalでラップされており、要素切れの場合は nil で返ってきます。

Optional(Optional("1")) / Optional(Optional("4"))
Optional(Optional("2")) / Optional(Optional("5"))
Optional(nil) / Optional(nil)
Optional(Optional("3")) / Optional(Optional("6"))
nil / Optional(Optional("7"))
nil / nil

まぁ要素に合わせてそのままnilを返していたら処理が進まなくて使い物にならないのでそれはそうという感じっすな。

参考

Discussion