Help us understand the problem. What is going on with this article?

IEnumerable<T>型のシーケンスから、任意の要素数ごとに分割したIEnumerable<T[]>型のブロックシーケンスを作る

More than 3 years have passed since last update.

こんにちはー!ニアです。

今回は、IEnumerable<T>型のシーケンスから、任意の要素数ごとに分割したIEnumerable<T[]>型のブロックシーケンスを作成する方法を紹介していきます。

26-1.PNG

※この記事で登場する「T」は、ジェネリクスにおける要素の型です。

1. シーケンスから、任意の要素数ごとに分割したブロックシーケンスを作成

1.1. 配列に変換して各要素を取り出してみよう

オーソドックスな方法としては、シーケンスを配列に変換しておき、for文を使って配列の先頭から順に要素を取り出し、ブロックに代入していきます。

ブロックの代入先で指定するインデックスは、シーケンスのインデックスからブロックの長さで割った余りとなります。

{Index}_{block} = {Index}_{seq.} \: mod \: [ブロックの長さ(Length\:per\:Block)]

ブロックの各要素に値を全てセットした or インデックスがシーケンスの末尾に達した時、yield return文でブロックを返し、ブロックシーケンスを生成します。

26-2.PNG

注:配列は参照型なので、yield return文でブロックを返した後、次のブロック用に配列を再作成する必要があります。

SeqToBlockSeq1.cs
/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
public static IEnumerable<T[]> ToBlockSequence<T>( this IEnumerable<T> source, int numInBlock ) {

    // シーケンスを配列に変換します。
    var source2 = source.ToArray();
    // ブロックとなる配列です。
    var block = new T[numInBlock];

    for( int i = 0; i < source2.Length; i++ ) {
        // シーケンスのi番目の要素を、ブロックの( i mod numInBlock )番目に代入します。
        block[i % numInBlock] = source2[i];

        // iをnumInBlockで割った余りが、ブロックの末尾のインデックスと等しい or 
        // iがシーケンスの末尾のインデックスと等しいかどうか判別します。
        if( i % numInBlock == numInBlock - 1 || i == source2.Length - 1 ) {
            // ブロックを返します。
            yield return block;
            // 新しいブロックを作成します。(注:配列は参照型です)
            block = new T[numInBlock];
        }
    }
}

1.2. LINQだけで実装してみよう

「C#でコレクションの列挙にfor文だなんて、センスないZE!」

というナウでヤングなアナタに、LINQはいかがでしょう。

Selectメソッドには、シーケンスの各要素にインデックスを付けて射影する機能があるのでそれを利用して、インデックスをブロックの長さで割った値をキーに、要素の値とのペア(KeyValuePair<int, T>型)を生成します。

次にGroupByメソッドでキーの値ごとにグループ化し、各グループに属する要素の値を抽出して、それらをSelectメソッドで配列に変換します。

26-3.PNG

SeqToBlockSeq2.cs
/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
public static IEnumerable<T[]> ToBlockSequence<T>( this IEnumerable<T> source, int numInBlock ) {

    // 各要素にインデックスを付加し、numInBlockで割った値と要素の値とのペアを作成します。
    return source.Select( ( v, i ) => new KeyValuePair<int, T>( i / numInBlock, v ) )
                    // numInBlockで割った値でグループ化し、そのグループに属する要素の値を抽出します。
                    .GroupBy( v1 => v1.Key, v2 => v2.Value )
                    // グループに属する要素の値を配列に変換します。
                    .Select( block => block.ToArray() );
}

1.3. ブロック単位でストリーミング処理してみよう

例えば、以下のような問題が出たとしましょう。

「長さが不定のシーケンスを、n個ずつに分割したブロックシーケンスに変換せよ」

1.1.にあるToArrayメソッドや、1.2.にあるGroupByメソッドでは、実行時にシーケンス内のすべての要素を列挙するので、長さが分からないシーケンスを扱うには、呼び出し側から必要なブロック数に応じてシーケンスの要素を列挙できるようにひと工夫が必要です。

