LINQ to find series of consecutive numbers
I have a list of integers. I want to find all runs of consecutive numbers on that list, defined by the start index and length. So for example, for input list of [1,2,3,5,7,8]
, the output would be [{1,3}, {5,1}, {7,2}]
. This is easy enough to do using a loop, something like this (untested pseudocode):
for(i=1, i < maxNum; i++)
{
number = list[i];
previousNumber = list[i-1];
if(number - previousNumber == 1)
{
runLength++;
}
else
{
result.Add(startingNumber, runLength);
runLength = 1;
startingNumber = number;
}
}
But I thought it would be possible to do using LINQ. Any ideas how to do that?
A linqish way can be writing an extension method GroupWhile
like below (All checks omitted. not optimized to understand easily.)
int[] list = new int[] { 1, 2, 3, 5, 7, 8 };
var result = list.GroupWhile((x, y) => y - x == 1)
.Select(x => new {i = x.First(), len = x.Count() })
.ToList();
public static IEnumerable<IEnumerable<T>> GroupWhile<T>(this IEnumerable<T> seq, Func<T,T,bool> condition)
{
T prev = seq.First();
List<T> list = new List<T>() { prev };
foreach(T item in seq.Skip(1))
{
if(condition(prev,item)==false)
{
yield return list;
list = new List<T>();
}
list.Add(item);
prev = item;
}
yield return list;
}
TODO: use IGrouping
:)
This seems like a reasonable approach:
- Zip the original list with a
Range
, so each element is tupled with its index - Select those elements whose list predecessor is not their natural predecessor
- Convert to array and save to temporary variable (to facilitate the last step).
- Deduce the length of the subarrays from the indices. For the last item it is the difference with the original list length. For the other items it is the difference with the next index.
var list = new int[] { 1, 2, 3, 5, 7, 8 };
var filtered = list.Zip(Enumerable.Range(0, list.Length), Tuple.Create)
.Where((x, i) => i == 0 || list[i - 1] != x.Item1 - 1).ToArray();
var result = filtered.Select((x, i) => i == filtered.Length - 1
? Tuple.Create(x.Item1, list.Length - x.Item2)
: Tuple.Create(x.Item1, filtered[i + 1].Item2 - x.Item2));
foreach (var t in result)
{
Console.WriteLine(t);
}
This results in
(1, 3)
(5, 1)
(7, 2)
Did someone ask to shoehorn a solution into a LINQ query of dubious readability?
var serieses = input.Aggregate(
new List<Tuple<int, int>>(),
(l, i) =>
{
var last = l.LastOrDefault();
if (last == null ||
last.Item1 + last.Item2 != i)
{
l.Add(new Tuple<int, int>(i, 1));
}
else if (last.Item1 + last.Item2 == i)
{
l.RemoveAt(l.Count - 1);
l.Add(new Tuple<int, int>(last.Item1, last.Item2 + 1));
}
return l;
},
l => l);
There is no such out of box extension method, but you can create you own:
public static class LinqUtils{
public class ConsecutiveGroup<T>
{
public T Left { get; internal set; }
public T Right { get; internal set; }
public long Count { get; internal set; }
}
public static IEnumerable<ConsecutiveGroup<T>> ConsecutiveCounts<T>(this IEnumerable<T> src, Func<T, T, bool> consecutive)
{
ConsecutiveGroup<T> current = null;
foreach (var s in src)
{
if (current==null)
{
current = new ConsecutiveGroup<T>
{
Left = s,
Right = s,
Count = 1
};
continue;
}
if(consecutive(current.Right, s))
{
current.Right = s;
current.Count += 1;
continue;
}
yield return current;
current = new ConsecutiveGroup<T>
{
Left = s,
Right = s,
Count = 1
};
}
if (current!=null)
{
yield return current;
}
}
}
[TestFixture]
public static class LinqUtilsTests
{
[Test]
public void TestConsecutiveCounts()
{
var src = new[] {1,2,3,5,7,8};
var expected = new[]
{
Tuple.Create<int,long>(1, 3),
Tuple.Create<int,long>(5, 1),
Tuple.Create<int,long>(7, 2)
};
var result = src
.ConsecutiveCounts((prev, current) => current == (prev + 1))
.Select(c=>Tuple.Create(c.Left, c.Count));
Assert.IsTrue(expected.SequenceEqual(result));
}
}