規則性に基づいて要素を分割する拡張メソッド

同じ要素が途切れるところで分割する拡張メソッドがほしかったのでMoreLINQや他のサイトを探したのですが、
それらしいのが見当たらなかったので実装しました。

やりたいことはこんな感じです。※GroupByとかLookupではありません。

[1, 1, 1, 2, 2, 3, 3, 4, 1, 1] → [[1, 1, 1], [2, 2], [3, 3], [4], [1, 1]] 

ただ、もう少し拡張性がほしいので、規則性に基づいて要素を分割する拡張メソッドにしました。

では実装です。

public static IEnumerable<IEnumerable<T>> SplitByRegularity<T>(
    this IEnumerable<T> source, Func<List<T>, T, bool> predicate)
{
    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
            yield break;

        var items = new List<T> { enumerator.Current };
        while (enumerator.MoveNext())
        {
            if (predicate(items, enumerator.Current))
            {
                items.Add(enumerator.Current);
                continue;
            }

            yield return items;

            items = new List<T> { enumerator.Current };
        }

        if (items.Any())
            yield return items;
    }
}

やっていることは単純です。

  1. 入力されたシーケンスをループで回す
  2. 引数で渡された規則性があるかを判定するメソッド(predicate)を実行
  3. 結果がtrueの場合は一時リストに追加、falseのときは一時リストを返す

predicateの引数に一時リストそのものを渡しているところがポイントです。


実際の使用例をいくつか紹介します。

  • 同じ要素が連続しているという規則性
var source = new List<int> { 1, 1, 1, 2, 2, 3, 3, 4, 1, 1 };
var result = source.SplitByRegularity((items, current) => items.First().Equals(current));

// [[1, 1, 1], [2, 2], [3, 3], [4], [1, 1]] 
  • 増加するという規則性
var source = new List<int> { 1, 2, 4, 1, 2, -1, 0, 3, 2, 1 };
var result = source.SplitByRegularity((items, current) => items.Last() <= current);

// [[1, 2, 4], [1, 2], [-1, 0, 3], [2], [1]] 
  • 連続するという規則性
var source = new List<int> { 1, 2, 3, 2, 0, -1, -2, -1, 2, };
var result = source.SplitByRegularity((items, current) => current == items.Last() + 1 || current == items.Last() - 1);

// [[1, 2, 3, 2], [0, -1, -2, -1], [2]] 
  • 重複がないという規則性
var source = new List<int> { 1, 2, 3, 1, 5, 6, 5, 7, 8, 9, 9, 9 };
var result = source.SplitByRegularity((items, current) => !items.Contains(current));

// [[1, 2, 3], [1, 5, 6], [5, 7, 8, 9], [9], [9]] 

他にも、よく見かけるサイズで分割するSplitなんかもできます。

  • 素数が3以下という規則性
var source = new List<int> { 1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 6, 6 };
var result = source.SplitByRegularity((items, current) => items.Count <= 2);

// [[1, 1, 1], [2, 2, 3], [3, 3, 3], [4, 4, 5], [6, 6]] 


同じ要素が連続している素数が〇以下は使用頻度が高そうなので、
別に定義してもいいかもしれません。

public static IEnumerable<IEnumerable<T>> SplitBySize<T>(
    this IEnumerable<T> source, int size)
{
    return source.SplitByRegularity((items, current) => items.Count < size);
}

public static IEnumerable<IEnumerable<T>> SplitByEquality<T>(
    this IEnumerable<T> source)
{
    return source.SplitByRegularity((items, current) => items.Last().Equals(current));
}