Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IEnumerable - Return items in range either side of element

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>.

like image 578
NeilD Avatar asked Aug 22 '11 12:08

NeilD


3 Answers

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);
    }
  }
}
like image 83
Matt Howells Avatar answered Oct 31 '22 20:10

Matt Howells


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();
like image 3
Sorin Comanescu Avatar answered Oct 31 '22 21:10

Sorin Comanescu


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

like image 2
Jamiec Avatar answered Oct 31 '22 21:10

Jamiec