I need to get an element from an IEnumerable and then return itself and a range of elements on either side.
So, something like this:
var enumerable = new[] {54, 107, 24, 223, 134, 65, 36, 7342, 812, 96, 106};
var rangeSize = 2;
var range = enumerable.MySelectRange(x => x == 134, rangeSize);
would return something like { 24, 223, 134, 65, 36 }
.
(This project uses .Net 3.5)
EDIT Ok, people seem to be getting hung up on the array of ints. I've changed the example to hopefully make it more clear what I'm after.
Bear in mind that this isn't necessarily for an IEnumerable<int>
, but will actually be an IEnumerable<TSomething>
.
This extension method finds the first element in the sequence satisfying a given predicate, and then returns that element along with a certain number of its neighbouring elements. It handles the end cases.
public static IEnumerable<T> FirstAndNeighbours<T>(
this IEnumerable<T> source,
Func<T,bool> predicate,
int numOfNeighboursEitherSide)
{
using (var enumerator = source.GetEnumerator())
{
var precedingNeighbours = new Queue<T>(numOfNeighboursEitherSide);
while(enumerator.MoveNext())
{
var current = enumerator.Current;
if (predicate(current))
{
//We have found the first matching element. First, we must return
//the preceding neighbours.
foreach (var precedingNeighbour in precedingNeighbours)
yield return precedingNeighbour;
//Next, return the matching element.
yield return current;
//Finally, return the succeeding neighbours.
for (int i = 0; i < numOfNeighboursEitherSide; ++i)
{
if (!enumerator.MoveNext())
yield break;
yield return enumerator.Current;
}
yield break;
}
//No match yet, keep track of this preceding neighbour.
if (precedingNeighbours.Count >= numOfNeighboursEitherSide)
precedingNeighbours.Dequeue();
precedingNeighbours.Enqueue(current);
}
}
}
Assuming you can obtain the index of the middle element:
var enumerable = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int range = 2;
int index = 10;
enumerable.Skip(index-range).Take(range)
.Union(enumerable.Skip(index).Take(1))
.Union(
enumerable.Skip(index+1).Take(range)
).Dump();
(the Dump()
call is for LinqPad)
EDIT:
Thanks to Gabe's comment, got rid of the two extra Skip()
/Take()
:
enumerable.Skip((index < range) ? 0 : index-range)
.Take(((index < range) ? index : range) + range + 1)
.Dump();
EDIT: Updated answer due to question update **
an extension method should do it, now supports any type T
public static IEnumerable<T> Range<T>(this IEnumerable<T> enumerable, Func<T,bool> selector, int size)
{
Queue<T> queue = new Queue<T>();
bool found = false;
int count = 0;
foreach(T item in enumerable)
{
if(found)
{
if(count++ < size)
{
yield return item;
}
else
{
yield break;
}
}
else
{
if(queue.Count>size)
queue.Dequeue();
if(selector(item))
{
found = true;
foreach(var stackItem in queue)
yield return stackItem;
yield return item;
}
else
{
queue.Enqueue(item);
}
}
}
usage is close to what you required
var enumerable = new[] {54, 107, 24, 223, 134, 65, 36, 7342, 812, 96, 106};
Console.WriteLine(String.Join(",",enumerable.ToArray()));
var rangeSize = 2;
var range = enumerable.Range((x) => x == 134, rangeSize);
Console.WriteLine(String.Join(",",range.ToArray()));
Live example: http://rextester.com/rundotnet?code=ACKDD76841
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With