Reputation: 48392
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
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
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
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
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