読者です 読者をやめる 読者になる 読者になる

前後の値も利用したシーケンス処理 (その 2)

.NET

前後の値も利用したシーケンス処理 - NyaRuRuの日記』の続き.

// 連続して同じ値が来る箇所だけを省いて取得する
// この場合だと1,2,4,3,4,0が取れることを目指す
int[] array = { 1, 2, 4, 4, 3, 3, 4, 0, 0 };

前回はこの問題を肴に Scan とか Pairwise を持ち出しましたが,この問題に特化して考えるとして,もうちょっと使いやすい関数はないでしょうか.
例えば,UNIX の uniq コマンド は要求されている仕様そのものスバリのように見えます.uniq コマンドに相当する PowerShell の Get-Unique を使ってみます.

PS Z:\> 1,2,4,4,3,3,4,0,0 | Get-Unique
1
2
4
3
4
0

同様の関数に,Haskell の group/groupBy,Python itertools の groupby などがあるようです.
Haskell.

Prelude> :m Data.List
Prelude Data.List> group [1,2,4,4,3,3,4,0,0]
[[1],[2],[4,4],[3,3],[4],[0,0]]
Prelude Data.List> map head $ group [1,2,4,4,3,3,4,0,0] 
[1,2,4,3,4,0]
Prelude Data.List> map head $ groupBy (==) [1,2,4,4,3,3,4,0,0]
[1,2,4,3,4,0]

IronPython 2.0.1

>>> from itertools import groupby
>>> [k for k, g in groupby([1,2,4,4,3,3,4,0,0])]
[1, 2, 4, 3, 4, 0]

一方で LINQ の GroupBy および F# の Seq.groupby は,SQL の GROUP BY 句に似た動作をするため,上記のような使い方はできません.そこでちょっと発想を変えて,run-length エンコーディングを行う関数を作ってみました.

public static IEnumerable<TResult> RunLength<T, TResult>(
    this IEnumerable<T> source, Func<T, int, TResult> resultSelector)
{
    //AssertNonNullArgument(source, "source");
    //AssertNonNullArgument(resultSelector, "resultSelector");

    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            yield break;
        }
        var count = 1;
        var first = enumerator.Current;
        var eq = EqualityComparer<T>.Default;
        while (enumerator.MoveNext())
        {
            var cur = enumerator.Current;
            if (eq.Equals(first, cur))
            {
                checked { ++count; }
            }
            else
            {
                yield return resultSelector(first, count);
                first = cur;
                count = 1;
            }
        }
        yield return resultSelector(first, count);
    }
}

これで,個数の方を捨てれば題意を満たせます.

// test
var source = new []{1,2,4,4,3,3,4,0,0};
var seq = source.RunLength((i,_) => i);
var ans = new []{1,2,4,3,4,0};
Assert.IsTrue(seq.SequenceEqual(ans));