そこで、Selectメソッドでシーケンスの各要素に付加したインデックスをブロックの長さで割った余りをキーに、要素の値とのペア(KeyValuePair<int, T>型)を生成します。

次にforeach文で各要素を列挙し、要素のキーをブロックのインデックスとして指定し、要素の値を代入します。

要素のキーがブロックの末尾のインデックスと等しい時、ブロックを返します。

26-4.PNG

SeqToBlockSeq3.cs
/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
public static IEnumerable<T[]> ToBlockSequence<T>( this IEnumerable<T> source, int numInBlock ) {

    // ブロックとなる配列です。
    var block = new T[numInBlock];

    // 各要素にインデックスを付加して、numInBlockで割った余り( = ブロック内のインデックス )と要素の値とのペアを作成し、それらを列挙します。
    foreach( var s in source.Select( ( v, i ) => new KeyValuePair<int, T>( i % numInBlock, v ) ) ) {
        // 要素のキーをブロックのインデックスとして指定し、要素の値を代入します。
        block[s.Key] = s.Value;

        // 現在の要素のキーとブロックの配列の末尾のインデックスが等しいかどうかを判別します。
        if( s.Key == numInBlock - 1 ) {
            // ブロックを返します。
            yield return block;
            // 新しいブロックを作成します。
            block = new T[numInBlock];
        }
    }
}

+α: 末尾側の要素も出力させるには?

しかし上の方法では、シーケンスの要素数が1ブロック当たりの要素数で割り切れない場合、末尾のブロックは切り捨てられてしまいます。

そこで、ブロック内の現在のインデックスを格納する変数を1つ作成し(初期値はブロック内の末尾のインデックス)、foreach文を実行後、その変数の値がブロック内の末尾のインデックスより小さい時は、ブロックを返します。これで末尾側の要素を含むブロックももれなく返すことができます。

26-5.PNG

SeqToBlockSeq3plus.cs
/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
public static IEnumerable<T[]> ToBlockSequence4<T>( this IEnumerable<T> source, int numInBlock ) {

    // ブロック内のインデックスです。
    int indexInBlock = numInBlock - 1;
    // ブロックとなる配列です。
    var block = new T[numInBlock];

    // 各要素にインデックスを付加して、numInBlockで割った余り( = ブロック内のインデックス )と要素の値とのペアを作成し、それらを列挙します。
    foreach( var s in source.Select( ( v, i ) => new KeyValuePair<int, T>( i % numInBlock, v ) ) ) {
        indexInBlock = s.Key;
        // ブロック内の要素にシーケンスの要素を代入します。
        block[s.Key] = s.Value;

        // 現在のブロック内のインデックスが末尾であるかどうかを判別します。
        if( s.Key == numInBlock - 1 ) {
            // ブロックを返します。
            yield return block;
            // 新しいブロックを作成します。
            block = new T[numInBlock];
        }
    }

    // indexInBlockの値が、ブロック内の末尾のインデックスより前にいる時、
    // シーケンスの末尾側の要素がforeach文内でまだ返していないので、
    // ここで返します。
    if( indexInBlock < numInBlock - 1 ) {
        yield return block;
    }
}

さらにbool型引数を1つ追加し、利用者のニーズに合わせて、ブロックの長さに満たない末尾のブロックを「含める or 切り捨てる」かを選択できるようにすると、使い勝手が向上します。

SeqToBlockSeq.cs
/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <param name="includeLastFractionBlock">1ブロック当たりの要素数に満たない末尾のブロックを含めるかどうか</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
private static IEnumerable<T[]> ToBlockSequenceCore<T>( this IEnumerable<T> source, int numInBlock, bool includeLastFractionBlock ) {

    // ブロック内のインデックスです。
    int indexInBlock = numInBlock - 1;
    // ブロックとなる配列です。
    var block = new T[numInBlock];

    // 各要素にインデックスを付加して、numInBlockで割った余り( = ブロック内のインデックス )と要素の値とのペアを作成し、それらを列挙します。
    foreach( var s in source.Select( ( v, i ) => new KeyValuePair<int, T>( i % numInBlock, v ) ) ) {
        indexInBlock = s.Key;
        // ブロック内の要素にシーケンスの要素を代入します。
        block[s.Key] = s.Value;

        // 現在のブロック内のインデックスが末尾であるかどうかを判別します。
        if( s.Key == numInBlock - 1 ) {
            // ブロックを返します。
            yield return block;
            // 新しいブロックを作成します。
            block = new T[numInBlock];
        }
    }

    // indexInBlockの値が、ブロック内の末尾のインデックスより前にいる時、
    // シーケンスの末尾にある端数の要素がforeach文内でまだ返していないので、
    // ここで返します。
    if( includeLastFractionBlock && indexInBlock < numInBlock - 1 ) {
        yield return block;
    }
}

