getrange - split c#
Divida una lista en listas más pequeñas de tamaño N (10)
Estoy intentando dividir una lista en una serie de listas más pequeñas.
Mi problema: Mi función para dividir listas no las divide en listas del tamaño correcto. ¿Debería dividirlos en listas de tamaño 30 pero en su lugar las divide en listas de tamaño 114?
¿Cómo puedo hacer que mi función divida una lista en un número X de listas de tamaño 30 o inferior ?
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 uso la función en una lista de tamaño 144, entonces la salida es:
Índice: 4, Tamaño: 120
Índice: 3, Tamaño: 114
Índice: 2, Tamaño: 114
Índice: 1, Tamaño: 114
Índice: 0, Tamaño: 114
Encuentro que la respuesta aceptada (Serj-Tm) es la más robusta, pero me gustaría sugerir una versión genérica.
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;
}
La biblioteca MoreLinq tiene un método llamado 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++;
}
El resultado es
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
se dividen en 5 fragmentos con 2 elementos.
La solución Serj-Tm está bien, también esta es la versión genérica como método de extensión para listas (ponerlo en una clase estática):
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;
}
Si bien muchas de las respuestas anteriores hacen el trabajo, todas fallan horriblemente en una secuencia interminable (o en una secuencia realmente larga). La siguiente es una implementación completamente en línea que garantiza la mejor complejidad de tiempo y memoria posible. Solo iteramos la fuente enumerable exactamente una vez y utilizamos return return para la evaluación perezosa. El consumidor podría descartar la lista en cada iteración haciendo que la huella de memoria sea igual a la de la lista con el número de elementos por batchSize
.
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;
}
}
}
EDITAR: Ahora mismo al darme cuenta de que el OP pregunta por romper una List<T>
en una List<T>
más pequeña List<T>
, entonces mis comentarios con respecto a los enumerables infinitos no son aplicables al PO, pero pueden ayudar a otros que terminan aquí. Estos comentarios fueron en respuesta a otras soluciones publicadas que sí usan IEnumerable<T>
como una entrada a su función, sin embargo, enumeran la fuente enumerable varias veces.
Sugeriría utilizar este método de extensión para dividir la lista fuente en las sublistas por tamaño de fragmento especificado:
/// <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();
}
}
Por ejemplo, si tira la lista de 18 elementos por 5 elementos por porción, le da la lista de 4 sublistas con los siguientes elementos dentro: 5-5-5-3.
Tengo un método genérico que incluiría cualquier tipo de flotador, y ha sido probado en unidades, espero que ayude:
/// <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;
}
qué tal si:
while(locations.Any())
{
list.Add(locations.Take(nSize).ToList());
locations= locations.Skip(nSize).ToList();
}
Adición después de un comentario muy útil de mhand al final
Respuesta original
Aunque la mayoría de las soluciones podrían funcionar, creo que no son muy eficientes. Supongamos que solo quiere los primeros elementos de los primeros trozos. Entonces no querrá iterar sobre todos los elementos (trillón) en su secuencia.
Lo siguiente enumerará al máximo dos veces: una para Take y otra para Skip. No enumerará más elementos de los que utilizará:
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
}
}
¿Cuántas veces esto Enumerar la secuencia?
Supongamos que divide su fuente en fragmentos de chunkSize
. Usted enumera solo los primeros N fragmentos. De cada porción enumerada, solo enumerarás los primeros M elementos.
While(source.Any())
{
...
}
el Cualquiera obtendrá el Enumerador, haga 1 MoveNext () y devuelve el valor devuelto después de Dispose the Enumerator. Esto se hará N veces
yield return source.Take(chunkSize);
De acuerdo con la fuente de referencia, esto hará algo como:
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;
}
}
Esto no funciona mucho hasta que empiece a enumerar sobre el fragmento obtenido. Si obtiene varios fragmentos, pero decide no enumerarlos en el primer fragmento, el foreach no se ejecuta, como lo mostrará su depurador.
Si decide tomar los primeros M elementos del primer fragmento, el retorno de rendimiento se ejecuta exactamente M veces. Esto significa:
- obtener el enumerador
- llama a MoveNext () y M actual.
- Eliminar el enumerador
Después de que se devuelve el primer fragmento, salteamos este primer fragmento:
source = source.Skip(chunkSize);
Una vez más: echaremos un vistazo a la fuente de referencia para encontrar el 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;
}
}
}
Como puede ver, SkipIterator
llama a MoveNext()
una vez por cada elemento en el fragmento. No llama Current
.
Entonces, por Chunk vemos que se hace lo siguiente:
- Any (): GetEnumerator; 1 MoveNext (); Eliminar el enumerador;
Tomar():
- nada si el contenido del fragmento no está enumerado.
Si el contenido está enumerado: GetEnumerator (), un MoveNext y un Current por cada elemento enumerado, Dispose enumerator;
Omitir (): para cada fragmento que se enumera (NO el contenido del fragmento): GetEnumerator (), MoveNext () chunkSize veces, no Current! Eliminar el enumerador
Si observa lo que sucede con el enumerador, verá que hay muchas llamadas a MoveNext () y solo llamadas a Current
para los elementos de TSource a los que realmente decide acceder.
Si toma N fragmentos de tamaño chunkSize, luego llama a MoveNext ()
- N veces para Any ()
- todavía no hay tiempo para Take, siempre y cuando no enumere los trozos
- N veces chunkSize para Skip ()
Si decide enumerar solo los primeros M elementos de cada fragmento recuperado, debe llamar a MoveNext M veces por Chunk enumerado.
El total
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Entonces, si decides enumerar todos los elementos de todos los fragmentos:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Si MoveNext es mucho trabajo o no, depende del tipo de secuencia fuente. Para listas y matrices, es un simple incremento de índice, con tal vez una verificación fuera de rango.
Pero si su IEnumerable es el resultado de una consulta a la base de datos, asegúrese de que los datos realmente se materialicen en su computadora, de lo contrario, los datos se obtendrán varias veces. DbContext y Dapper transferirán correctamente los datos al proceso local antes de poder acceder a ellos. Si enumera la misma secuencia varias veces, no se busca varias veces. Dapper devuelve un objeto que es una lista, DbContext recuerda que los datos ya se obtuvieron.
Depende de su Repositorio si es aconsejable llamar a AsEnumerable () o ToLists () antes de comenzar a dividir los elementos en 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);
}
}
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;
}
Versión genérica:
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));
}
}