2009-06-18 12 views
7

Supposons que j'ai une liste avec des objets de type Value. Value a une Name propriété:Utiliser Linq pour trouver des éléments répétitifs consécutifs

private List<Value> values = new List<Value> { 
    new Value { Id = 0, Name = "Hello" }, 
    new Value { Id = 1, Name = "World" }, 
    new Value { Id = 2, Name = "World" }, 
    new Value { Id = 3, Name = "Hello" }, 
    new Value { Id = 4, Name = "a" }, 
    new Value { Id = 5, Name = "a" }, 
}; 

Maintenant, je veux obtenir une liste de tous les « répéter » les valeurs (éléments où la propriété du nom était identique à la propriété du nom de l'élément précédent).
Dans cet exemple, je souhaite qu'une liste contenant les deux éléments "world" et "a" (id = 2 et 5) soit renvoyée.

Cet événement est-il possible avec linq? Bien sûr, je pourrais si bien. comme ceci:

List<Value> tempValues = new List<Value>(); 
String lastName = String.Empty(); 
foreach (var v in values) 
{ 
    if (v.Name == lastName) tempValues.Add(v); 
    lastName = v.Name; 
} 

mais comme je veux utiliser cette requête dans un contexte plus complexe, peut-être il y a une solution « linqish ».

Répondre

7

Il n'y aura pas rien construit dans ces lignes le long, mais si vous avez besoin de cette fréquence à laquelle vous pourrait rouler quelque chose sur mesure, mais assez générique:

static IEnumerable<TSource> WhereRepeated<TSource>(
    this IEnumerable<TSource> source) 
{ 
    return WhereRepeated<TSource,TSource>(source, x => x); 
} 
static IEnumerable<TSource> WhereRepeated<TSource, TValue>(
    this IEnumerable<TSource> source, Func<TSource, TValue> selector) 
{ 
    using (var iter = source.GetEnumerator()) 
    { 
     if (iter.MoveNext()) 
     { 
      var comparer = EqualityComparer<TValue>.Default; 
      TValue lastValue = selector(iter.Current); 
      while (iter.MoveNext()) 
      { 
       TValue currentValue = selector(iter.Current); 
       if (comparer.Equals(lastValue, currentValue)) 
       { 
        yield return iter.Current; 
       } 
       lastValue = currentValue; 
      } 
     } 
    } 
} 

Utilisation:

foreach (Value value in values.WhereRepeated(x => x.Name)) 
    { 
     Console.WriteLine(value.Name); 
    } 

Vous voudrez peut-être pour réfléchir à ce qu'il faut faire avec les triplets, etc - actuellement, tout sauf le premier sera cédé (ce qui correspond à votre description), mais cela pourrait ne pas être tout à fait raison.

+0

c'est plus efficace que la méthode Zip. mais je trouve que la méthode Zip se lit un peu mieux (c'est beaucoup plus clair que ça) –

+0

+1 en passant, c'est une bonne réponse –

+0

Fonctionne comme un charme –

4

Vous pouvez implémenter un Zip extension, puis compresser votre liste avec .Skip (1), puis sélectionner les lignes correspondantes.

Cela devrait fonctionner et être assez facile à entretenir:

values 
    .Skip(1) 
    .Zip(items, (first,second) => first.Name==second.Name?first:null) 
    .Where(i => i != null); 

Le léger inconvénient de cette méthode est que vous itérer dans la liste deux fois.

+0

solution Grande, aussi. la performance n'est pas un problème dans mon cas (juste quelques centaines d'éléments). –

-1

Vous pouvez utiliser l'extension GroupBy pour cela.

+1

Pouvez-vous élaborer avec du code, s'il vous plaît? –

1

Je pense que cela fonctionnerait (non testé) - cela vous donnera à la fois le mot répété et son index. Pour les répétitions multiples, vous pouvez parcourir cette liste et vérifier les indices consécutifs.

var query = values.Where((v,i) => values.Count > i+1 && v == values[i+1]) 
        .Select((v,i) => new { Value = v, Index = i }); 
+1

Cela ne me semble pas LINQy ... et ne fonctionne pas contre un général IEnumerable ... –

+0

Nice - J'aime :) @ Sam: qu'est-ce que tu veux dire n'est pas LINQy? C'est assez LINQy à moi :) (ou si vous voulez vraiment obtenir la technologie, Lambday .. qui peut être fait LINQy dans une fraction de seconde) :) –

+0

@Pure, si les valeurs sont purement IEnumerable (et non IList) alors cela ne ne fonctionne pas, c'est donc une vraie solution spécifique qui ne fonctionne qu'avec IList. Il correspond cependant à la spécification et faire le travail. –

-1

Quelque chose comme ça

var dupsNames = 
    from v in values 
    group v by v.Name into g 
    where g.Count > 1 // If a group has only one element, just ignore it 
    select g.Key; 

devrait fonctionner. Vous pouvez ensuite utiliser les résultats dans une seconde requête:

dupsNames.Select(d => values.Where(v => v.Name == d)) 

Cela devrait renvoyer un groupement avec key = nom, les valeurs = {éléments avec un nom}

Disclaimer: Je n'ai pas testé ce qui précède, je peut être loin.

+1

Cela va tirer n'importe quoi avec des doublons, pas seulement des répétitions consécutives. – tvanfosson

1

Voici une autre approche simple qui devrait fonctionner si les ID sont toujours séquentielle comme dans votre exemple:

var data = from v2 in values 
      join v1 in values on v2.Id equals v1.Id + 1 
      where v1.Name == v2.Name 
      select v2; 
1

Je sais que cette question est ancienne, mais je viens de travailler sur la même chose si ....

static class utils 
{ 
    public static IEnumerable<T> FindConsecutive<T>(this IEnumerable<T> data, Func<T,T,bool> comparison) 
    { 
     return Enumerable.Range(0, data.Count() - 1) 
     .Select(i => new { a=data.ElementAt(i), b=data.ElementAt(i+1)}) 
     .Where(n => comparison(n.a, n.b)).Select(n => n.a); 
    } 
} 

devrait fonctionner pour quoi que ce soit - il suffit de fournir une fonction de comparer les éléments

Questions connexes