/// <summary>
///     シーケンスを指定した要素数ごとに分割したブロックシーケンスに変換します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="source">シーケンス</param>
/// <param name="numInBlock">1ブロック当たりの要素数</param>
/// <param name="includeLastFractionBlock">1ブロック当たりの要素数に満たない末尾のブロックを含めるかどうか</param>
/// <returns><paramref name="numInBlock"/>ごとに分割したブロックシーケンス</returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/>がnullの時</exception>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="numInBlock"/>が0以下の時</exception>
public static IEnumerable<T[]> ToBlockSequence<T>( this IEnumerable<T> source, int numInBlock, bool includeLastFractionBlock = false ) {

    if( source == null ) {
        throw new ArgumentNullException( nameof( source ) );
    }
    else if( numInBlock <= 0 ) {
        throw new ArgumentOutOfRangeException( nameof( numInBlock ), "1ブロック当たりの要素数は、1以上にする必要があります。" );
    }

    return source.ToBlockSequenceCore( numInBlock, includeLastFractionBlock );
}

2.【おまけ】IEnumerable<T[]>型ブロックシーケンスからIEnumerable<T>型のシーケンスに平坦化する時は?

逆に、IEnumerable<T[]>型ブロックシーケンスからIEnumerable<T>型のシーケンスに平坦化したい時は、SelectManyメソッドを利用し、各ブロック内の要素を1つずつ取り出します。

/// <summary>
///     ブロックシーケンスからシーケンスに平坦化します。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
/// <param name="blockSource">ブロックシーケンス</param>
/// <returns>シーケンス</returns>
public static IEnumerable<T> ToStreamSequence<T>( this IEnumerable<T[]> blockSource ) =>
    blockSource.SelectMany( s => s );

3. サンプルプログラム

RNAの塩基配列からコドンに変換し、コドン表を利用して対応するアミノ酸を求めるプログラムです。

文字列で表したRNAの塩基配列から、3つずつ要素を分割して(IEnumerable<char> ⇒ IEnumerable<char[]>)コドンへ変換し、コドン表に従ってアミノ酸に変換していきます。

26-6.PNG

RNACodonSample.cs
using System;
using System.Collections.Generic;
using System.Linq;

class Program {
    static void Main( string[] args ) {

        // 塩基配列(RNA)
        string rnaSeq = "AUGAUGGAGCUUCGGAGCUAG";
        // コドンに変換します。
        var codon = rnaSeq.ToBlockSequence( 3 ).Select( c => new string( c ) );
        // コドンを出力します。
        Console.WriteLine( $"Codon      : {string.Join( "-", codon )}" );
        // コドン表に従ってアミノ酸に変換し、出力します。
        Console.WriteLine( $"Amino acid : {string.Join( "-", codon.Select( c => RNACodonTable[c] ) )}" );
    }

