I need to get an element from an IEnumerable and then return itself and a range of elements on e开发者_如何学Goither 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
The following gives correct answers for non-linear sequences and is efficient, e.g:
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 600, 700, 800, 900, 1000 };
IEnumerable<int> range = enumerable.PivotRange(PivotValue, RangeSize);}
//3, 4, 5, 600, 700.
Code - Generic Version
public static IEnumerable<T> PivotRange<T>(
this IEnumerable<T> source, T pivot, int size) where T : IComparable<T>
{
T[] left = new T[size];
int lCount = 0, rCount = 0;
IEnumerator<T> enumerator = source.GetEnumerator();
while (enumerator.MoveNext())
{
T item = enumerator.Current;
if (item.CompareTo(pivot) == 0)
{
int start = lCount > size ? lCount % size : 0;
int end = Math.Min(size, lCount);
for (int i = start; i < start + end; i++)
yield return left[i % size];
yield return pivot;
while (enumerator.MoveNext() && rCount++ < size)
yield return enumerator.Current;
break;
}
if (size <= 0) continue;
left[lCount++ % size] = item;
}
}
Update - Unit Tests
[Test]
public void Linear()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 3, 4, 5, 6, 7 }, range);
}
[Test]
public void NonLinear()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 0, 1, 2, 3, 4, 5, 600, 700, 800, 900, 1000 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 3, 4, 5, 600, 700 }, range);
}
[Test]
public void NoLeft()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 5, 600, 700, 800, 900, 1000 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 5, 600, 700 }, range);
}
[Test]
public void NoRight()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 0, 1, 2, 3, 4, 5 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 3, 4, 5 }, range);
}
[Test]
public void ZeroRange()
{
const int PivotValue = 5;
const int RangeSize = 0;
int[] enumerable = new[] { 0, 1, 2, 3, 4, 5 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 5 }, range);
}
[Test]
public void LeftShorterThanRange()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 4, 5, 6, 7, 8 };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 4, 5, 6, 7 }, range);
}
[Test]
public void RightShorterThanRange()
{
const int PivotValue = 5;
const int RangeSize = 2;
int[] enumerable = new[] { 2, 3, 4, 5, 6, };
int[] range = enumerable.PivotRange(PivotValue, RangeSize).ToArray();
CollectionAssert.AreEqual(new[] { 3, 4, 5, 6 }, range);
}
You can do something like this.
var enumerable = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var rangeSize = 2;
var index = enumerable.FirstOrDefault(x=> x == 2);
var range = enumerable.Skip(index + 1).Take(5);
As far the elements are unique, below function will work for you.
public static IEnumerable<T> MySelectRange<T>(this IEnumerable<T> me, Func<T, bool> pred, int range)
{
var first = me.TakeWhile(i => !pred(i)).TakeLast(range);
var second = me.SkipWhile(i => !pred(i)).Take(range + 1);
return first.Concat(second);
}
NOTE: The TakeLast seems to be from Rx library
Could be as simple as this:
public IEnumerable<T> GetRange<T>(IEnumerable<T> enumerable, int rangeSize, T value)
{
for (int i = 0; i < enumerable.Count(); ++i)
{
if (enumerable.ElementAt(i).Equals(value))
{
for (int j = Math.Max(0, i - rangeSize); j < Math.Min(i + rangeSize + 1, enumerable.Count()); ++j)
{
yield return enumerable.ElementAt(j);
}
}
}
}
}
This is a queue-based version, only I implement my own queue (unlike the others that use Queue
). The advantage of implementing my own queue is that it is just slightly more efficient.
public static IEnumerable<T> MySelectRange<T>(this IEnumerable<T> source,
Func<T, bool> selector,
int rangeSize)
{
var firstN = new T[rangeSize];
int pos = 0;
bool found = false;
foreach (T item in source)
{
if (found)
if (pos++ <= rangeSize)
yield return item;
else
break;
if (selector(item))
{
found = true;
for (int i = Math.Max(0, pos - rangeSize); i < pos; i++)
yield return firstN[i % rangeSize];
yield return item;
pos = 0;
}
else if (rangeSize > 0)
firstN[pos++ % rangeSize] = item;
}
}
精彩评论