Swift で一次元配列を評価して二次元配列を生成する方法(自作高階関数)

  • 2
    Like
  • 0
    Comment

以前 Swiftで一次元配列を評価して二次元配列を生成する方法という記事を読みましたが、その時は既存の reduce 関数でどうにかしようと考えました。

ところで今日 Advanced Swift という本を読んで、map はこんな風に作られているよというような記述を見かけて、「そう言えば確かによくよく考えればこれグルーピングであってリデュースではないよな」って思って、じゃあ標準ライブラリーになければ自分でこれ作ればいいんじゃね?って思いました。

アプローチは非常に簡単で、配列を全部走査して、隣り合う要素を何らかの方法で同じグループに所属するかどうかを判断し、すれば同じグループにまとめるし、しなければ新しいグループ作ればいいです。

というわけで早速作ってみました:

Array.swift
extension Array {

    private var lastIndex: Index {
        return self.index(before: self.endIndex)
    }

    private var lastElement: Element {
        get {
            return self[lastIndex]
        }
        set {
            self[lastIndex] = newValue
        }
    }

    public func group(condition: (_ previous: Element, _ next: Element) throws -> Bool) rethrows -> [[Element]] {

        guard let first = self.first else {
            return [[]]
        }

        var groupedArray: [[Element]] = [[first]]

        for next in self.dropFirst() {
            let previous = groupedArray.lastElement.lastElement

            if try condition(previous, next) == true {
                groupedArray.lastElement.append(next)

            } else {
                groupedArray.append([next])
            }

        }

        return groupedArray

    }

}

通常の last 要素は Element? で返されるし get-only なので、ここでかなりの頻度で最後の要素を操作するから、privateget setlastElement: Element 変数を作りました。

そして配列に必ず最低でも 1 要素があることを保証し、そうでなければ空二次元配列を返します。
1 要素以上あれば、仮の groupedArray: [[Element]] を作ってとりあえずまず 1 個目の要素をぶちこんでおき、そして元配列の 2 番目以降の要素を走査しながら groupedArray の最後の要素と比較し、同じグループであれば最後のグループにこの要素を追加し、そうでなければ新しいブループを作って追加します、いたって単純。
同じグループであるかどうかの判断はここではせず、高階関数として condition を利用時に中身を定義してもらいます。

これで利用するときはグルーピングのルールを教えればグループされた二次元配列が作られます。

例えば、素数の数列を渡して、その中に十の桁が一致する数字をまとめたい、といった感じですと、

let primeNumbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
let groupedPrimeNumbers = primeNumbers.group { $0 / 10 == $1 / 10 }
print(groupedPrimeNumbers) //[[2, 3, 5, 7], [11, 13, 17, 19], [23, 29], [31, 37], [41, 43, 47]]

と、見事に 10 未満、10 以上で 20 未満、20 以上で 30 未満…といった感じにグルーピングしてくれました。めでたしめでたし。

============================追伸============================

せっかくなので Array だけでなくすべての Sequence に対応させてみました。

Collection.swift
extension Collection {

    public var lastIndex: Index {
        guard let lastIndex = self.index(self.endIndex, offsetBy: -1, limitedBy: self.startIndex) else {
            fatalError("Index out of range")
        }
        return lastIndex
    }

}

extension Collection {

    public var lastElement: Iterator.Element {
        get {
            return self[lastIndex]
        }
    }

}
MutableCollection.swift
extension MutableCollection {

    public var lastElement: Iterator.Element {
        get {
            return self[lastIndex]
        }
        set {
            self[lastIndex] = newValue
        }
    }

}
Sequence.swift
extension Sequence {

    public func group(condition: (_ previous: Iterator.Element, _ next: Iterator.Element) throws -> Bool) rethrows -> [[Iterator.Element]] {

        var iterator = self.makeIterator()
        guard let first = iterator.next() else {
            return [[]]
        }

        var groupedArray: [[Iterator.Element]] = [[first]]

        while let next = iterator.next() {
            let previous = groupedArray.lastElement.lastElement

            if try condition(previous, next) == true {
                groupedArray.lastElement.append(next)

            } else {
                groupedArray.append([next])
            }

        }

        return groupedArray

    }

}

lastIndex とか lastElement とかについて特に何も説明する必要ないと思いますが、group の実現のところで、前 Array で実現した際に使った for 文がなくなって、代わりに while let next = iterator.next() になりました。これは Sequence プロトコルの Iterator というもので、簡単に言うと中身を順番に取ってきてくれるやつです。詳しくは公式資料を参照すれば分かるかと。これで Array だけでなく、すべての Sequence 対応の型をグルーピングできるようになります。

ただぶっちゃけ MutableCollection の存在意義がわからない。setmutating なのでそれだけで判別できるから、なんで get-onlyCollectionget setMutableCollection があるのか。これじゃあまるで NSArrayNSMutableArray と変わらないじゃないか…と思ったがよくよく考えてみたら MutableCollection がなかったらコレクションの中身だけを変えるかそれともコレクション丸ごと変えるかを判別できないな確かに