ENTERPRISE

LINQ to Objects : How to Sort the Results

9/19/2012 7:07:51 PM
LINQ to Objects has comprehensive support for ordering and sorting results. Whether you need to sort in ascending order, descending order using different property values in any sequence, or all the way to writing a specific ordering algorithm of your own, LINQ’s sorting features can accommodate the full range of ordering requirements.

Basic Sorting Syntax

The resulting collection of results from a query can be sorted in any desired fashion, considering culture and case sensitivity. When querying using extension method syntax, the OrderBy, OrderByDescending, ThenBy, and ThenByDescending standard query operators manage this process. The OrderBy and ThenBy operators sort in an ascending order (for example, a to z), and the OrderByDescending and ThenByDescending operators sort in descending order (z to a). Only the first sorting extension can use the OrderBy operators, and each subsequent sorting expression must use the ThenBy operators, of which there can be zero or many depending on how much control you want over the subsorting when multiple elements share equal order after the previous expressions.

The following samples demonstrate sorting a source sequence first by the [w] key, then in descending order by the [x] key, and then in ascending order by the [y] key:

[source].OrderBy([w])
        .ThenByDescending([x])
        .ThenBy([y]);

When using the query expression syntax, each sorting key and the optional direction keyword needs to be separated by a comma character. If the descending or ascending direction keywords are not specified, LINQ assumes ascending order.

from [v] in [source]
orderBy [w], [x] descending, [y]
select [z];

The result from ordering a collection will be an IOrderedEnumerable<T>, which implements IEnumerable<T> to allow further query operations to be cascaded end-to-end.

