2015年12月2日水曜日

.Net(VB C#) LINQ
Enumerable.Where メソッドのAND条件やOR条件を動的に組み立てる

画面の検索条件などによりWhereメソッドの条件を動的に組み立てる事があると思います。
「Linq 動的 Where」などってググってみると、いろいろ情報が出てくると思います。
Expressionで動的に組み立てるのが正攻法なようですね。
う~んメンドクサイ。

メンドクサイ事はイヤなので簡単に動的OR検索する方法です。

テストデータです。
【C#】
private class Fruit
{
    public string Name { get; set; }
    public string Rank { get; set; }
    public decimal Price { get; set; }
}

var fruits = new List<Fruit>()
                {
                    new Fruit(){Name = "りんご", Rank = "A" , Price = 1000 },
                    new Fruit(){Name = "みかん", Rank = "A" , Price = 600 },
                    new Fruit(){Name = "ぶどう", Rank = "B" , Price = 1200 },
                    new Fruit(){Name = "りんご", Rank = "B" , Price = 800 },
                    new Fruit(){Name = "みかん", Rank = "A" , Price = 500 },
                    new Fruit(){Name = "ばなな", Rank = "C" , Price = 100 }
                };
【VB】
Private Class Fruit
    Public Property Name As String
    Public Property Rank As String
    Public Property Price As Decimal
End Class

Dim fruits = New List(Of Fruit)() From
            {
                New Fruit() With {.Name = "りんご", .Rank = "A", .Price = 1000},
                New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 600},
                New Fruit() With {.Name = "ぶどう", .Rank = "B", .Price = 1200},
                New Fruit() With {.Name = "りんご", .Rank = "B", .Price = 800},
                New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 500},
                New Fruit() With {.Name = "ばなな", .Rank = "C", .Price = 500}
            }

検索画面から検索条件が指定されたとします。

名称は複数指定でき、条件値はリストになっています。今回の条件は「りんご または みかん」
ランクも複数指定で、条件値はリストになっています。今回の条件は指定がありません。
値段は下限値と上限値が指定できます。今回の条件は800円以下です。
つまり、今回の条件は「りんご か みかん」 か 「800円以下」の果物になります。

【C#】
//検索条件
//--名称
var searchNameList = new List<string>() { "りんご", "みかん" };
//--ランク
var searchRankList = new List<string>() { /*指定なし*/ };
//--値段(下限、上限)
var priceRange = new Tuple<decimal?, decimal?>(null, 800);

【VB】
'検索条件
'--名称
Dim searchNameList = New List(Of String)() From {"りんご", "みかん"}
'--ランク
Dim searchRankList = New List(Of String)() From {} '指定なし
'--値段(下限、上限)
Dim priceRange = New Tuple(Of Decimal?, Decimal?)(Nothing, 800)

次に検索条件が指定されている場合に実行する処理を、リストに溜めて行きます。

List型の変数「funcSearchList」には、Fruit型の引数をとりboolを返す処理を格納できます。
例えば名称で検索する場合、Fruit型の引数が名称検索条件内に存在すればtrueを返す処理を、リストに追加しています。

【C#】
//検索処理リストに検索条件ごとの処理を追加していく
var funcSeachList = new List<Func<Fruit, bool>>();

//--名称
if (searchNameList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchNameList.Contains(fruit.Name); });
}
//--ランク
if (searchRankList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchRankList.Contains(fruit.Rank); });
}
//--値段
if (priceRange.Item1.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price >= priceRange.Item1.Value; });
}
if (priceRange.Item2.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price <= priceRange.Item2.Value; });
}
【VB】
'検索処理リストに検索条件ごとの処理を追加していく
Dim funcSeachList = New List(Of Func(Of Fruit, Boolean))()
'--名称
If (searchNameList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchNameList.Contains(fruit.Name))
End If
'--ランク
If (searchRankList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchRankList.Contains(fruit.Rank))
End If
'--値段
If (priceRange.Item1.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price >= priceRange.Item1.Value)
End If
If (priceRange.Item2.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price <= priceRange.Item2.Value)
End If

検索処理を溜めたList型の変数「funcSearchList」から、OR検索用のメソッドを作成します。

