Jürgen Steinblock
Jürgen Steinblock

Reputation: 31723

Use Linq to find consecutively repeating elements

Let's assume I have a list with objects of type Value. Value has a Name property:

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

Now I want to get a list of all "repeating" values (elements where the name property was identical with the name property of the previous element).
In this example I want a list with the two elements "world" and "a" (id = 2 and 5) to be returned.

Is this event possible with linq? Of course I could so smth. like this:

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

but since I want to use this query in a more complex context, maybe there is a "linqish" solution.

Upvotes: 7

Views: 4118

Answers (7)

Kelly Robins
Kelly Robins

Reputation: 7288

I know this question is ancient but I was just working on the same thing so ....

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

Should work for anything - just provide a function to compare the elements

Upvotes: 1

Chris W
Chris W

Reputation: 3314

Here's another simple approach that should work if the IDs are always sequential as in your sample:

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

Upvotes: 1

Sam Saffron
Sam Saffron

Reputation: 131102

You could implement a Zip extension, then Zip your list with .Skip(1) and then Select the rows that match.

This should work and be fairly easy to maintain:

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

The slight disadvantage of this method is that you iterate through the list twice.

Upvotes: 4

Lennaert
Lennaert

Reputation: 2465

Something like this

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;

should work. You can then use the results in a second query:

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

This should return a grouping with key=name, values = { elements with name }

Disclaimer: I did not test the above, so I may be way off.

Upvotes: -1

tvanfosson
tvanfosson

Reputation: 532445

I think this would work (untested) -- this will give you both the repeated word and it's index. For multiple repeats you could traverse this list and check for consecutive indices.

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

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1062512

There won't be anything built in along those lines, but if you need this frequently you could roll something bespoke but fairly generic:

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

Usage:

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

You might want to think about what to do with triplets etc - currently everything except the first will be yielded (which matches your description), but that might not be quite right.

Upvotes: 7

Derek Ekins
Derek Ekins

Reputation: 11391

You could use the GroupBy extension to do this.

Upvotes: -1

Related Questions