Randy Minder
Randy Minder

Reputation: 48392

Finding Consecutive Items in List using Linq

Say I have the following array of integers:

int[] numbers = { 1, 6, 4, 10, 9, 12, 15, 17, 8, 3, 20, 21, 2, 23, 25, 27, 5, 67,33, 13, 8, 12, 41, 5 };

How could I write a Linq query that finds 3 consecutive elements that are, say, greater than 10? Also, it would be nice if I could specify I want say the first, second, third etc. group of such elements.

For example, the Linq query should be able to identify: 12,15,17 as the first group of consecutive elements 23,25,27 as the second group 67,33,13 as the third group

The query should return to me the 2nd group if I specify I want the 2nd group of 3 consecutive elements.

Thanks.

Upvotes: 7

Views: 6873

Answers (4)

Michael
Michael

Reputation: 1

I had to do this for a list of doubles. There is an upper as well as a lower limit. This is also not a true Linq solution, it is just a pragmatic approach I wrote this in scripting language that only implements a subset of C#.

var sequence =
 [0.25,0.5,0.5,0.5,0.7,0.8,0.7,0.9,0.5,0.5,0.8,0.8,0.5,0.5,0.65,0.65,0.65,0.65,0.65,0.65,0.65];
double lowerLimit = 0.1;
double upperLimit = 0.6;
int minWindowLength = 3;

// return type is a list of lists
var windows = [[0.0]];
windows.Clear();

int consec = 0;
int index = 0;

while (index < sequence.Count){

        // store segments here
        var window = new System.Collections.Generic.List<double>();

        while ((index < sequence.Count) && (sequence[index] > upperLimit || sequence[index] < lowerLimit)) {        
            window.Add(sequence[index]);
            consec = consec + 1;
            index = index +1;
        }

        if (consec > minWindowLength) {
            windows.Add(window);
        }

        window = new System.Collections.Generic.List<double>();
        consec = 0;

        index = index+1;
}

return windows;

Upvotes: 0

Jim
Jim

Reputation: 4950

UPDATE: While not technically a "linq query" as Patrick points out in the comments, this solution is reusable, flexible, and generic.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication32
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] numbers = { 1, 6, 4, 10, 9, 12, 15, 17, 8, 3, 20, 21, 2, 23, 25, 27, 5, 67,33, 13, 8, 12, 41, 5 };

            var consecutiveGroups = numbers.FindConsecutiveGroups((x) => x > 10, 3);

            foreach (var group in consecutiveGroups)
            {
                Console.WriteLine(String.Join(",", group));
            }
        }        
    }

    public static class Extensions
    {
        public static IEnumerable<IEnumerable<T>> FindConsecutiveGroups<T>(this IEnumerable<T> sequence, Predicate<T> predicate, int count)
        {
            IEnumerable<T> current = sequence;

            while (current.Count() > count)
            {
                IEnumerable<T> window = current.Take(count);

                if (window.Where(x => predicate(x)).Count() >= count)
                    yield return window;

                current = current.Skip(1);
            }
        }
    }
}

Output:

12,15,17
23,25,27
67,33,13 

To get the 2nd group, change:

var consecutiveGroups = numbers.FindConsecutiveGroups((x) => x > 10, 3);

To:

var consecutiveGroups = numbers.FindConsecutiveGroups((x) => x > 10, 3).Skip(1).Take(1);

UPDATE 2 After tweaking this in our production use, the following implementation is far faster as the count of items in the numbers array grows larger.

public static IEnumerable<IEnumerable<T>> FindConsecutiveGroups<T>(this IEnumerable<T> sequence, Predicate<T> predicate, int sequenceSize)
{
    IEnumerable<T> window = Enumerable.Empty<T>();

    int count = 0;

    foreach (var item in sequence)
    {
        if (predicate(item))
        {
            window = window.Concat(Enumerable.Repeat(item, 1));
            count++;

            if (count == sequenceSize)
            {
                yield return window;
                window = window.Skip(1);
                count--;
            }
        }
        else
        {
            count = 0;
            window = Enumerable.Empty<T>();
        }
    }
}

Upvotes: 12

as-cii
as-cii

Reputation: 13019

Why don't you try this extension method?

public static IEnumerable<IEnumerable<T>> Consecutives<T>(this IEnumerable<T> numbers, int ranges, Func<T, bool> predicate)
{
    IEnumerable<T> ordered = numbers.OrderBy(a => a).Where(predicate);
    decimal n = Decimal.Divide(ordered.Count(), ranges);
    decimal max = Math.Ceiling(n); // or Math.Floor(n) if you want
    return from i in Enumerable.Range(0, (int)max)
           select ordered.Skip(i * ranges).Take(ranges);
}

The only thing to improve could be the call to Count method because causes the enumeration of numbers (so the query loses its laziness).

Anyway I'm sure this could fit your linqness requirements.

EDIT: Alternatively this is the less words version (it doesn't make use of Count method):

public static IEnumerable<IEnumerable<T>> Consecutives<T>(this IEnumerable<T> numbers, int ranges, Func<T, bool> predicate)
{
    var ordered = numbers.OrderBy(a => a);
    return ordered.Where(predicate)
                  .Select((element, i) => ordered.Skip(i * ranges).Take(ranges))
                  .TakeWhile(Enumerable.Any);
}

Upvotes: 0

Aducci
Aducci

Reputation: 26634

int[] numbers = { 1, 6, 4, 10, 9, 12, 15, 17, 8, 3, 20, 21, 2, 23, 25, 27, 5, 67, 33, 13, 8, 12, 41, 5 };

var numbersQuery = numbers.Select((x, index) => new { Index = index, Value = x});

var query = from n in numbersQuery
            from n2 in numbersQuery.Where(x => n.Index == x.Index - 1).DefaultIfEmpty()
            from n3 in numbersQuery.Where(x => n.Index == x.Index - 2).DefaultIfEmpty()
            where n.Value > 10
            where n2 != null && n2.Value > 10
            where n3 != null && n3.Value > 10
            select new
            {
              Value1 = n.Value,
              Value2 = n2.Value,
              Value3 = n3.Value
            };

In order to specify which group, you can call the Skip method

query.Skip(1)

Upvotes: 3

Related Questions