Waldemar Gałęzinowski
Waldemar Gałęzinowski

Reputation: 1233

order objects by given values

Given:

class C
{
    public string Field1;
    public string Field2;
}
template = new [] { "str1", "str2", ... }.ToList() // presents allowed values for C.Field1 as well as order
list = new List<C> { ob1, ob2, ... }

Question:

How can I perform Linq's

list.OrderBy(x => x.Field1) 

which will use template above for order (so objects with Field1 == "str1" come first, than objects with "str2" and so on)?

Upvotes: 0

Views: 418

Answers (5)

anaximander
anaximander

Reputation: 7140

I've actually written a method to do this before. Here's the source:

public static IOrderedEnumerable<T> OrderToMatch<T, TKey>(this IEnumerable<T> source, Func<T, TKey> sortKeySelector, IEnumerable<TKey> ordering)
{
    var orderLookup = ordering
        .Select((x, i) => new { key = x, index = i })
        .ToDictionary(k => k.key, v => v.index);

    if (!orderLookup.Any())
    {
        throw new ArgumentException("Ordering collection cannot be empty.", nameof(ordering));
    }

    T[] sourceArray = source.ToArray();

    return sourceArray
        .OrderBy(x =>
        {
            int index;
            if (orderLookup.TryGetValue(sortKeySelector(x), out index))
            {
                return index;
            }
            return Int32.MaxValue;
        })
        .ThenBy(x => Array.IndexOf(sourceArray, x));
}

You can use it like this:

 var ordered = list.OrderToMatch(x => x.Field1, template);

If you want to see the source, the unit tests, or the library it lives in, you can find it on GitHub. It's also available as a NuGet package.

Upvotes: 0

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726479

In LINQ to Object, use Array.IndexOf:

var ordered = list
    .Select(x => new { Obj = x, Index = Array.IndexOf(template, x.Field1)})
    .OrderBy(p => p.Index < 0 ? 1 : 0) // Items with missing text go to the end
    .ThenBy(p => p.Index)              // The actual ordering happens here
    .Select(p => p.Obj);               // Drop the index from the result

This wouldn't work in EF or LINQ to SQL, so you would need to bring objects into memory for sorting.

Note: The above assumes that the list is not exhaustive. If it is, a simpler query would be sufficient:

var ordered = list.OrderBy(x => Array.IndexOf(template, x.Field1));

Upvotes: 2

Blue
Blue

Reputation: 66

var orderedList = list.OrderBy(d => Array.IndexOf(template, d.MachingColumnFromTempalate) < 0 ? int.MaxValue : Array.IndexOf(template, d.MachingColumnFromTempalate)).ToList();

Upvotes: 0

Matt Burland
Matt Burland

Reputation: 45135

As others have said, Array.IndexOf should do the job just fine. However, if template is long and or list is long, it might be worthwhile transforming your template into a dictionary. Something like:

var templateDict = template.Select((item,idx) => new { item, idx })
                           .ToDictionary(k => k.item, v => v.idx);

(or you could just start by creating a dictionary instead of an array in the first place - it's more flexible when you need to reorder stuff)

This will give you a dictionary keyed off the string from template with the index in the original array as your value. Then you can sort like this:

var ordered = list.OrderBy(x => templateDict[x.Field1]);

Which, since lookups in a dictionary are O(1) will scale better as template and list grow.

Note: The above code assumes all values of Field1 are present in template. If they are not, you would have to handle the case where x.Field1 isn't in templateDict.

Upvotes: 1

Vlad Stryapko
Vlad Stryapko

Reputation: 1057

I think IndexOf might work here:

list.OrderBy(_ => Array.IndexOf(template, _.Field1))

Please note that it will return -1 when object is not present at all, which means it will come first. You'll have to handle this case. If your field is guaranteed to be there, it's fine.

Upvotes: 1

Related Questions