以前 Swiftで一次元配列を評価して二次元配列を生成する方法という記事を読みましたが、その時は既存の reduce
関数でどうにかしようと考えました。
ところで今日 Advanced Swift という本を読んで、map
はこんな風に作られているよというような記述を見かけて、「そう言えば確かによくよく考えればこれグルーピングであってリデュースではないよな」って思って、じゃあ標準ライブラリーになければ自分でこれ作ればいいんじゃね?って思いました。
アプローチは非常に簡単で、配列を全部走査して、隣り合う要素を何らかの方法で同じグループに所属するかどうかを判断し、すれば同じグループにまとめるし、しなければ新しいグループ作ればいいです。
というわけで早速作ってみました:
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
なので、ここでかなりの頻度で最後の要素を操作するから、private
な get set
の lastElement: 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
に対応させてみました。
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]
}
}
}
extension MutableCollection {
public var lastElement: Iterator.Element {
get {
return self[lastIndex]
}
set {
self[lastIndex] = newValue
}
}
}
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
の存在意義がわからない。set
は mutating
なのでそれだけで判別できるから、なんで get-only
の Collection
と get set
の MutableCollection
があるのか。これじゃあまるで NSArray
と NSMutableArray
と変わらないじゃないか…MutableCollection
がなかったらコレクションの中身だけを変えるかそれともコレクション丸ごと変えるかを判別できないな確かに