🌇

順番つきの Set(要素が重複しない Array)な OrderedSet が Swift にやってきた

2021/04/07に公開

Summary

// 埼玉県 が2つある
let kantoArray = ["茨城県", "栃木県", "群馬県", "埼玉県", "埼玉県", "千葉県", "東京都", "神奈川県"]
// OrderedSet を使う
let kantoOrderedSet = OrderedSet(kantoArray)
// 要素の重複がなく順番が保証されたコレクション、OrderedSet
print(kantoOrderedSet) // [茨城県, 栃木県, 群馬県, 埼玉県, 千葉県, 東京都, 神奈川県]

関連

https://zenn.dev/treastrain/articles/e4bac83e2cc5a6

順番つきの Set(要素が重複しない Array)な OrderedSet

関東地方を北関東と南関東に分けるとき、埼玉県をどちらに所属させるかについていろいろあるそうです。
とりあえず、両方に埼玉県を入れておくことにします。

let northKanto = ["茨城県", "栃木県", "群馬県", "埼玉県"]
let southKanto = ["埼玉県", "千葉県", "東京都", "神奈川県"]

この2つの配列から関東を作ると埼玉県が重複してしまいます。

let kantoArray = northKanto + southKanto
print(kantoArray) // ["茨城県", "栃木県", "群馬県", "埼玉県", "埼玉県", "千葉県", "東京都", "神奈川県"]

Swift において要素が重複しない CollectionType に Set があります。これを用いて重複を取り除いてみます。

let kantoSet = Set(kantoArray)
print(kantoSet) // ["栃木県", "埼玉県", "千葉県", "群馬県", "神奈川県", "茨城県", "東京都"]

確かに埼玉県は1つになりましたが、Set は要素の順番を保証しないため、実行のたびに print(kantoSet) の出力結果が変わってしまいます。

しかし、ISO 3166-2:JP(JIS X 0401)に合わせて順番は保証したいところ…
順番つきの Set(要素が重複しない Array)を使いたいときは Collections にある OrderedSet を用います。

// Swift Package Manager で Collections を導入

import Collections // または import OrderedCollections

let kantoOrderedSet = OrderedSet(kantoArray)
print(kantoOrderedSet) // [茨城県, 栃木県, 群馬県, 埼玉県, 千葉県, 東京都, 神奈川県]

https://github.com/apple/swift-collections

NSOrderedSet ってあったよね

これまで順番つきの Set(要素が重複しない Array)を使いたいときは Objective-C のときからある NSOrderedSet を使うという手法がありました。しかし、NSOrderedSet はあくまで SetArray ではないため、それが欲しいときはそれぞれに合ったプロパティを用いて得る必要がありますが、これでは要素の型が Set<AnyHashable>[Any] になってしまいます。

import Foundation

let kantoNSOrderedSet = NSOrderedSet(array: kantoArray)

print(type(of: kantoNSOrderedSet)) // __NSOrderedSetI
print(type(of: kantoNSOrderedSet.array)) // Array<Any>
print(type(of: kantoNSOrderedSet.set)) // Set<AnyHashable>

しかし、CollectionsOrderedSetHashable に準拠した要素の型を保持しています。

print(type(of: kantoOrderedSet)) // OrderedSet<String>
print(type(of: kantoOrderedSet.elements)) // Array<String>

OrderedSetSetArray に変換しなくても柔軟に使える

OrderedSet はほとんどの SetAlgebra に対応するため、そもそも SetArray に変換しなくても、NSOrderedSet のときより柔軟に取り扱うことができます。

let kantoOrderedSetSortedByUnicode: OrderedSet = ["千葉県", "埼玉県", "埼玉県", "東京都", "栃木県", "神奈川県", "群馬県", "茨城県"]
print(kantoOrderedSet == kantoOrderedSetSortedByUnicode) // false
kantoOrderedSet.sort() // kantoOrderedSet を let から var へ変更
print(kantoOrderedSet == kantoOrderedSetSortedByUnicode) // true

print(kantoOrderedSet.contains("群馬県")) // true
print(kantoOrderedSet.contains("山梨県")) // false

OrderedSet への要素の挿入・追加は (inserted: Bool, index: Int) のタプルが返ってくる

OrderedSet への要素の挿入や追加で用いる insert(_:at:)append(_:) メソッドは (inserted: Bool, index: Int) のタプルが返されます。OrderedSet にすでに挿入・追加したい要素が含まれる場合は insertedfalse となります。

var greaterTokyoArea = OrderedSet(kantoArray)
print(greaterTokyoArea) // [茨城県, 栃木県, 群馬県, 埼玉県, 千葉県, 東京都, 神奈川県]

let result1 = greaterTokyoArea.append("山梨県")
print(result1.inserted, result1.index) // true 7

let result2 = greaterTokyoArea.append("東京都")
print(result2.inserted, result2.index) // false 5

let removedElement = greaterTokyoArea.removeLast()
print(removedElement) // 山梨県

添字によるアクセス

OrderedSet のまま、Array のように添字でもアクセスできます。

for index in 0..<greaterTokyoArea.count {
    print("都道府県コード: \(index + 8)", greaterTokyoArea[index])
}
// 都道府県コード: 8 茨城県
// 都道府県コード: 9 栃木県
// 都道府県コード: 10 群馬県
// 都道府県コード: 11 埼玉県
// 都道府県コード: 12 千葉県
// 都道府県コード: 13 東京都
// 都道府県コード: 14 神奈川県

参考文献

Discussion