各パーティションの要素数を指定することにより、リストをリストのリストに分割したいと思います。
たとえば、リスト{1、2、... 11}があり、各セットに4つの要素があり、最後のセットができるだけ多くの要素を満たすように分割したいとします。結果のパーティションは、{{1..4}、{5..8}、{9..11}}のようになります。
これを書くエレガントな方法は何でしょうか?
ここにあなたが望むことをする拡張メソッドがあります:
public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
for (int i = 0; i < (source.Count / size) + (source.Count % size > 0 ? 1 : 0); i++)
yield return new List<T>(source.Skip(size * i).Take(size));
}
編集:関数のよりクリーンなバージョンを次に示します。
public static IEnumerable<List<T>> Partition<T>(this IList<T> source, Int32 size)
{
for (int i = 0; i < Math.Ceiling(source.Count / (Double)size); i++)
yield return new List<T>(source.Skip(size * i).Take(size));
}
LINQを使用すると、次のようなコードを1行にまとめることができます...
var x = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var groups = x.Select((i, index) => new
{
i,
index
}).GroupBy(group => group.index / 4, element => element.i);
その後、次のようにグループを反復処理できます...
foreach (var group in groups)
{
Console.WriteLine("Group: {0}", group.Key);
foreach (var item in group)
{
Console.WriteLine("\tValue: {0}", item);
}
}
次のような出力が得られます...
Group: 0
Value: 1
Value: 2
Value: 3
Value: 4
Group: 1
Value: 5
Value: 6
Value: 7
Value: 8
Group: 2
Value: 9
Value: 10
Value: 11
以下のようなもの(未テストのエアコード):
IEnumerable<IList<T>> PartitionList<T>(IList<T> list, int maxCount)
{
List<T> partialList = new List<T>(maxCount);
foreach(T item in list)
{
if (partialList.Count == maxCount)
{
yield return partialList;
partialList = new List<T>(maxCount);
}
partialList.Add(item);
}
if (partialList.Count > 0) yield return partialList;
}
これはリストのリストではなくリストの列挙を返しますが、結果をリストに簡単にラップできます:
IList<IList<T>> listOfLists = new List<T>(PartitionList<T>(list, maxCount));
グループ化、数学、繰り返しを避けるため。
このメソッドは、不要な計算、比較、および割り当てを回避します。パラメータの検証が含まれています。
これが フィドルで動作するデモ です。
public static IEnumerable<IList<T>> Partition<T>(
this IEnumerable<T> source,
int size)
{
if (size < 2)
{
throw new ArgumentOutOfRangeException(
nameof(size),
size,
"Must be greater or equal to 2.");
}
T[] partition;
int count;
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
partition = new T[size];
partition[0] = e.Current;
count = 1;
}
else
{
yield break;
}
while(e.MoveNext())
{
partition[count] = e.Current;
count++;
if (count == size)
{
yield return partition;
count = 0;
partition = new T[size];
}
}
}
if (count > 0)
{
Array.Resize(ref partition, count);
yield return partition;
}
}
var yourList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
var groupSize = 4;
// here's the actual query that does the grouping...
var query = yourList
.Select((x, i) => new { x, i })
.GroupBy(i => i.i / groupSize, x => x.x);
// and here's a quick test to ensure that it worked properly...
foreach (var group in query)
{
foreach (var item in group)
{
Console.Write(item + ",");
}
Console.WriteLine();
}
List<List<T>>
ではなく実際のIEnumerable<IEnumerable<T>>
が必要な場合は、クエリを次のように変更します。
var query = yourList
.Select((x, i) => new { x, i })
.GroupBy(i => i.i / groupSize, x => x.x)
.Select(g => g.ToList())
.ToList();
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list, int size)
{
while (list.Any()) { yield return list.Take(size); list = list.Skip(size); }
}
文字列の特別な場合
public static IEnumerable<string> Partition(this string str, int size)
{
return str.Partition<char>(size).Select(AsString);
}
public static string AsString(this IEnumerable<char> charList)
{
return new string(charList.ToArray());
}
なぜJochemsがArraySegmentを使用して回答したのかがわかりません。 (IListにキャストして)セグメントを拡張する必要がない限り、これは非常に便利です。たとえば、実行しようとしているのが、並行処理のためにTPL DataFlowパイプラインにセグメントを渡すことだとします。 IListインスタンスとしてセグメントを渡すと、同じコードで配列とリストを不可知的に処理できます。
もちろん、それは疑問を投げかけます:ToArray()を呼び出すことによるメモリの浪費を必要としないListSegmentクラスを単に派生させないのはなぜですか?その答えは、配列は実際には状況によってはわずかに速く処理できる(わずかに高速なインデックス付け)ことができるということです。ただし、違いの多くに気づくには、かなりハードコアな処理を行う必要があります。さらに重要なことに、リストへの参照を保持する他のコードによるランダムな挿入および削除操作から保護するための良い方法はありません。
100万の数値リストでToArray()を呼び出すと、ワークステーションで約3ミリ秒かかります。通常、それを使用して、ロックにかかる多大なコストをかけずに、並行操作でより堅牢なスレッドセーフティの利点を得るために支払うコストはそれほど高くありません。
ArraySegmentsを使用すると、読みやすく短い解決策になる場合があります(リストを配列にキャストする必要があります)。
var list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; //Added 0 in front on purpose in order to enhance simplicity.
int[] array = list.ToArray();
int step = 4;
List<int[]> listSegments = new List<int[]>();
for(int i = 0; i < array.Length; i+=step)
{
int[] segment = new ArraySegment<int>(array, i, step).ToArray();
listSegments.Add(segment);
}
または.Net 2.0では、次のようにします。
static void Main(string[] args)
{
int[] values = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
List<int[]> items = new List<int[]>(SplitArray(values, 4));
}
static IEnumerable<T[]> SplitArray<T>(T[] items, int size)
{
for (int index = 0; index < items.Length; index += size)
{
int remains = Math.Min(size, items.Length-index);
T[] segment = new T[remains];
Array.Copy(items, index, segment, 0, remains);
yield return segment;
}
}
あなたは拡張メソッドを使うことができます:
public static IList<HashSet<T>> Partition<T>(this IEnumerable<T> input, Func<T, object> partitionFunc)
{
Dictionary<object, HashSet> partitions = new Dictionary<object, HashSet<T>>();
object currentKey = null;
foreach (T item in input ?? Enumerable.Empty<T>())
{
currentKey = partitionFunc(item);
if (!partitions.ContainsKey(currentKey))
{
partitions[currentKey] = new HashSet<T>();
}
partitions[currentKey].Add(item);
}
return partitions.Values.ToList();
</ code>
}
複数のチェック、不必要なインスタンス化、反復の繰り返しを回避するには、次のコードを使用できます。
namespace System.Collections.Generic
{
using Linq;
using Runtime.CompilerServices;
public static class EnumerableExtender
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEmpty<T>(this IEnumerable<T> enumerable) => !enumerable?.GetEnumerator()?.MoveNext() ?? true;
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (size < 2)
throw new ArgumentOutOfRangeException(nameof(size));
IEnumerable<T> items = source;
IEnumerable<T> partition;
while (true)
{
partition = items.Take(size);
if (partition.IsEmpty())
yield break;
else
yield return partition;
items = items.Skip(size);
}
}
}
}