Linq OrderBy against specific values

C#LinqLinq to-Objects

C# Problem Overview


Is there a way in Linq to do an OrderBy against a set of values (strings in this case) without knowing the order of the values?

Consider this data:

A
B
A
C
B
C
D
E

And these variables:

string firstPref, secondPref, thirdPref;

When the values are set like so:

firstPref = 'A';
secondPref = 'B';
thirdPref = 'C';

Is it possible to order the data like so:

A
A
B
B
C
C
D
E

C# Solutions


Solution 1 - C#

If you put your preferences into a list, it might become easier.

List<String> data = new List<String> { "A","B","A","C","B","C","D","E" };
List<String> preferences = new List<String> { "A","B","C" };

IEnumerable<String> orderedData = data.OrderBy(
   item => preferences.IndexOf(item));

This will put all items not appearing in preferences in front because IndexOf() returns -1. An ad hoc work around might be reversing preferences and order the result descending. This becomes quite ugly, but works.

IEnumerable<String> orderedData = data.OrderByDescending(
   item => Enumerable.Reverse(preferences).ToList().IndexOf(item));

The solution becomes a bit nicer if you concat preferences and data.

IEnumerable<String> orderedData = data.OrderBy(
   item => preferences.Concat(data).ToList().IndexOf(item));

I don't like Concat() and ToList() in there. But for the moment I have no really good way around that. I am looking for a nice trick to turn the -1 of the first example into a big number.

Solution 2 - C#

In addition to @Daniel Brückner answer and problem defined at the end of it: >I don't like Concat() and ToList() in there. But for the moment I have no really >good way around that. I am looking for a nice trick to turn the -1 of the first >example into a big number.

I think that the solution is to use a statement lambda instead of an expression lambda.

var data = new List<string> { "corge", "baz", "foo", "bar", "qux", "quux" };
var fixedOrder = new List<string> { "foo", "bar", "baz" };
data.OrderBy(d => {
					var index = fixedOrder.IndexOf(d);
					return index == -1 ? int.MaxValue : index; 
				  });

The ordered data is:

foo 
bar 
baz 
corge 
qux 
quux 

Solution 3 - C#

Put the preferred values in a dictionary. Looking up keys in a dictionary is a O(1) operation compared to finding values in a list which is a O(n) operation, so it scales much better.

Create a sort string for each preferred value so that they are placed before the other values. For the other values the value itself will be used as sorting string so that they are actually sorted. (Using any arbitrary high value would only place them at the end of the list unsorted).

List<string> data = new List<string> {
	"E", "B", "D", "A", "C", "B", "A", "C"
};
var preferences = new Dictionary<string, string> {
	{ "A", " 01" },
	{ "B", " 02" },
	{ "C", " 03" }
};

string key;
IEnumerable<String> orderedData = data.OrderBy(
	item => preferences.TryGetValue(item, out key) ? key : item
);

Solution 4 - C#

Combined all answers (and more) into a generic LINQ extension supporting caching which handles any data type, can be case-insensitive and allows to be chained with pre- and post-ordering:

public static class SortBySample
{
	public static BySampleSorter<TKey> Create<TKey>(IEnumerable<TKey> fixedOrder, IEqualityComparer<TKey> comparer = null)
	{
		return new BySampleSorter<TKey>(fixedOrder, comparer);
	}

	public static BySampleSorter<TKey> Create<TKey>(IEqualityComparer<TKey> comparer, params TKey[] fixedOrder)
	{
		return new BySampleSorter<TKey>(fixedOrder, comparer);
	}

	public static IOrderedEnumerable<TSource> OrderBySample<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, BySampleSorter<TKey> sample)
	{
		return sample.OrderBySample(source, keySelector);
	}

	public static IOrderedEnumerable<TSource> ThenBySample<TSource, TKey>(this IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector, BySampleSorter<TKey> sample)
	{
		return sample.ThenBySample(source, keySelector);
	}
}

public class BySampleSorter<TKey>
{
	private readonly Dictionary<TKey, int> dict;

	public BySampleSorter(IEnumerable<TKey> fixedOrder, IEqualityComparer<TKey> comparer = null)
	{
		this.dict = fixedOrder
			.Select((key, index) => new KeyValuePair<TKey, int>(key, index))
			.ToDictionary(kv => kv.Key, kv => kv.Value, comparer ?? EqualityComparer<TKey>.Default);
	}

	public BySampleSorter(IEqualityComparer<TKey> comparer, params TKey[] fixedOrder)
		: this(fixedOrder, comparer)
	{
	}

	public IOrderedEnumerable<TSource> OrderBySample<TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
	{
		return source.OrderBy(item => this.GetOrderKey(keySelector(item)));
	}

	public IOrderedEnumerable<TSource> ThenBySample<TSource>(IOrderedEnumerable<TSource> source, Func<TSource, TKey> keySelector)
	{
		return source.CreateOrderedEnumerable(item => this.GetOrderKey(keySelector(item)), Comparer<int>.Default, false);
	}

	private int GetOrderKey(TKey key)
	{
		int index;
		return dict.TryGetValue(key, out index) ? index : int.MaxValue;
	}
}

Sample usage using LINQPad-Dump():

