user1129988
user1129988

Reputation: 1586

Using LINQ to take the top 100 and bottom 100?

I would like to do something like this (below) but not sure if there is a formal/optimized syntax to do so?

.Orderby(i => i.Value1)
.Take("Bottom 100 & Top 100")
.Orderby(i => i.Value2);

basically, I want to sort by one variable, then take the top 100 and bottom 100, and then sort those results by another variable.

Any suggestions?

Upvotes: 27

Views: 44149

Answers (5)

Farhad Jabiyev
Farhad Jabiyev

Reputation: 26665

You can write your own extension method like Take(), Skip() and other methods from Enumerable class. It will take the numbers of elements and the total length in list as input. Then it will return first and last N elements from the sequence.

var result = yourList.OrderBy(x => x.Value1)
                     .GetLastAndFirst(100, yourList.Length)
                     .OrderBy(x => x.Value2)
                     .ToList();

Here is the extension method:

public static class SOExtensions
{
    public static IEnumerable<T> GetLastAndFirst<T>(
        this IEnumerable<T> seq, int number, int totalLength
    )
    {
        if (totalLength < number*2) 
            throw new Exception("List length must be >= (number * 2)");

        using (var en = seq.GetEnumerator())
        {
            int i = 0;

            while (en.MoveNext())
            {
                i++;
                if (i <= number || i >= totalLength - number) 
                     yield return en.Current;
            }
        }
    }
}

Upvotes: 3

BartoszKP
BartoszKP

Reputation: 35921

You can do it with in one statement also using this .Where overload, if you have the number of elements available:

var elements = ...

var count = elements.Length; // or .Count for list

var result = elements
    .OrderBy(i => i.Value1)
    .Where((v, i) => i < 100 || i >= count - 100)
    .OrderBy(i => i.Value2)
    .ToArray();             // evaluate

Here's how it works:

| first 100 elements | middle elements | last 100 elements |
        i < 100        i < count - 100    i >= count - 100

Upvotes: 3

Giannis Paraskevopoulos
Giannis Paraskevopoulos

Reputation: 18431

Take the top 100 and bottom 100 separately and union them:

var tempresults = yourenumerable.OrderBy(i => i.Value1);
var results = tempresults.Take(100);
results = results.Union(tempresults.Skip(tempresults.Count() - 100).Take(100))
                 .OrderBy(i => i.Value2);

Upvotes: 5

T_D
T_D

Reputation: 1728

var sorted = list.OrderBy(i => i.Value);
var top100 = sorted.Take(100);
var last100 = sorted.Reverse().Take(100);
var result = top100.Concat(last100).OrderBy(i => i.Value2);

I don't know if you want Concat or Union at the end. Concat will combine all entries of both lists even if there are similar entries which would be the case if your original list contains less than 200 entries. Union would only add stuff from last100 that is not already in top100.

Some things that are not clear but that should be considered:

  • If list is an IQueryable to a db, it probably is advisable to use ToArray() or ToList(), e.g.

    var sorted = list.OrderBy(i => i.Value).ToArray();
    

    at the beginning. This way only one query to the database is done while the rest is done in memory.

  • The Reverse method is not optimized the way I hoped for, but it shouldn't be a problem, since ordering the list is the real deal here. For the record though, the skip method explained in other answers here is probably a little bit faster but needs to know the number of elements in list.

  • If list would be a LinkedList or another class implementing IList, the Reverse method could be done in an optimized way.

Upvotes: 35

Thomas Levesque
Thomas Levesque

Reputation: 292765

You can use an extension method like this:

public static IEnumerable<T> TakeFirstAndLast<T>(this IEnumerable<T> source, int count)
{
    var first = new List<T>();
    var last = new LinkedList<T>();
    foreach (var item in source)
    {
        if (first.Count < count)
            first.Add(item);
        if (last.Count >= count)
            last.RemoveFirst();
        last.AddLast(item);
    }

    return first.Concat(last);
}

(I'm using a LinkedList<T> for last because it can remove items in O(1))

You can use it like this:

.Orderby(i => i.Value1)
.TakeFirstAndLast(100)
.Orderby(i => i.Value2);

Note that it doesn't handle the case where there are less then 200 items: if it's the case, you will get duplicates. You can remove them using Distinct if necessary.

Upvotes: 6

Related Questions