2010-04-08 5 views
6

J'ai un tableau d'éléments où l'élément a une valeur booléenne Flagged.en utilisant Linq pour partitionner des données en tableaux

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

Je veux casser dans des tableaux en fonction de l'indicateur signalé

sortie>

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

Répondre

0

Je ne pense pas que LINQ est adapté pour très bien. Cela pourrait être fait avec Aggregate() mais je pense que vous feriez mieux de simplement boucler avec un foreach() en construisant le résultat.

7

Linq n'a pas un opérateur pour cela, mais je l'ai écrit une méthode d'extension que vous pourriez être en mesure d'utiliser (dans le processus de le soumettre à MoreLinq, que vous devriez également vérifier):

Utilisation de l'opérateur ci-dessous, vous pouvez écrire:

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

Voici le code de la méthode d'extension:

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

Pourquoi la condition dans la fonction lambda ' item.Flagged! = prevItem.Flagged '? Vous créez un nouveau segment si l'indicateur passe de false à true. Je pense que la condition devrait être juste "item.Flagged", et pas dépendant de l'élément précédent du tout. –

+0

@phild: Vous avez raison, 'item.Flagged' est suffisant. J'étais pressé plus tôt et j'ai mal interprété les attentes du PO. J'ai mis à jour ma réponse. – LBushkin

+0

+1, méthode d'extension très utile :) –

1

Je ne pense pas que LINQ est l'outil idéal pour thi s tâche. Qu'en est-ce:

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 petit exemple, avec les données du poste Fábio Batistas:

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

Je pense que cela a besoin d'un output.Add (currentList); après l'instruction foreach, ou votre dernier tableau ne sera jamais ajouté. – Kenoyer130

+0

Je l'ai mis à jour, et en ai fait une fonction générique. –

2

Considérant:

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 } 
}; 

Vous pouvez écrire:

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(); 

Cela fonctionne si vos éléments sont ordere d par l'attribut Id. Si ce n'est pas le cas, vous devrez injecter une séquence sur votre tableau d'origine, ce qui devrait aussi être facile avec linq, donc vous obtiendrez une séquence.

En outre, une meilleure alternative devrait être:

// 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(); 

Notez que ces réponses utilisent LINQ pure.

+0

J'aime beaucoup Linq et je l'utilise où je peux, mais ne pensez-vous pas que c'est plutôt moche par rapport à l'approche classique (voir mon article)? Je pense même que cela va être pire dans la performance. –

+0

Je l'ai un peu développé, pour supprimer le besoin de les commander par ID. Mais je suis d'accord, c'est pire sur la performance. J'ai aimé la réponse @LBushkin, en créant une nouvelle méthode d'extension pour cela. –

+0

+1 J'aime votre deuxième alternative. C'est bien de voir combien de tâches peuvent être faites avec linq pur, que je n'aurais même pas pensé à faire en linq. –

5

J'ai eu un problème similaire avec ceci, et l'ai résolu en utilisant GroupBy et la fermeture.

//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; 
     }); 

Ce qu'il fait est le regroupement sur un int (flagCounter), qui augmente chaque fois qu'un élément Signalé se trouve.
Veuillez noter que cela ne fonctionnera pas avec AsParallel().

Test des résultats:

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

Sorties:

Groupe: 123
Groupe: 4567
Groupe: 89

+0

Approche très intéressante. Très différent des autres solutions, mais vous savez, beaucoup de routes mènent à Rome ... –

+0

Bien qu'intéressant, je pense que c'est un peu "hackish" d'introduire l'état à la requête. Le résultat final, cependant, est sympa (un groupement linq). Peut-être y a-t-il un moyen de le faire sans la fermeture? –

+0

@ Fábio: Les fermetures sont un excellent ajout à C#, et sont plus adaptés pour une utilisation dans les délégués (comme dans ce cas). Mes connaissances ne me permettent pas de les utiliser pour l'instant, mais ce code est aussi lisible que performant et maintenable. –

Questions connexes