var sample = SortBySample.Create(StringComparer.OrdinalIgnoreCase, "one", "two", "three", "four");
var unsorted = new[] {"seven", "six", "five", "four", "THREE", "tWo", "One", "zero"};
unsorted
	.OrderBySample(x => x, sample)
	.ThenBy(x => x)
	.Dump("sorted by sample then by content");
unsorted
	.OrderBy(x => x.Length)
	.ThenBySample(x => x, sample)
	.Dump("sorted by length then by sample");

Solution 5 - C#

Danbrucs solution is more elegant, but here is a solution using a custom IComparer. This might be useful if you need more advanced conditions for your sort ordering.

    string[] svals = new string[] {"A", "B", "A", "C", "B", "C", "D", "E"};
    List<string> list = svals.OrderBy(a => a, new CustomComparer()).ToList();

    private class CustomComparer : IComparer<string>
    {
        private string firstPref = "A";
        private string secondPref = "B";
        private string thirdPref = "C";
        public int Compare(string x, string y)
        {
            // first pref 
            if (y == firstPref && x == firstPref)
                return 0;
            else if (x == firstPref && y != firstPref)
                return -1;
            else if (y == firstPref && x != firstPref)
                return 1;
            // second pref
            else if (y == secondPref && x == secondPref)
                return 0;
            else if (x == secondPref && y != secondPref)
                return -1;
            else if (y == secondPref && x != secondPref)
                return 1;
            // third pref
            else if (y == thirdPref && x == thirdPref)
                return 0;
            else if (x == thirdPref && y != thirdPref)
                return -1;
            else
                return string.Compare(x, y);
        }
    }

Solution 6 - C#

Yes, you must implement your own IComparer<string> and then pass it in as the second argument of LINQ's OrderBy method.

An example can be found here: Ordering LINQ results

Solution 7 - C#

Not really efficient for large lists but fairly easy to read:

public class FixedOrderComparer<T> : IComparer<T>
{
    private readonly T[] fixedOrderItems;

    public FixedOrderComparer(params T[] fixedOrderItems)
    {
        this.fixedOrderItems = fixedOrderItems;
    }

    public int Compare(T x, T y)
    {
        var xIndex = Array.IndexOf(fixedOrderItems, x);
        var yIndex = Array.IndexOf(fixedOrderItems, y);
        xIndex = xIndex == -1 ? int.MaxValue : xIndex;
        yIndex = yIndex == -1 ? int.MaxValue : yIndex;
        return xIndex.CompareTo(yIndex);
    }
}

Usage:

var orderedData = data.OrderBy(x => x, new FixedOrderComparer<string>("A", "B", "C"));

Note: Array.IndexOf<T>(....) uses EqualityComparer<T>.Default to find the target index.

Solution 8 - C#

I use these. I like the IEnumerable overload for cleanliness, but the priority map version should have better performance on repeated calls.

    public static IEnumerable<T> OrderByStaticList<T>(this IEnumerable<T> items, IReadOnlyDictionary<T, double> priorityMap)
    {
        return items.OrderBy(x => priorityMap.GetValueOrDefault(x, double.MaxValue));
    }

    public static IEnumerable<T> OrderByStaticList<T>(this IEnumerable<T> items, IEnumerable<T> preferenceOrder)
    {
        int priority = 0;
        var priorityMap = preferenceOrder.ToDictionary(x => x, x => (double) priority++);
        return OrderByStaticList(items, priorityMap);
    }
    



    [TestMethod]
    public void PriorityMap_DeterminesSort()
    {
        var map = new Dictionary<char, double>()
        {
            {'A', 1},
            {'B', 7},
            {'C', 3},
            {'D', 42},
            {'E', -1},
        };
        Assert.AreEqual("EACBD", new string("ABCDE".OrderByStaticList(map).ToArray()));
    }

    [TestMethod]
    public void PriorityMapMissingItem_SortsLast()
    {
        var map = new Dictionary<char, double>()
        {
            {'A', 1},
            {'B', 7},
            {'D', 42},
            {'E', -1},
        };
        Assert.AreEqual("EABDC", new string("ABCDE".OrderByStaticList(map).ToArray()));
    }

    [TestMethod]
    public void OrderedList_DeterminesSort()
    {
        Assert.AreEqual("EACBD", new string("ABCDE".OrderByStaticList("EACBD").ToArray()));
    }

    [TestMethod]
    public void OrderedListMissingItem_SortsLast()
    {
        Assert.AreEqual("EABDC", new string("ABCDE".OrderByStaticList("EABD").ToArray()));
    }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJonathan ParkerView Question on Stackoverflow
Solution 1 - C#Daniel BrücknerView Answer on Stackoverflow
Solution 2 - C#alexqcView Answer on Stackoverflow
Solution 3 - C#GuffaView Answer on Stackoverflow
Solution 4 - C#springy76View Answer on Stackoverflow
Solution 5 - C#JamesView Answer on Stackoverflow
Solution 6 - C#Ben HoffsteinView Answer on Stackoverflow
Solution 7 - C#Mike RowleyView Answer on Stackoverflow
Solution 8 - C#solublefishView Answer on Stackoverflow