linq extension method to take elements from the end of the sequence
Asked Answered
O

4

8

There is the enumerable extension method

Take<TSource>(
    IEnumerable<TSource> source,
    int count
)

which takes the first count elements from the start.

Is there a way to take the elements from the end? or even better a way to take the elements from an offset to the end?

Thanks

Oreilly answered 23/9, 2010 at 17:7 Comment(0)
S
16
finiteList.Reverse().Take(count).Reverse();

or

finiteList.Skip(finiteList.Count() - count)

There is some overhead in doing this so a custom method would be better.

Update: A custom method

public static class EnumerableExtensions
{
    public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (count < 0) throw new ArgumentOutOfRangeException("count");

        if (count == 0) yield break;

        var queue = new Queue<T>(count);

        foreach (var t in source)
        {
            if (queue.Count == count) queue.Dequeue();

            queue.Enqueue(t);
        }

        foreach (var t in queue)
            yield return t;
    }
}

Update: Changed the code a littlebit with ideas from dtb´s answer :-)

Comment to Bear: Look at this example:

var lastFive = Enumerable.Range(1, 10).TakeLast(5);
var lastFive2 = Enumerable.Range(1, 10).TakeLast2(5); //Bear´s way

Queue<int> q = (Queue<int>)lastFive2;
q.Dequeue();

//Is lastFive2 still last five? no...

You could potentially change the values of lastFive2 and therefore that approach can be unsafe or at least it´s not the functional way.

To Bear:

What I meant about safe is this:

var lastFive2 = Enumerable.Range(1, 10).TakeLast2(5); //Bear´s way

//some = Some method which you don't control - it could be from another assembly which represents a crazy plugin etc.
some(lastFive2);
//Now what?

In these cases you would have to make a copy to be sure. But in most cases your way would be fine - and a little bit more efficient than this so +1 :)

An idea is to use a queue which only have internal Enqueue etc.

Solita answered 23/9, 2010 at 17:9 Comment(14)
Yikes, that'll work, but it's not gonna be efficient, nor will it be elegant.Adal
@Adal As I state, I agree with you :) I will come up with a more efficient method...Solita
Also, depending on what finiteList is, it may or may not even be POSSIBLE to enumerate it twice (which your second suggestion would require)Adal
second one is fine, but first is uuuglySulfathiazole
arg. didn't see the skip methodOreilly
TakeLast doesnt need to be an iterator as its aggregating. The redundant yields can be replaced with return queue.Greco
@Bear ahh yes! Didn't even see that :D thanks for your contribution :)Solita
@Bear Hhm no, not quite. A (Queue)some.TakeLast(5) could give unexpected results... but generally, it would be fine :)Solita
@lasse Not sure I follow? See post below for what i meant.Greco
@Bear Yes, I know what you meant, but I have updated with an example of what I mean :)Solita
@lasse ok i see what you mean but it is safe because your not mutating the source collection. Nonetheless your right it probably not functional though.Greco
@lasse ok i concede, i think your approach is better than mine ;) Its shame ReadOnlyCollection doesnt take an ICollection then we could just wrap queue in that and we both win!Greco
@Bear I like your solution - I would use it when skipping a enumerator would be important (properly not very often, though). ReadOnlyCollection would also wrap the IEnumerable so it is properly implemented like my loop and therefore the efficiency is gone.Solita
@lasse Ok good point about ReadOnlyCollection but what about ReadOnlyEnumerable? Your thought would be appreciated. Ive added the class to my post I made to my earlier. I could make this a new question if you think its interesting enough.Greco
S
3

MoreLINQ provides a TakeLast extension method:

var last10 = finiteList.TakeLast(10);

To take the elements from an offset to the end, Enumerable.Skip should do the trick:

var allFromOffsetToEnd = finiteList.Skip(offset);
Slabber answered 23/9, 2010 at 17:18 Comment(1)
+1 ALMOST exactly what I came up with - a little later than your post ;)Solita
G
2

@lasseespeholt:

public static class EnumerableExtensions
{
    public static ReadOnlyEnumerable<T> AsReadOnly<T>(
         this IEnumerable<T> source)
    {
        return new ReadOnlyEnumerable<T>(source);
    }
}

public sealed class ReadOnlyEnumerable<T> : IEnumerable<T>
{
    private readonly IEnumerable<T> _source;

    public ReadOnlyEnumerable(IEnumerable<T> source)
    {
        if (_source == null)
        {
            throw new ArgumentNullException("source");
        }

        _source = source;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _source.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _source.GetEnumerator();
    }
}

public static IEnumerable<T> TakeLast<T>(this IEnumerable<T> source, int count) 
{ 
    if (source == null) throw new ArgumentNullException("source"); 
    if (count < 0) throw new ArgumentOutOfRangeException("count"); 

    if (count == 0) 
       return Enumerable.Empty<T>();

    var queue = new Queue<T>(count); 

    foreach (var t in source) 
    { 
        if (queue.Count == count) queue.Dequeue(); 

        queue.Enqueue(t); 
    } 

    return queue.AsReadOnly(); 
} 
Greco answered 23/9, 2010 at 18:57 Comment(2)
What exactly is ReadOnlyEnumerable<T> there for? The IEnumerable/IEnumerable<T> interfaces are already read-only.K2
Please see the accepted answer. I've added this is a reply to a discussion with lasseespeholt. Basically its to guard against modification by consumers who could upcast the IEnumerable<T> to a Queue<T> and modify it. I was hoping to get his opinion.Greco
R
1

A note on performance. Plenty of answers here operating on IEnumerable<> and that is probably what you need and should use.

But if the datasets are large and of type List<> or similar, you can prevent a lot of unnecessary iterating with something like:

// demo, no errorhandling
public static IEnumerable<T> TakeFrom<T>(this IList<T> list, int offset)
{
    for (int i = offset; i < list.Count; i += 1)
    {
        yield return list[i];
    }
}
Rubbish answered 23/9, 2010 at 17:57 Comment(3)
+1 And as a comment, most of .Net framework code (including LINQ methods) checks whether the arguments are lists, arrays or just IEnumerables and then outputs the answer the most efficient way :) A combined method would be nice...Solita
@lassee: I was wondering if this could work automatically by overload resolution or that you have to write a wrapper that uses is/as.Rubbish
Overloading would do it, but an advantage with the is/as approach is that if you are giving a source you don't is a list, then you would have to make the is/as yourself.Solita

© 2022 - 2024 — McMap. All rights reserved.