【C#】
//--Or検索用メソッドの作成
Func<Fruit, bool> searchOr = (fruit) =>
{
    //--検索条件が1件もなければ、要素は条件に一致したとする。
    if (funcSeachList.Count == 0)
        return true;
    //--要素が検索条件に一致するか判定する
    bool? result = null;
    foreach (var func in funcSeachList)
    {
        bool funcResult = func(fruit);
        //And検索したければココを||から&&に変更する
        result = (result.HasValue ? result.Value || funcResult : funcResult);     
    }
    return result.Value;
};
【VB】
'--Or検索用メソッドの作成
Dim searchOr = Function(fruit As Fruit)
                   '--検索条件が1件もなければ、要素は条件に一致したとする。
                   If (funcSeachList.Count = 0) Then
                       Return True
                   End If
                   '--要素が検索条件に一致するか判定する
                   Dim result As Nullable(Of Boolean) = Nothing
                   For Each func In funcSeachList
                       Dim funcResult As Boolean = func(fruit)
                       'And検索したければココをOrElseからAndAlsoに変更する
                       result = If(result.HasValue, result.Value OrElse funcResult, funcResult) 
                   Next
                   Return result.Value
               End Function
あとは作成したメソッドをWhereメソッドの引数に指定してあげればOKです。

【C#】
//Or検索
var searchOrResultList = fruits.Where(itm => { return searchOr(itm); });

//出力
foreach (var itm in searchOrResultList)
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"));
//名称=りんご, ランク=A, 値段=1,000
//名称=みかん, ランク=A, 値段=600
//名称=りんご, ランク=B, 値段=800
//名称=みかん, ランク=A, 値段=500
//名称=ばなな, ランク=C, 値段=100
【VB】
'Or検索
Dim searchOrResultList = fruits.Where(Function(itm) searchOr(itm))

'出力
For Each itm In searchOrResultList
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"))
Next
'名称=りんご, ランク=A, 値段=1,000
'名称=みかん, ランク=A, 値段=600
'名称=りんご, ランク=B, 値段=800
'名称=みかん, ランク=A, 値段=500
'名称=ばなな, ランク=C, 値段=100

コード全体です。

【C#】
//テストデータ作成
var fruits = new List<Fruit>()
    {
        new Fruit(){Name = "りんご", Rank = "A" , Price = 1000 },
        new Fruit(){Name = "みかん", Rank = "A" , Price = 600 },
        new Fruit(){Name = "ぶどう", Rank = "B" , Price = 1200 },
        new Fruit(){Name = "りんご", Rank = "B" , Price = 800 },
        new Fruit(){Name = "みかん", Rank = "A" , Price = 500 },
        new Fruit(){Name = "ばなな", Rank = "C" , Price = 100 }
    };

/* 検索画面から検索条件が指定されたとする */
//検索条件
//--名称
var searchNameList = new List<string>() { "りんご", "みかん" };
//--ランク
var searchRankList = new List<string>() { /*指定なし*/ };
//--値段(下限、上限)
var priceRange = new Tuple<decimal?, decimal?>(null, 800);


//検索処理リストに検索条件ごとの処理を追加していく
var funcSeachList = new List<Func<Fruit, bool>>();
//--名称
if (searchNameList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchNameList.Contains(fruit.Name); });
}
//--ランク
if (searchRankList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchRankList.Contains(fruit.Rank); });
}
//--値段
if (priceRange.Item1.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price >= priceRange.Item1.Value; });
}
if (priceRange.Item2.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price <= priceRange.Item2.Value; });
}

//Or検索用メソッドの作成
Func<Fruit, bool> searchOr = (fruit) =>
{
    //--検索条件が1件もなければ、要素は条件に一致したとする。
    if (funcSeachList.Count == 0)
        return true;
    //--要素が検索条件に一致するか判定する
    bool? result = null;
    foreach (var func in funcSeachList)
    {
        bool funcResult = func(fruit);
        result = (result.HasValue ? result.Value || funcResult : funcResult);
    }
    return result.Value;
};

//Or検索
var searchOrResultList = fruits.Where(itm => { return searchOr(itm); });