The ordering extension methods are implemented using a basic but efficient Quicksort algorithm (see http://en.wikipedia.org/wiki/Quicksort for further explanation of how this algorithm works). The implementation LINQ to Objects uses is a sorting type called unstable, which simply means that elements that compare to equal key values may not retain their relative positions to the source collections (although this is simply solved by cascading the result into a ThenBy or ThenByDescending operator). The algorithm is fairly fast, and it lends itself to parallelization, which is certainly leveraged by Microsoft’s investment in Parallel LINQ.

Reversing the Order of a Result Sequence (Reverse)

Another ordering extension method that reverses an entire sequence is the Reverse operator. It is simply called in the form: [source].Reverse();. An important point to note when using the Reverse operator is that it doesn’t test the equality of the elements or carry out any sorting; it simply returns elements starting from the last element, back to the first element. The order returned will be the exact reciprocal of the order that would have been returned from the result sequence. The following example demonstrates the Reverse operator, returning T A C in the Console window:

string[] letters = new string[] { "C", "A", "T" };
var q = letters.Reverse();
foreach (string s in q)
    Console.Write(" " + s);

Case Insensitive and Cultural-specific String Ordering

Any standard query operator that involves sorting has an overload that allows a specific comparer function to be supplied (when written using extension method syntax). The .NET class libraries contain a handy helper class called StringComparer, which has a set of predefined static comparers ready for use. The comparers allow us to alter string sorting behavior, controlling case-sensitivity and current culture (the language setting for the current thread). Table 1 lists the static Comparer instances that can be used in any OrderBy or ThenBy ascending or descending query operator.

Table 1. The Built-in StringComparer Functions to Control String Case Sensitivity and Culture-aware String Ordering
ComparerDescription
CurrentCulturePerforms a case-sensitive string comparison using the word comparison rules of the current culture.
CurrentCultureIgnoreCasePerforms case-insensitive string comparisons using the word comparison rules of the current culture.
InvariantCulturePerforms a case-sensitive string comparison using the word comparison rules of the invariant culture.
InvariantCultureIgnoreCasePerforms a case-insensitive string comparison using the word comparison rules of the invariant culture.
OrdinalPerforms a case-sensitive ordinal string comparison.
OrdinalIgnoreCasePerforms a case-insensitive ordinal string comparison.

Listing 1 demonstrates the syntax and effect of using the built-in string comparer instances offered by the .NET Framework. The Console output is shown in Output 1, where the default case-sensitive result can be forced to case-insensitive.

Listing 1. Case and culture sensitive/insensitive ordering using StringComparer functions—see Output 1
string[] words = new string[] {
    "jAnet", "JAnet", "janet", "Janet" };

var cs = words.OrderBy(w => w);
var ci = words.OrderBy(w => w,
    StringComparer.CurrentCultureIgnoreCase);

Console.WriteLine("Original:");
foreach (string s in words)
    Console.WriteLine(" " + s);

Console.WriteLine("Case Sensitive (default):");
foreach (string s in cs)
    Console.WriteLine(" " + s);

Console.WriteLine("Case Insensitive:");
foreach (string s in ci)
    Console.WriteLine(" " + s);

Output 1.
Original:
 jAnet
 JAnet
 janet
 Janet
Case Sensitive (default):
 janet
 jAnet
 Janet
 JAnet
Case Insensitive:
 jAnet
 JAnet
 janet
 Janet

Specifying Your Own Custom Sort Comparison Function

To support any sorting order that might be required, custom sort comparison classes are easy to specify. A custom compare class is a class based on a standard .NET Interface called IComparer<T>, which exposes a single method: Compare. This interface is not specifically for LINQ; it is the basis for all .NET Framework classes that require sorting (or custom sorting) capabilities.

Comparer functions work by returning an integer result, indicating the relationship between a pair of instance types. If the two types are deemed equal, the function returns zero. If the first instance is less than the second instance, a negative value is returned, or if the first instance is larger than the second instance, the function returns a positive value. How you equate the integer result value is entirely up to you.

To demonstrate a custom IComparer<T>, Listing 2 demonstrates a comparison function that simply shuffles (in a random fashion) the input source. The algorithm simply makes a random choice as to whether two elements are less than or greater than each other. Output 3-8 shows the Console output from a simple sort of a source of strings in an array, although this result will be different (potentially) each time this code is executed.

Listing 2. Ordering using our custom IComparer<T> implementation to shuffle the results—see Output 2
public class RandomShuffleStringSort<T> : IComparer<T>
{
    internal Random random = new Random();

    public int Compare(T x, T y)
    {
        // get a random number: 0 or 1
        int i = random.Next(2);

        if (i == 0)
            return -1;
        else
            return 1;
    }
}

string[] strings = new string[] { "1-one", "2-two",
   "3-three", "4-four", "5-five" };

var normal = strings.OrderBy(s => s);
var custom = strings.OrderBy(s => s,
   new RandomShuffleStringSort<string>());

Console.WriteLine("Normal Sort Order:");
foreach (string s in normal) {
    Console.WriteLine(" " + s);
}

Console.WriteLine("Custom Sort Order:");
foreach (string s1 in custom) {
    Console.WriteLine(" " + s1);
}

					  

Output 2.
Normal Sort Order:
 1-one
 2-two
 3-three
 4-four
 5-five
Custom Sort Order:
 1-one
 2-two
 5-five
 4-four
 3-three

A common scenario that has always caused me trouble is where straight alphabetical sorting doesn’t properly represent alpha-numeric strings. The most common example is alphabetic sorting strings such as File1, File10, File2. Naturally, the desired sorting order would be File1, File2, File10, but that’s not alphabetical. A custom IComparer that will sort the alphabetic part and then the numeric part separately is needed to achieve this common scenario. This is called natural sorting.

Listing 3 and Output 3 demonstrate a custom sort class that correctly orders alpha strings that end with numbers. Anywhere this sort order is required, the class name can be passed into any of the OrderBy or ThenBy extension methods in the following way:

string[] partNumbers = new string[] { "SCW10", "SCW1",
    "SCW2", "SCW11", "NUT10", "NUT1", "NUT2", "NUT11" };

var custom = partNumbers.OrderBy(s => s,
    new AlphaNumberSort());

The code in Listing 3 first checks if either input string is null or empty. If either string is empty, it calls and returns the result from the default comparer (no specific alpha-numeric string to check). Having determined that there are two actual strings to compare, the numeric trailing section of each string is extracted into the variables numericX and numericY. If either string doesn’t have a trailing numeric section, the result of the default comparer is returned (if no trailing numeric section exists for one of the strings, then a straight string compare is adequate). If both strings have a trailing numeric section, the alpha part of the strings is compared. If the strings are different, the result of the default comparer is returned (if the strings are different, the numeric part of the string is irrelevant). If both alpha parts are the same, the numeric values in numericX and numericY are compared, and that result is returned. The final result is that all strings are sorted alphabetically, and where the string part is the same between elements, the numeric section controls the final order.

Listing 3. Sorting using a custom comparer. This comparer properly sorts strings that end with a number—see Output 3
public class AlphaNumberSort : IComparer<string>
{
    public int Compare(string a, string b)
    {
        StringComparer sc =
            StringComparer.CurrentCultureIgnoreCase;

        // if either input is null or empty,
        // do a straight string comparison
        if (string.IsNullOrEmpty(a) ||
            string.IsNullOrEmpty(b))
                return sc.Compare(a, b);

        // find the last numeric sections
        string numericX = FindTrailingNumber(a);
        string numericY = FindTrailingNumber(b);

        // if there is a numeric end to both strings,
        // we need to investigate further
        if (numericX != string.Empty &&
            numericY != string.Empty)
        {
            // first, compare the string prefix only
            int stringPartCompareResult =
                sc.Compare(
                    a.Remove(a.Length - numericX.Length),
                    b.Remove(b.Length - numericY.Length));

            // the strings prefix are different,
            // return the comparison result for the strings
            if (stringPartCompareResult != 0)
                return stringPartCompareResult;

            // the strings prefix is the same,
            // need to test the numeric sections as well
            double nX = double.Parse(numericX);
            double nY = double.Parse(numericY);
            return nX.CompareTo(nY);
        }
        else
            return sc.Compare(a, b);
    }

    private static string FindTrailingNumber(string s)
    {
        string numeric = string.Empty;
        for (int i = s.Length - 1; i > -1; i--)
        {
            if (char.IsNumber(s[i]))
                numeric = s[i] + numeric;
            else
                break;
        }
        return numeric;
    }
}

string[] partNumbers = new string[] { "SCW10", "SCW1",
    "SCW2", "SCW11", "NUT10", "NUT1", "NUT2", "NUT11" };

var normal = partNumbers.OrderBy(s => s);
var custom = partNumbers.OrderBy(s => s,
    new AlphaNumberSort());

Console.WriteLine("Normal Sort Order:");
foreach (string s in normal)
    Console.WriteLine(" " + s);

Console.WriteLine("Custom Sort Order:");
foreach (string s in custom)
    Console.WriteLine(" " + s);

					  

Output 3.
Normal Sort Order:
NUT1
NUT10
NUT11
NUT2
SCW1
SCW10
SCW11
SCW2
Custom Sort Order:
 NUT1
 NUT2
 NUT10
 NUT11
 SCW1
 SCW2
 SCW10
 SCW11

Note

To achieve the same result in most Windows operating systems (not Windows 2000, but ME, XP, 2003, Vista, and Windows 7) and without guarantee that it won’t change over time (it bears the following warning “Note Behavior of this function, and therefore the results it returns, can change from release to release. It should not be used for canonical sorting applications”), Microsoft has an API that it uses to sort files in Explorer (and presumably other places).

internal static class Shlwapi
{
    // http://msdn.microsoft.com/
    // en-us/library/bb759947(VS.85).aspx
    [DllImport("shlwapi.dll",
                CharSet = CharSet.Unicode)]
    public static extern int StrCmpLogicalW(
        string a,
        string b);
}

public sealed class
    NaturalStringComparer: IComparer<string>
{
    public int Compare(string a, string b)
    {
        return Shlwapi.StrCmpLogicalW(a, b);
    }
}
Other  
  •  LINQ to Objects : How to Get the Index Position of the Results, How to Remove Duplicate Results
  •  Integrating Exchange Server 2007 in a Non-Windows Environment : Synchronizing Exchange Server 2007 with Novell eDirectory
  •  Integrating Exchange Server 2007 in a Non-Windows Environment : Synchronizing Directory Information with Microsoft Identity Integration Server (MIIS) 2003
  •  IBM WebSphere Process Server 7 and Enterprise Service Bus 7 : Solution administration tasks (part 2)
  •  IBM WebSphere Process Server 7 and Enterprise Service Bus 7 : Solution administration tasks (part 1) - Performing common tasks using the administrative console
  •  Hardware With An Expiry Date (Part 2)
  •  Hardware With An Expiry Date (Part 1)
  •  Managing SharePoint 2010 Data : Custom Field Types
  •  Managing SharePoint 2010 Data : Content Types
  •  Active Directory Domain Services 2008 : Enable a Group Policy Object Link, Enforce a Group Policy Object Link, Remove the Enforcement of a Group Policy Object Link
  •  
    Most View
    AMD A10-6800K Review – Richland-Based Desktop APU (Part 4)
    Buy A Decent System For $325 (Part 1) : , Zoostorm Desktop PC, Primo 6000i, HP Compaq DC7900 SFF, Dell Optiplex 745 MT
    Alternatives To Online Backups
    iPhone SDK 3 Programming : XML Processing - An RSS Reader Application
    Holiday Gift Guide – Smartphones – Aug 2013
    SharePoint 2010 : The Search User Interface - The Search Results Page (part 2) - Alert Me
    Lenovo Ultrabook IdeaPad U410 - Stylish And Affordable
    Kindle Fire HD - Most Advanced 7" Tablet (Part 1)
    BlackBerry Z10 - A Touchscreen-Based Smartphone (Part 2)
    Be Quiet! Shadow Rock TopFlow SR1
    Top 10
    Cooler Master HAF Stacker Range
    CPU Water Cooler Antec Kühler H2O 650
    CPU Water Cooler Corsair HYDRO Series H75
    Floorstanding Loudspeaker Focal Aria 926 Review (Part 2)
    Floorstanding Loudspeaker Focal Aria 926 Review (Part 1)
    Cooler Master HAF Stacker 935 (Part 2)
    Cooler Master HAF Stacker 935 (Part 1)
    Canon Powershot SX510 HS – The Family-Friendly Bridge With A Mighty Zoom
    Camera Develop Your Skills – Panasonic Lumix DMC-GX7
    ASUS PQ321QE Widescreen LCD Monitor