As my answer to this question, I came up with this solution:
static public IEnumerable<T> SkipLast<T>(this IEnumerable<T> data, int count)
{
if (data == null || count < 0) yield break;
Queue<T> queue = new Queue<T>(data.Take(count));
foreach (T item in data.Skip(count))
{
queue.Enqueue(item);
yield return queue.Dequeue();
}
}
It returns all items in the set but the count
last, but without knowing anything about the size of the data set. I think it's funny and that it's working, but a comment claims that is doesn't. Am I overlooking something?
A version with a circular queue could be:
static public IEnumerable<T> SkipLast<T>(this IEnumerable<T> data, int count)
{
if (data == null || count < 0) yield break;
if (count == 0)
{
foreach (T item in data)
yield return item;
}
else
{
T[] queue = data.Take(count).ToArray();
int index = 0;
foreach (T item in data.Skip(count))
{
index %= count;
yield return queue[index];
queue[index] = item;
index++;
}
}
}
Performance wise they seems to be even.
Compared to other solutions like the most obvious:
data.Reverse().Skip(count).Reverse()
It seems to be at least as fast and for very large set about twice as fast.
Test case:
int count = 20;
var data = Enumerable.Range(1, count);
for (int i = 0; i < count + 5; i++)
{
Console.WriteLine($"Skip: {i} => {(string.Join(", ", data.SkipLast1(i)))}");
}
Any comments are useful.
count
is large, the queue gets large, resulting in a large memory consumption an the overhead of copying the data. \$\endgroup\$1
so your caching techinique with theQueue
is very cool :-) ifcount
it would get so large that it would cause memory issues then there is a much bigger issue with the application logic. Or can you think of any example when it would make sense to useSkipLast(1_000_000)
? \$\endgroup\$