[Swift] 複数のOR条件をAND検索してArrayをフィルタリングする

検証環境

本エントリは以下の環境で検証を行っています。

  • macOS Sierra バージョン 10.12.6
  • Version 9.2 (9C40b)
  • Swift 4

Swiftのfilter便利ですよね

SwiftでArrayから特定の条件に合致したものだけ抽出する場合、filter使いますよね。

ごく簡単なFilterの使い方として、例えば以下のUserというstructとArrayを定義したとします。

struct User {
    /// 名
    let firstName: String
    /// 姓
    let lastName: String
}

let users = [
    User(firstName: "Jun", lastName: "Kato"),
    User(firstName: "Taro", lastName: "Kato"),
    User(firstName: "Hanako", lastName: "Nakamura"),
    User(firstName: "Hanako", lastName: "Yoshida"),
    User(firstName: "Taichi", lastName: "Honda"),
]

このようなArrayがあるとき、検索ワードが1つで、姓名どちらかに検索ワードが含まれているUserを抽出(OR検索)したい場合、 以下のようにfilterでOR条件を指定するだけです。簡単ですね。

// 以下は姓名どちらかに"Kato"が含まれるUserを抽出しています。
let searchWord = "Kato"
let filteredUsers = users.filter {
    $0.firstName.contains(searchWord) || $0.lastName.contains(searchWord)
}

結果はこちら。ちゃんと意図通り抽出できていますね。

[__lldb_expr_103.User(firstName: "Jun", lastName: "Kato"), __lldb_expr_103.User(firstName: "Taro", lastName: "Kato")]

複数のOR条件をAND検索したい

さて、本題です。 前述したOR条件が複数あってそれを使ってAND検索したい場合どうすればいいでしょうか。
具体的には、検索ワードが2つあって、どちらの検索ワードも姓名どちらかにヒットする といった場合です。

Objective-C時代からあるNSPredicateを使えばできそうですが、条件を文字列で指定することになるためイケテないですね。 そしてせっかくSwiftで書いているならタイプセーフに書きたいと思うはずです。

Swiftの場合は以下のようにfilterとreduceを組み合わせることでそれが実現できます。

// 検索ワードが2つで、どちらの検索ワードも姓名どちらかにヒットする(OR検索条件をANDで検索)
let searchWords = ["Kato", "Taro"]
var conditions = [(User) -> Bool]()

for word in searchWords {
    conditions.append { $0.firstName.contains(word) || $0.lastName.contains(word) }
}

let filteredUsers = users.filter { user in
    conditions.reduce(true) { $0 && $1(user) }
}

結果はこちら。ちゃんと検索できてますね。

[__lldb_expr_193.User(firstName: "Taro", lastName: "Kato")]

OR条件のクロージャーをArrayとしておき、filterするときにOR条件をreduceによって畳み込むことでAND検索しています。 ポイントはreduceの初期値をtrueにすることでしょうか。ANDで検索しているので当たり前ですが、falseにしてしまうと必ず条件に合致しなくなってしまうためです。

おわりに

今回はSwiftのfilterとreduceを組み合わせることでArrayを複数のOR条件をAND検索してフィルタリングする方法をご紹介しました。 わかってしまえばなんてことはないのですが、私はすぐに実装が思い浮かばなかったのでブログとして残しておくことにしました。
どなたかの参考になれば幸いです。