4

We all know that Skip() can omit records that are not needed at the start of a collection.

But is there a way to Skip() records at the end of a collection?

How do you not take the last record in a collection?

Or do you have to do it via Take()

ie, the below code,

var collection = MyCollection

var listCount = collection.Count();

var takeList = collection.Take(listCount - 1);

Is this the only way exclude the last record in a collection?

KyloRen
  • 2,691
  • 5
  • 29
  • 59
  • 1
    There is [MoreLinq](https://www.nuget.org/packages/MoreLinq) with lots of functionalities, and one of them is [SkipLast](https://github.com/morelinq/MoreLINQ/blob/master/MoreLinq/SkipLast.cs) that essentially does the same thing as you did. – Dialecticus Jul 06 '19 at 10:46
  • `SkipLast` also does some extra magic if `Count` cannot be determined without actually counting the enumeration, so the code enumerates only once, instead of twice. – Dialecticus Jul 06 '19 at 10:53
  • @Dialecticus, SkipLast does not show in my MoreLing library. Is it a separate property? – KyloRen Jul 06 '19 at 10:55
  • 1
    It's in `MoreLinq.MoreEnumerable` namespace, like all the rest. If it's not there you may have an old version of the library. You can also just copy-paste the code from the second link I provided. Just have to copy several sources, because there are dependecies on other parts of the library – Dialecticus Jul 06 '19 at 10:58
  • See marked duplicates. Both include generalized "skip last N" implementations, similar to those being proposed below. – Peter Duniho Jul 08 '19 at 00:43
  • @Dialecticus, thanks, I found it. I was not referencing MoreLinq in my test project – KyloRen Jul 08 '19 at 02:12

4 Answers4

5

With enumerator you can efficiently delay yielding by one enumeration.

public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source)
{
    using (IEnumerator<T> e = source.GetEnumerator()) 
    {
        if (e.MoveNext() == false) yield break;

        var current = e.Current;
        while (e.MoveNext())
        {
            yield return current;
            current = e.Current;
        }
    }   
}

Usage

var items = new int[] {};
items.WithoutLast(); // returns empty

var items = new int[] { 1 };
items.WithoutLast(); // returns empty

var items = new int[] { 1, 2 };
items.WithoutLast(); // returns { 1 }

var items = new int[] { 1, 2, 3 };
items.WithoutLast(); // returns { 1, 2 }
Fabio
  • 31,528
  • 4
  • 33
  • 72
  • 1
    Please explain downvote, I would be glad to correct or delete my answer if it is wrong – Fabio Jul 07 '19 at 19:32
  • This appears to be the simplest and most efficient solution. The only change might be to throw if the sequence is empty. – Kit Jul 07 '19 at 19:59
  • @Fabio: But this can only handle to skip the one last. But what about skipping the 5 last items? –  Jul 07 '19 at 20:52
  • 2
    @HenrikHansen, OP's question is about skipping last record: _"How do you not take the last record in a collection?"_. I was wonder why everybody tried to implement skipping of multiple items. YAGNI? ;) – Fabio Jul 07 '19 at 20:55
  • For multiple items you need to store required amount of items before you start yielding them one by one when storage size exceeds given count. Other answers already provide valuable solutions – Fabio Jul 07 '19 at 20:57
  • The question reads _"is there a way to Skip() records at the end of a collection?"_ Note the plural **_"records"_**. It is clear that the question is asking for the equivalent of `Skip()`, but applied to the end of the sequence instead of the beginning, and without having to first enumerate the entire collection to obtain the count (with which then `Take()` suffices). – Peter Duniho Jul 08 '19 at 00:34
  • @PeterDuniho, you took sentence from the question which suits your approach, I took another ;) – Fabio Jul 08 '19 at 01:58
3

A slightly different version of Henrik Hansen's answer:

static public IEnumerable<TSource> SkipLast<TSource>(
    this IEnumerable<TSource> source, int count)
{
    if (count < 0) count = 0;
    var queue = new Queue<TSource>(count + 1);
    foreach (TSource item in source)
    {
        queue.Enqueue(item);
        if (queue.Count > count) yield return queue.Dequeue();
    }
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
2

What about this:

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();
  }
}

Update

With help from some reviews an optimized version building on the same idea could be:

static public IEnumerable<T> SkipLast<T>(this IEnumerable<T> data, int count)
{
  if (data == null) throw new ArgumentNullException(nameof(data));
  if (count <= 0) return data;

  if (data is ICollection<T> collection)
    return collection.Take(collection.Count - count);

  IEnumerable<T> Skipper()
  {
    using (var enumer = data.GetEnumerator())
    {
      T[] queue = new T[count];
      int index = 0;

      while (index < count && enumer.MoveNext())
        queue[index++] = enumer.Current;

      index = -1;
      while (enumer.MoveNext())
      {
        index = (index + 1) % count;
        yield return queue[index];
        queue[index] = enumer.Current;
      }
    }
  }

  return Skipper();
}
  • 2
    I like this approach, personally speaking. Here I thought i could have a sleepy, dozey weekend, and there comes your code requiring me to actually be awake and pay attention while looking at it. It's a real mind twister, at least for me... ;-) –  Jul 06 '19 at 12:31
  • @elgonzo: thanks for your comment. Apparently I can't wake Mr. Ian Mercer from his weekend slumper so he can give an invalidating example :-) –  Jul 06 '19 at 12:38
  • 2
    Yeah, sorry, you are right; it does work but it potentially enumerates the first `count` elements twice which is not ideal. May be better to iterate over all but `yield return` from queue only after passing `count` elements. – Ian Mercer Jul 06 '19 at 17:23
  • Enumerating twice could cause a second database query, in the context of LINQ to SQL, and the second query could potentially fetch a different number of elements. – Theodor Zoulias Jul 07 '19 at 18:01
  • @TheodorZoulias: Agree: The first version isn't good, but could you elaborate on your comment about the second? –  Jul 07 '19 at 18:05
  • @HenrikHansen the second version is fine! – Theodor Zoulias Jul 07 '19 at 19:30
  • 1
    The `ICollection` optimization is useful. The explicit circular buffer, not so much, since that's essentially the internal implementation of `Queue`. You might as well just use `Queue`, which will give you the same performance but with much easier code. – Peter Duniho Jul 08 '19 at 00:40
1

One way would be:

var result = l.Reverse().Skip(1);

And if needed another Reverse to get them back in the original order.

Magnus
  • 45,362
  • 8
  • 80
  • 118
  • 1
    The [`Reverse`](https://referencesource.microsoft.com/system.core/system/linq/Enumerable.cs.html#3af306c560f8c669) methods buffers all the elements of the source, and then enumerates the buffer backwards. As a solution to this problem, it is quite inefficient. – Theodor Zoulias Jul 07 '19 at 19:42
  • @TheodorZoulias It is a simple one liner to achieve the goal. Of course if the collection is very large you might need a more delicate solution, but this might very well be enough. You start simple and than make it more complicated if needed. – Magnus Jul 08 '19 at 05:43