LINQで集合の交わり・結び・差

こんな表を作った。(TablePressでJavaScriptのDataTablesライブラリというのを有効にしてるので、表を並べ替えたり値を検索してフィルタリングしたりできます。いじってみてください。)

料理材料
カレー
カレーじゃがいも
すき焼き
すき焼き
コロッケ
コロッケじゃがいも
コロッケ
ポテトサラダじゃがいも
ポテトサラダ

ポテトサラダに卵!? という人もいるかもしれないが、私はゆで卵をつぶして入れてるやつが好きだからいいのだ。それにマヨネーズには卵が入ってるのだし…。

今回は、この表から材料を指定して料理を選ぶというのをLINQを使ってやってみる。

準備

まず、データを用意する。

//料理とその材料をペアにして保持するクラス
class Food
{
    public string Dish; //料理
    public string Stuff; //材料
}

//データの配列
Food[] menu = new Food[]
{
    new Food() { Dish ="カレー", Stuff="肉"},
    new Food() { Dish ="カレー", Stuff="じゃがいも"},
    new Food() { Dish = "すき焼き", Stuff = "肉" },
    new Food() { Dish = "すき焼き", Stuff = "卵" },
    new Food() { Dish = "コロッケ", Stuff = "肉" },
    new Food() { Dish = "コロッケ", Stuff = "じゃがいも" },
    new Food() { Dish = "コロッケ", Stuff = "卵" },
    new Food() { Dish = "ポテトサラダ", Stuff = "じゃがいも" },
    new Food() { Dish = "ポテトサラダ", Stuff = "卵" }
};

材料を一つ指定して抽出

「肉を使った料理」を抽出する。

//肉を使った料理を抽出
IEnumerable<string> foods1 =
    menu
    .Where(x => x.Stuff == "肉")
    .Select(x => x.Dish);

//抽出結果を表示
Console.WriteLine("肉を使った料理:");
foreach (string s in foods1)
{
    Console.WriteLine(s);
}

出力
肉を使った料理:
カレー
すき焼き
コロッケ

まぁ、ひとつの条件でできるのは当たり前だ。基本だし。

二つの材料をどちらも使っているのを抽出する(交わり)

では、二つの条件を同時に満たす場合を考えてみる。まずは単純に…

//肉とじゃがいもを使った料理【悪い例】
IEnumerable<string> foods2 =
    menu
    .Where(x => x.Stuff == "肉" && x.Stuff == "じゃがいも")
    .Select(x => x.Dish);

肉とじゃがいもを使った料理:

何も出てこない。表を見ればわかるが、材料列に肉とじゃがいもを両方書いている料理はない。このデータの場合、単純にWhere内で条件を追加しても「そんなデータはない」という結果になってしまう。ではどうするか?

肉を使った料理とじゃがいもを使った料理それぞれを抽出して、どちらにも名前の挙がった料理を選べばいい。つまり(肉料理)∩(じゃがいも料理)(集合の交わり)を出せばいいわけだ。

抽出結果の交わりを求める場合、IEnumerable<T>の拡張メソッド「Intersect」を使う。

//肉とじゃがいもを使った料理【良い例】
IEnumerable<string> foods2 =
    menu
    .Where(x => x.Stuff == "肉")
    .Select(x => x.Dish)
    .Intersect(
        menu
        .Where(x => x.Stuff == "じゃがいも")
        .Select(x => x.Dish));

肉とじゃがいもを使った料理:
カレー
コロッケ

「Intersect」メソッドはIEnumerable<T>なオブジェクトに付いてそれから列挙される要素と、引数で指定したIEnumerale<T>から列挙される要素との交わりを返す(戻り値もIEnumerable<T>)。

それぞれIEnumerable<T>なA、Bの交わりを出すには

A.Intersect(B)

と書く。(第2引数に、交わりを求める条件を指定するIEqualityComparerを取るオーバーロードもある。)

二つの材料のどちらかを使っているのを抽出する(結び)

両方使うのを抽出するのは、表に両方書いてある行がないんだから無理だったけど、どちらかを選ぶのなら大丈夫だろう、と思ったが…

//肉またはじゃがいもを使った料理【悪い例】
IEnumerable<string> foods3 =
    menu
    .Where(x => x.Stuff == "肉" || x.Stuff == "じゃがいも")
    .Select(x => x.Dish);

肉またはじゃがいもを使った料理:
カレー
カレー
すき焼き
コロッケ
コロッケ
ポテトサラダ

…結果にダブりが。どちらかの条件に合うものを単純に列挙していけば、そりゃそうなるわな…。

というわけで、この場合も集合を使う。今回は(肉料理)∪(ジャガイモ料理)(集合の結び)を取る。

結びを求める拡張メソッドは「Union」。

//肉またはじゃがいもを使った料理【良い例】
IEnumerable<string> foods3 =
    menu
    .Where(x => x.Stuff == "肉")
    .Select(x => x.Dish)
    .Union(
        menu
        .Where(x => x.Stuff == "じゃがいも")
        .Select(x => x.Dish));

肉またはじゃがいもを使った料理:
カレー
すき焼き
コロッケ
ポテトサラダ

Unionの構文もIntersectと同じ。

A.Union(B)

オーバーロードについても同じ。

ある材料は使っても別のある材料は使わないのを抽出する(差)

肉は使っても、じゃがいもを使わない料理を抽出したい…そんな場合は(肉料理)-(じゃがいも料理)(集合の差)を使う。拡張メソッドは「Except」。

//肉を使ってじゃがいもを使わない料理
IEnumerable<string> foods4 =
    menu
    .Where(x => x.Stuff == "肉")
    .Select(x => x.Dish)
    .Except(
        menu
        .Where(x => x.Stuff == "じゃがいも")
        .Select(x => x.Dish));

肉を使ってじゃがいもを使わない料理:
すき焼き

構文やオーバーロードは前二つと同じ。

A.Except(B)

条件を増やしたいとき

LINQの各メソッドはIEnumerable<T>を受けてIEnumerable<T>を返すようになっているので、いくつでも繋げられる。

//肉とじゃがいもと卵を使った料理
IEnumerable<string> foods5 =
    menu
    .Where(x => x.Stuff == "肉")
    .Select(x => x.Dish)
    .Intersect(
        menu
        .Where(x => x.Stuff == "じゃがいも")
        .Select(x => x.Dish))
    .Intersect(
        menu
        .Where(x => x.Stuff == "卵")
        .Select(x => x.Dish));

肉とじゃがいもと卵を使った料理:
コロッケ

ということで、コロッケが最強となりました。(え

まとめ

A、B二つのクエリがあり、クエリ結果を組み合わせるとき

  • AとBの交わり → A.Intersect(B)
  • AとBの結び  → A.Union(B)
  • AからBを引いた → A.Except(B)

(ところで、コロ助がコロッケ好きなのはアニメ版だけの設定らしい…。)