//出力
foreach (var itm in searchOrResultList)
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"));
【VB】
'テストデータ作成
Dim fruits = New List(Of Fruit)() From
    {
        New Fruit() With {.Name = "りんご", .Rank = "A", .Price = 1000},
        New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 600},
        New Fruit() With {.Name = "ぶどう", .Rank = "B", .Price = 1200},
        New Fruit() With {.Name = "りんご", .Rank = "B", .Price = 800},
        New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 500},
        New Fruit() With {.Name = "ばなな", .Rank = "C", .Price = 500}
    }

'検索画面から検索条件が指定されたとする
'検索条件
'--名称
Dim searchNameList = New List(Of String)() From {"りんご", "みかん"}
'--ランク
Dim searchRankList = New List(Of String)() From {} '指定なし
'--値段(下限、上限)
Dim priceRange = New Tuple(Of Decimal?, Decimal?)(Nothing, 800)


'検索処理リストに検索条件ごとの処理を追加していく
Dim funcSeachList = New List(Of Func(Of Fruit, Boolean))()
'--名称
If (searchNameList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchNameList.Contains(fruit.Name))
End If
'--ランク
If (searchRankList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchRankList.Contains(fruit.Rank))
End If
'--値段
If (priceRange.Item1.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price >= priceRange.Item1.Value)
End If
If (priceRange.Item2.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price <= priceRange.Item2.Value)
End If

'Or検索用メソッドの作成
Dim searchOr = Function(fruit As Fruit)
                   '--検索条件が1件もなければ、要素は条件に一致したとする。
                   If (funcSeachList.Count = 0) Then
                       Return True
                   End If
                   '--要素が検索条件に一致するか判定する
                   Dim result As Nullable(Of Boolean) = Nothing
                   For Each func In funcSeachList
                       Dim funcResult As Boolean = func(fruit)
                       result = If(result.HasValue, result.Value OrElse funcResult, funcResult)
                   Next
                   Return result.Value
               End Function

'Or検索
Dim searchOrResultList = fruits.Where(Function(itm) searchOr(itm))

'出力
For Each itm In searchOrResultList
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"))
Next

Or検索用メソッドsearchOrのOR演算子「|| または OrElse」をAND演算子「&& または AndAlso」にかえればAND検索になります。
検索処理を溜めたList型の変数「funcSearchList」に、検索メソッドと一緒にAND検索かOR検索か判定するフラグも格納すればAND検索とOR検索のゴチャ混ぜ検索もできます。

OR検索用メソッドを組み立てる部分は汎用化できるので、Linqに独自の拡張メソッドとして追加してもいいかと思います。
以下はOR検索する処理のリストを引数にとり、条件に該当した要素のリストを返す拡張メソッドです。

【C#】
public static class LinqExtension
{
    public static IEnumerable<T> WhereOr<T>(this IEnumerable<T> source, List<Func<T, bool>> lstFunc)
    {
        //検索条件が1件もなければ、要素は条件に一致したとする。
        if (lstFunc.Count == 0)
            return source;
        //検索条件に一致した要素のリストを作成する
        var returnList = new List<T>();
        foreach (var item in source)
        {
            //--要素が検索条件に一致するか判定する
            bool? match = null;
            foreach (var func in lstFunc)
            {
                bool funcResult = func(item);
                match = (match.HasValue ? match.Value || funcResult : funcResult);
            }
            //--条件が一致した要素のみリストに追加
            if (match.Value == true)
                returnList.Add(item);
        }
        return returnList;
    }
}
【VB】
Imports System.Runtime.CompilerServices

Module LinqExtenstions

    <Extension()> _
    Public Function WhereOr(Of T) _
        (ByVal source As IEnumerable(Of T), lstFunc As List(Of Func(Of T, Boolean))) As IEnumerable(Of T)
        '検索条件が1件もなければ、要素は条件に一致したとする。
        If (lstFunc.Count = 0) Then
            Return source
        End If
        '検索条件に一致した要素のリストを作成する
        Dim returnList = New List(Of T)()
        For Each item In source
            '--要素が検索条件に一致するか判定する
            Dim match As Nullable(Of Boolean) = Nothing
            For Each func In lstFunc
                Dim funcResult As Boolean = func(item)
                match = If(match.HasValue, match.Value OrElse funcResult, funcResult)
            Next
            '--条件が一致した要素のみリストに追加
            If (match.Value = True) Then
                returnList.Add(item)
            End If
        Next
        Return returnList
    End Function

