J'essaye de scinder une liste en une série de listes plus petites.
Mon problème: Ma fonction pour diviser des listes ne les divise pas en listes de la taille correcte. Il devrait les scinder en listes de taille 30 mais plutôt en listes de taille 114?
Comment faire en sorte que ma fonction divise une liste en un nombre X de listes de taille 30 ou moins ?
public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30)
{
List<List<float[]>> list = new List<List<float[]>>();
for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
List <float[]> subLocat = new List <float[]>(locations);
if (subLocat.Count >= ((i*nSize)+nSize))
subLocat.RemoveRange(i*nSize, nSize);
else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));
Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
list.Add (subLocat);
}
return list;
}
Si j'utilise la fonction sur une liste de taille 144, le résultat est le suivant:
Indice: 4, Taille: 120
Indice: 3, Taille: 114
Indice: 2, Taille: 114
Indice: 1, Taille: 114
Indice: 0, Taille: 114
public static List<List<float[]>> splitList(List<float[]> locations, int nSize=30)
{
var list = new List<List<float[]>>();
for (int i=0; i < locations.Count; i+= nSize)
{
list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
}
return list;
}
Version générique:
public static IEnumerable<List<T>> splitList<T>(List<T> locations, int nSize=30)
{
for (int i=0; i < locations.Count; i+= nSize)
{
yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i));
}
}
Je suggérerais d'utiliser cette méthode d'extension pour réduire la liste source aux sous-listes en fonction de la taille de bloc spécifiée:
/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
{
return source
.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / chunkSize)
.Select(x => x.Select(v => v.Value).ToList())
.ToList();
}
}
Par exemple, si vous réduisez la liste de 18 articles par 5 articles par morceau, vous obtenez la liste des 4 sous-listes contenant les articles suivants: 5-5-5-3.
que diriez-vous:
while(locations.Any())
{
list.Add(locations.Take(nSize).ToList());
locations= locations.Skip(nSize).ToList();
}
La solution Serj-Tm convient, il s'agit également de la version générique comme méthode d'extension pour les listes (placez-la dans une classe statique):
public static List<List<T>> Split<T>(this List<T> items, int sliceSize = 30)
{
List<List<T>> list = new List<List<T>>();
for (int i = 0; i < items.Count; i += sliceSize)
list.Add(items.GetRange(i, Math.Min(sliceSize, items.Count - i)));
return list;
}
Je trouve réponse acceptée (Serj-Tm) plus robuste, mais je voudrais suggérer une version générique.
public static List<List<T>> splitList<T>(List<T> locations, int nSize = 30)
{
var list = new List<List<T>>();
for (int i = 0; i < locations.Count; i += nSize)
{
list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i)));
}
return list;
}
J'ai une méthode générique qui prendrait n'importe quel type, y compris float, et elle a été testée à l'unité, j'espère que cela aidera:
/// <summary>
/// Breaks the list into groups with each group containing no more than the specified group size
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="values">The values.</param>
/// <param name="groupSize">Size of the group.</param>
/// <returns></returns>
public static List<List<T>> SplitList<T>(IEnumerable<T> values, int groupSize, int? maxCount = null)
{
List<List<T>> result = new List<List<T>>();
// Quick and special scenario
if (values.Count() <= groupSize)
{
result.Add(values.ToList());
}
else
{
List<T> valueList = values.ToList();
int startIndex = 0;
int count = valueList.Count;
int elementCount = 0;
while (startIndex < count && (!maxCount.HasValue || (maxCount.HasValue && startIndex < maxCount)))
{
elementCount = (startIndex + groupSize > count) ? count - startIndex : groupSize;
result.Add(valueList.GetRange(startIndex, elementCount));
startIndex += elementCount;
}
}
return result;
}
Bien que beaucoup des réponses ci-dessus fassent le travail, elles échouent toutes horriblement sur une séquence sans fin (ou une très longue séquence). Ce qui suit est une implémentation entièrement en ligne qui garantit la meilleure complexité possible en temps et en mémoire. Nous n'itérons le nom de la source énumérable qu'une seule fois et utilisons le rendement du rendement pour une évaluation paresseuse. Le consommateur peut jeter la liste à chaque itération, ce qui rend l’empreinte mémoire égale à celle de la liste w/batchSize
nombre d’éléments.
public static IEnumerable<List<T>> BatchBy<T>(this IEnumerable<T> enumerable, int batchSize)
{
using (var enumerator = enumerable.GetEnumerator())
{
List<T> list = null;
while (enumerator.MoveNext())
{
if (list == null)
{
list = new List<T> {enumerator.Current};
}
else if (list.Count < batchSize)
{
list.Add(enumerator.Current);
}
else
{
yield return list;
list = new List<T> {enumerator.Current};
}
}
if (list?.Count > 0)
{
yield return list;
}
}
}
EDIT: Je viens juste de comprendre que le PO demande de briser un List<T>
en un List<T>
plus petit. Par conséquent, mes commentaires concernant les enumerables infinis ne sont pas applicables au PO mais peuvent aider les autres. Ces commentaires faisaient suite à d'autres solutions publiées qui utilisaient IEnumerable<T>
comme entrée de leur fonction, mais énuméraient la source énumérable plusieurs fois.
Ajout après le commentaire très utile de mhand à la fin
Bien que la plupart des solutions fonctionnent, je pense qu’elles ne sont pas très efficaces. Supposons que vous souhaitiez uniquement les quelques premiers éléments des premiers morceaux. Dans ce cas, vous ne voudriez pas parcourir tous les éléments (zillions) de votre séquence.
Au maximum, les éléments suivants seront énumérés deux fois: une pour la prise et une autre pour le saut. Il n'énumérera pas plus d'éléments que vous n'utiliserez:
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
(this IEnumerable<TSource> source, int chunkSize)
{
while (source.Any()) // while there are elements left
{ // still something to chunk:
yield return source.Take(chunkSize); // return a chunk of chunkSize
source = source.Skip(chunkSize); // skip the returned chunk
}
}
Supposons que vous divisiez votre source en morceaux de chunkSize
. Vous n'énumérez que les N premiers morceaux. À partir de chaque morceau énuméré, vous n'énumérerez que les premiers M éléments.
While(source.Any())
{
...
}
n'importe quel obtiendra l'énumérateur, faites 1 MoveNext () et retourne la valeur renvoyée après la mise au rebut de l'énumérateur. Ce sera fait N fois
yield return source.Take(chunkSize);
Selon la source de référence cela fera quelque chose comme:
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
return TakeIterator<TSource>(source, count);
}
static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}
Cela ne fait pas beaucoup jusqu'à ce que vous commenciez à énumérer sur le Chunk récupéré. Si vous récupérez plusieurs morceaux, mais décidez de ne pas énumérer le premier morceau, foreach n'est pas exécuté, comme le débogueur vous le montrera.
Si vous décidez de prendre les M premiers éléments du premier bloc, le rendement est exécuté exactement M fois. Ça signifie:
Une fois que le premier morceau a été renvoyé, nous sautons ce premier morceau:
source = source.Skip(chunkSize);
Encore une fois: nous allons jeter un oeil à source de référence pour trouver le skipiterator
static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (count > 0 && e.MoveNext()) count--;
if (count <= 0)
{
while (e.MoveNext()) yield return e.Current;
}
}
}
Comme vous le voyez, la SkipIterator
appelle MoveNext()
une fois pour chaque élément du bloc. Il n'appelle pas Current
.
Donc, par morceau, nous voyons que ce qui suit est fait:
Prendre():
Si le contenu est énuméré: GetEnumerator (), un MoveNext et un courant par élément énuméré, Dispose enumerator;
Skip (): pour chaque bloc énuméré (PAS le contenu du bloc): GetEnumerator (), MoveNext () chunkSize fois, pas de courant! Éliminer l'énumérateur
Si vous regardez ce qui se passe avec l'énumérateur, vous verrez qu'il y a beaucoup d'appels à MoveNext () et uniquement à Current
pour les éléments TSource auxquels vous décidez réellement d'accéder.
Si vous prenez N morceaux de taille chunkSize, alors appelez MoveNext ()
Si vous décidez de n'énumérer que les M premiers éléments de chaque bloc extrait, vous devez appeler MoveNext M fois par bloc énuméré.
Le total
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Donc, si vous décidez d’énumérer tous les éléments de tous les morceaux:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Que MoveNext nécessite beaucoup de travail ou non dépend du type de séquence source. Pour les listes et les tableaux, il s’agit d’un simple incrément d’index, avec éventuellement une vérification hors limites.
Mais si votre IEnumerable est le résultat d’une interrogation de base de données, assurez-vous que les données sont bien matérialisées sur votre ordinateur, sinon les données seront extraites plusieurs fois. DbContext et Dapper transféreront correctement les données au processus local avant de pouvoir y accéder. Si vous énumérez plusieurs fois la même séquence, elle ne sera pas extraite plusieurs fois. Dapper retourne un objet qui est une liste, DbContext se souvient que les données sont déjà extraites.
Cela dépend de votre référentiel s'il est sage d'appeler AsEnumerable () ou ToLists () avant de commencer à diviser les éléments de Chunks.
public static IEnumerable<IEnumerable<T>> SplitIntoSets<T>
(this IEnumerable<T> source, int itemsPerSet)
{
var sourceList = source as List<T> ?? source.ToList();
for (var index = 0; index < sourceList.Count; index += itemsPerSet)
{
yield return sourceList.Skip(index).Take(itemsPerSet);
}
}
La bibliothèque MoreLinq a une méthode appelée Batch
List<int> ids = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 10 elements
int counter = 1;
foreach(var batch in ids.Batch(2))
{
foreach(var eachId in batch)
{
Console.WriteLine("Batch: {0}, Id: {1}", counter, eachId);
}
counter++;
}
Le résultat est
Batch: 1, Id: 1
Batch: 1, Id: 2
Batch: 2, Id: 3
Batch: 2, Id: 4
Batch: 3, Id: 5
Batch: 3, Id: 6
Batch: 4, Id: 7
Batch: 4, Id: 8
Batch: 5, Id: 9
Batch: 5, Id: 0
ids
sont divisés en 5 morceaux avec 2 éléments.
Celui-ci, ça va? L'idée était d'utiliser une seule boucle. Et, qui sait, vous utilisez peut-être uniquement des implémentations IList dans votre code et vous ne souhaitez pas utiliser la conversion vers List.
private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
IList<T> auxList = new List<T>();
int totalItems = list.Count();
if (totalChunks <= 0)
{
yield return auxList;
}
else
{
for (int i = 0; i < totalItems; i++)
{
auxList.Add(list[i]);
if ((i + 1) % totalChunks == 0)
{
yield return auxList;
auxList = new List<T>();
}
else if (i == totalItems - 1)
{
yield return auxList;
}
}
}
}
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}
public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize)
{
var result = new List<List<T>>();
for (int i = 0; i < source.Count; i += chunkSize)
{
var rows = new List<T>();
for (int j = i; j < i + chunkSize; j++)
{
if (j >= source.Count) break;
rows.Add(source[j]);
}
result.Add(rows);
}
return result;
}
Un de plus
public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
var chunks = new List<IList<T>>();
List<T> chunk = null;
for (var i = 0; i < list.Count; i++)
{
if (i % chunkSize == 0)
{
chunk = new List<T>(chunkSize);
chunks.Add(chunk);
}
chunk.Add(list[i]);
}
return chunks;
}