    /// <summary>
    ///     RNAのコドン表を表します。
    /// </summary>
    /// <remarks>「[E]」のキーは終止コドンです。</remarks>
    static Dictionary<string, string> RNACodonTable { get; } = new Dictionary<string, string> {
        ["UUU"] = "Phe", ["UUC"] = "Phe", ["UUA"] = "Leu", ["UUG"] = "Leu",
        ["UCU"] = "Ser", ["UCC"] = "Ser", ["UCA"] = "Ser", ["UCG"] = "Ser",
        ["UAU"] = "Tyr", ["UAC"] = "Tyr", ["UAA"] = "[E]", ["UAG"] = "[E]",
        ["UGU"] = "Cys", ["UGC"] = "Cys", ["UGA"] = "[E]", ["UGG"] = "Trp",
        ["CUU"] = "Leu", ["CUC"] = "Leu", ["CUA"] = "Leu", ["CUG"] = "Leu",
        ["CCU"] = "Pro", ["CCC"] = "Pro", ["CCA"] = "Pro", ["CCG"] = "Pro",
        ["CAU"] = "His", ["CAC"] = "His", ["CAA"] = "Gln", ["CAG"] = "Gln",
        ["CGU"] = "Arg", ["CGC"] = "Arg", ["CGA"] = "Arg", ["CGG"] = "Arg",
        ["AUU"] = "Ile", ["AUC"] = "Ile", ["AUA"] = "Ile", ["AUG"] = "Met",
        ["ACU"] = "Thr", ["ACC"] = "Thr", ["ACA"] = "Thr", ["ACG"] = "Thr",
        ["AAU"] = "Asn", ["AAC"] = "Asn", ["AAA"] = "Lys", ["AAG"] = "Lys",
        ["AGU"] = "Ser", ["AGC"] = "Ser", ["AGA"] = "Arg", ["AGG"] = "Arg",
        ["GUU"] = "Val", ["GUC"] = "Val", ["GUA"] = "Val", ["GUG"] = "Val",
        ["GCU"] = "Ala", ["GCC"] = "Ala", ["GCA"] = "Ala", ["GCG"] = "Ala",
        ["GAU"] = "Asp", ["GAC"] = "Asp", ["GAA"] = "Gln", ["GAG"] = "Gln",
        ["GGU"] = "Gly", ["GGC"] = "Gly", ["GGA"] = "Gly", ["GGG"] = "Gly",
    };
}

static class BlockEnumerable {

    private static IEnumerable<T[]> ToBlockSequenceCore<T>( this IEnumerable<T> source, int numInBlock, bool includeLastFractionBlock ) {

        int indexInBlock = numInBlock - 1;
        var block = new T[numInBlock];

        foreach( var s in source.Select( ( v, i ) => new KeyValuePair<int, T>( i % numInBlock, v ) ) ) {
            indexInBlock = s.Key;
            block[s.Key] = s.Value;

            if( s.Key == numInBlock - 1 ) {
                yield return block;
                block = new T[numInBlock];
            }
        }

        if( includeLastFractionBlock && indexInBlock < numInBlock - 1 ) {
            yield return block;
        }
    }

    public static IEnumerable<T[]> ToBlockSequence<T>( this IEnumerable<T> source, int numInBlock, bool includeLastFractionBlock = false ) {

        if( source == null ) {
            throw new ArgumentNullException( nameof( source ) );
        }
        else if( numInBlock <= 0 ) {
            throw new ArgumentOutOfRangeException( nameof( numInBlock ), "1ブロック当たりの要素数は、1以上にする必要があります。" );
        }

        return source.ToBlockSequenceCore( numInBlock, includeLastFractionBlock );
    }
}

◆ 実行結果
Codon : AUG-AUG-GAG-CUU-CGG-AGC-UAG
Amino acid : Met-Met-Gln-Leu-Arg-Ser-[E]

4. おわりに

今回は、IEnumerable<T>型のシーケンスから、任意の要素数ごとに分割したIEnumerable<T[]>型ブロックシーケンスを作成する方法を紹介しました。

3.のサンプルプログラムでは、タンパク質の分野で例に示しましたが、他にも情報セキュリティの分野にて、「ブロック暗号で暗号化するためにbyte型配列の平文を8バイト(64ビット)や16バイト(128ビット)ごとに分割する」という用途に応用することができます。

それでは、See you next!

nia_tn1012
湘南生まれのITエンジニア(サーバー・フロント・DB・インフラ(クラウド))です。主にC#、PHP、TypeScript、GO言語、Docker、gRPCを使っています。好物は紅茶とコーヒー、シラス丼、趣味は写真撮影と音ゲーです。よろしくお願いします!
https://chronoir.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away