End Module

使用方法は、先ほどのWhereメソッド部分をWhereOrメソッドに変更します。
【C#】
//テストデータ作成
var fruits = new List<Fruit>()
    {
        new Fruit(){Name = "りんご", Rank = "A" , Price = 1000 },
        new Fruit(){Name = "みかん", Rank = "A" , Price = 600 },
        new Fruit(){Name = "ぶどう", Rank = "B" , Price = 1200 },
        new Fruit(){Name = "りんご", Rank = "B" , Price = 800 },
        new Fruit(){Name = "みかん", Rank = "A" , Price = 500 },
        new Fruit(){Name = "ばなな", Rank = "C" , Price = 100 }
    };

/* 検索画面から検索条件が指定されたとする */
//検索条件
//--名称
var searchNameList = new List<string>() { "りんご", "みかん" };
//--ランク
var searchRankList = new List<string>() { /*指定なし*/ };
//--値段(下限、上限)
var priceRange = new Tuple<decimal?, decimal?>(null, 800);


//検索処理リストに検索条件ごとの処理を追加していく
var funcSeachList = new List<Func<Fruit, bool>>();
//--名称
if (searchNameList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchNameList.Contains(fruit.Name); });
}
//--ランク
if (searchRankList.Count > 0)
{
    funcSeachList.Add((fruit) => { return searchRankList.Contains(fruit.Rank); });
}
//--値段
if (priceRange.Item1.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price >= priceRange.Item1.Value; });
}
if (priceRange.Item2.HasValue)
{
    funcSeachList.Add((fruit) => { return fruit.Price <= priceRange.Item2.Value; });
}


//Or検索
//独自に追加したWhereOrメソッドを使用する
var searchOrResultList = fruits.WhereOr(funcSeachList);

//出力
foreach (var itm in searchOrResultList)
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"));
【VB】
'テストデータ作成
Dim fruits = New List(Of Fruit)() From
    {
        New Fruit() With {.Name = "りんご", .Rank = "A", .Price = 1000},
        New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 600},
        New Fruit() With {.Name = "ぶどう", .Rank = "B", .Price = 1200},
        New Fruit() With {.Name = "りんご", .Rank = "B", .Price = 800},
        New Fruit() With {.Name = "みかん", .Rank = "A", .Price = 500},
        New Fruit() With {.Name = "ばなな", .Rank = "C", .Price = 500}
    }

'検索画面から検索条件が指定されたとする
'検索条件
'--名称
Dim searchNameList = New List(Of String)() From {"りんご", "みかん"}
'--ランク
Dim searchRankList = New List(Of String)() From {} '指定なし
'--値段(下限、上限)
Dim priceRange = New Tuple(Of Decimal?, Decimal?)(Nothing, 800)


'検索処理リストに検索条件ごとの処理を追加していく
Dim funcSeachList = New List(Of Func(Of Fruit, Boolean))()
'--名称
If (searchNameList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchNameList.Contains(fruit.Name))
End If
'--ランク
If (searchRankList.Count > 0) Then
    funcSeachList.Add(Function(fruit) searchRankList.Contains(fruit.Rank))
End If
'--値段
If (priceRange.Item1.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price >= priceRange.Item1.Value)
End If
If (priceRange.Item2.HasValue) Then
    funcSeachList.Add(Function(fruit) fruit.Price <= priceRange.Item2.Value)
End If

'Or検索
'独自に追加したWhereOrメソッドを使用する
Dim searchOrResultList = fruits.WhereOr(funcSeachList)

'出力
For Each itm In searchOrResultList
    Console.WriteLine("名称={0}, ランク={1}, 値段={2}", itm.Name, itm.Rank, itm.Price.ToString("#,##0"))
Next


.Net(VB C#) LINQのメソッド一覧

0 件のコメント: