2010-04-08 12 views
6

Tengo una matriz de elementos donde el elemento tiene un valor booleano señalado.utilizando Linq para particionar datos en matrices

1 flagged 
2 not flagged 
3 not flagged 
4 flagged 
5 not flagged 
6 not flagged 
7 not flagged 
8 flagged 
9 not flagged 

Quiero romperla en matrices basadas en el indicador marcado

de salida>

array 1 {1,2,3} 
array 2 {4,5,6,7} 
array 3 {8,9} 

Respuesta

0

No creo que LINQ es adecuado para esto muy bien. Se podría hacer con Aggregate(), pero creo que sería mejor simplemente iterar con un foreach() para construir el resultado.

7

LINQ no tiene un operador para esto, pero he escrito un método de extensión que puede ser capaz de utilizar (en el proceso de someterlo a MoreLinq, que también debe salir):

mediante el siguiente operador, podría escribir:

var result = 
    items.Segment((item,prevItem,idx) => item.Flagged) 
     .Select(seq => seq.ToArray()) // converts each sequence to an array 
     .ToList(); 

Aquí está el código del método de extensión:

public static IEnumerable<IEnumerable<T>> Segment<T>(IEnumerable<T> sequence, Func<T, T, int, bool> newSegmentIdentifier) 
    { 
     var index = -1; 
     using (var iter = sequence.GetEnumerator()) 
     { 
      var segment = new List<T>(); 
      var prevItem = default(T); 

      // ensure that the first item is always part 
      // of the first segment. This is an intentional 
      // behavior. Segmentation always begins with 
      // the second element in the sequence. 
      if (iter.MoveNext()) 
      { 
       ++index; 
       segment.Add(iter.Current); 
       prevItem = iter.Current; 
      } 

      while (iter.MoveNext()) 
      { 
       ++index; 
       // check if the item represents the start of a new segment 
       var isNewSegment = newSegmentIdentifier(iter.Current, prevItem, index); 
       prevItem = iter.Current; 

       if (!isNewSegment) 
       { 
        // if not a new segment, append and continue 
        segment.Add(iter.Current); 
        continue; 
       } 
       yield return segment; // yield the completed segment 

       // start a new segment... 
       segment = new List<T> { iter.Current }; 
      } 
      // handle the case of the sequence ending before new segment is detected 
      if (segment.Count > 0) 
       yield return segment; 
     } 
    } 
+0

¿Por qué es la condición en la función lambda ' item.Flagged! = prevItem.Flagged '? Debería crear un nuevo segmento si la bandera cambia de falso a verdadero. Creo que la condición debería ser solo 'item.Flagged', y no dependiente del ítem anterior en absoluto. –

+0

@phild: tiene razón, 'item.Flagged' es suficiente. Tenía prisa antes y malinterpreté las expectativas del OP. He actualizado mi respuesta. – LBushkin

+0

+1, método de extensión muy útil :) –

1

no creo que LINQ es la herramienta adecuada para thi s tarea. ¿Qué hay de esto:

public static List<List<T>> PartitionData<T>(T[] arr, Func<T, bool> flagSelector){ 
    List<List<T>> output = new List<List<T>>(); 
    List<T> partition = null; 
    bool first = true; 

    foreach(T obj in arr){ 
     if(flagSelector(obj) || first){ 
      partition = new List<T>(); 
      output.Add(partition); 
      first = false; 
     } 
     partition.Add(obj); 
    } 

    return output; 
} 

Un pequeño ejemplo, con los datos de Fábio Batista mensaje:

var arrayOfElements = new[] { 
    new { Id = 1, Flagged = true }, 
    new { Id = 2, Flagged = false }, 
    new { Id = 3, Flagged = false }, 
    new { Id = 4, Flagged = true }, 
    new { Id = 5, Flagged = false }, 
    new { Id = 6, Flagged = false }, 
    new { Id = 7, Flagged = false }, 
    new { Id = 8, Flagged = true }, 
    new { Id = 9, Flagged = false } 
}; 

var partitioned = PartitionData(arrayOfElements, x => x.Flagged); 
+0

Creo que esto necesita un output.Add (currentList); después de la declaración foreach, o su última matriz nunca se agrega. – Kenoyer130

+0

Lo actualicé e hice una función genérica de él. –

2

Considerando:

var arrayOfElements = new[] { 
    new { Id = 1, Flagged = true }, 
    new { Id = 2, Flagged = false }, 
    new { Id = 3, Flagged = false }, 
    new { Id = 4, Flagged = true }, 
    new { Id = 5, Flagged = false }, 
    new { Id = 6, Flagged = false }, 
    new { Id = 7, Flagged = false }, 
    new { Id = 8, Flagged = true }, 
    new { Id = 9, Flagged = false } 
}; 

puede escribir:

var grouped = 
    from i in arrayOfElements 
    where i.Flagged 
    select 
     (new[] { i.Id }) 
     .Union(arrayOfElements.Where(i2 => i2.Id > i.Id).TakeWhile(i2 => !i2.Flagged).Select(i2 => i2.Id)) 
     .ToArray(); 

Esto funciona si tus elementos son ordere d por el atributo Id. Si no lo hacen, tendrás que inyectar una secuencia en tu matriz original, que también debería ser fácil de hacer con linq, por lo que obtendrás una secuencia.

Además, una mejor alternativa debe ser:

// for each flagged element, slice the array, 
// starting on the flagged element until the next flagged element 
var grouped = 
    from i in arrayOfElements 
    where i.Flagged 
    select 
     arrayOfElements 
      .SkipWhile(i2 => i2 != i) 
      .TakeWhile(i2 => i2 == i || !i2.Flagged) 
      .Select(i2 => i2.Id) 
      .ToArray(); 

Tenga en cuenta que esas respuestas están utilizando LINQ puro.

+0

Me gusta mucho Linq, y lo uso donde puedo, pero ¿no crees que esto es bastante feo en comparación con el enfoque clásico (ver mi publicación)? Incluso creo que esto va a ser peor en rendimiento. –

+0

Lo desarrollé un poco, para eliminar la necesidad de ordenarlos por Id. Pero estoy de acuerdo, es peor en el rendimiento. Me gustó la respuesta @LBushkin, creando un nuevo método de extensión para eso. –

+0

+1 Me gusta tu segunda alternativa. Es bueno ver cuántas tareas se pueden hacer con pure linq, que ni siquiera hubiera pensado hacer en linq. –

5

Tuve un problema similar con esto, y lo resolví usando GroupBy y cierre.

//sample data 
var arrayOfElements = new[] { 
    new { Id = 1, Flagged = true }, 
    new { Id = 2, Flagged = false }, 
    new { Id = 3, Flagged = false }, 
    new { Id = 4, Flagged = true }, 
    new { Id = 5, Flagged = false }, 
    new { Id = 6, Flagged = false }, 
    new { Id = 7, Flagged = false }, 
    new { Id = 8, Flagged = true }, 
    new { Id = 9, Flagged = false } 
}; 

//this is the closure which will increase each time I see a flagged 
int flagCounter = 0; 

var query = 
    arrayOfElements.GroupBy(e => 
     { 
      if (e.Flagged) 
       flagCounter++; 
      return flagCounter; 
     }); 

lo que hace es agrupar en un int (flagCounter), que se incrementa cada vez que se encuentra un elemento marcado.
Tenga en cuenta que esto no funcionará con AsParallel().

Prueba los resultados:

foreach(var group in query) 
{ 
    Console.Write("\r\nGroup: "); 
    foreach (var element in group) 
     Console.Write(element.Id); 
} 

Salidas:

Grupo: 123
Grupo: 4567
Grupo: 89

+0

enfoque muy interesante. Muy diferente a las otras soluciones, pero ya sabes, muchos caminos conducen a Roma ... –

+0

Aunque es interesante, creo que es un poco "hackish" introducir el estado de la consulta. El resultado final, sin embargo, es agradable (una agrupación linq). Tal vez hay una manera de hacerlo sin el cierre? –

+0

@ Fábio: los cierres son una gran adición a C#, y son más adecuados para su uso en los delegados (como en este caso). Mi conocimiento no me puede llevar a una solución segura para subprocesos todavía, pero seguramente este código es tan fácil de leer como de rendimiento y mantenible. –

Cuestiones relacionadas