LINQ Custom Sort

15,844

Solution 1

You need to use a comparison function, they are functions that from two instances of your type return an integer that return 0 if both are equals, a negative value if the first is less than the second and a positive value if the first is greater than the second.

MSDN has a nice table that is easier to follow than text (StackOverflow still doesn't support tables in 2014)

IComparer<T>

Most sort methods accept a custom comparer implementation of type IComparer<T> you should create one encapsulating your custom rules for Group :

class GroupComparer : IComparer<Group>
{
    public int Compare(Group a, Group b)
    {
        if (a != null && b != null && (a.Id == 0 || b.Id == 0))
        {
            if (a.Id == b.Id)
            {
                // Mandatory as some sort algorithms require Compare(a, b) and Compare(b, a) to be consitent
                return 0; 
            }
            return a.Id == 0 ? -1 : 1;
        }

        if (a == null || b == null)
        {
            if (ReferenceEquals(a, b))
            {
                return 0;
            }
            return a == null ? -1 : 1;
        }
        return Comparer<string>.Default.Compare(a.Name, b.Name);
    }
}

Usage:

items.OrderBy(_ => _, new GroupAuthorityComparer());



IComparable<T>

If it is the only way to compare Group instances you should make it implement IComparable<T> so that no aditional code is needed if anyone want to sort your class :

class Group : IComparable<Group>
{
    ...

    public int CompareTo(Group b)
    {
        if (b != null && (Id == 0 || b.Id == 0))
        {
            if (Id == b.Id)
            {
                // Mandatory as some sort algorithms require Compare(a, b) and Compare(b, a) to be consitent
                return 0; 
            }
            return Id == 0 ? -1 : 1;
        }

        return Comparer<string>.Default.Compare(Name, b.Name);
    }   
}

Usage:

items.OrderBy(_ => _.Group);

The choice between one way or the other should be done depending on where this specific comparer is used: Is it the main ordering for this type of item or just the ordering that should be used in one specific case, for example only in some administrative view.

You can even go one level up and provide an IComparable<GroupAuthority> implementation (It's easy once Group implement IComparable<Group>):

class GroupAuthority : IComparable<GroupAuthority>
{
    ...

    public int CompareTo(GroupAuthority b)
    {
        return Comparer<Group>.Default.Compare(Group, b.Group);
    }
}

Usage:

items.OrderBy(_ => _);

The advantage of the last one is that it will be used automatically, so code like: GroupAuthoritys.ToList().Sort() will do the correct thing out of the box.

Solution 2

You can try something like this

list.Sort((x, y) =>
{
    if (x.Id == 0)
    {
        return -1;
    }
    if (y.Id == 0)
    {
        return 1;
    }

    return x.Group.Name.CompareTo(y.Group.Name);
});

Where list is List<T>.

This method takes advantage of custom sort option provided by List<T> using Comparison<T> delegate.

Basically what this method does is, it just adds special condition for comparison when Id, If it is zero it will return a value indicating the object is smaller which makes the object to come in top of the list. If not, it sorts the object using its Group.Name property in ascending order.

Solution 3

    public IEnumerable<GroupAuthority> GroupAuthoritysSorted 
    { 
        get 
        { 
            return GroupAuthoritys.OrderBy(x => x.Group.ID == 0)
                                  .ThenBy(x => x.Group.Name); 
        } 
    }
Share:
15,844
paparazzo
Author by

paparazzo

Design, develop and maintain C# WPF and ASP.NET business application with large MSSQL backends. Degree in engineering and got into computing via numerical methods.

Updated on June 11, 2022

Comments

  • paparazzo
    paparazzo almost 2 years

    I want an alphabetic sort with one exception.
    There is a Group with a Name = "Public" and an ID = "0" that I want first.
    (would rather use ID = 0)
    After that then sort the rest by Name.
    This does not return public first.

    public IEnumerable<GroupAuthority> GroupAuthoritysSorted 
    { 
       get 
       { 
           return GroupAuthoritys.OrderBy(x => x.Group.Name); 
       } 
    }
    

    What I want is:

    return GroupAuthoritys.Where(x => x.ID == 0)
           UNION 
           GroupAuthoritys.Where(x => x.ID > 0).OrderBy(x => x.Group.Name);
    

    GroupAuthority has a public property Group and Group has Public properties ID and Name.

    I used basically the accepted answer

    using System.ComponentModel;
    
    namespace SortCustom
    {
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
                TestSort();
            }
            private void TestSort()
            {
                List<CustomSort> LCS = new List<CustomSort>();
                LCS.Add(new CustomSort(5, "sss"));
                LCS.Add(new CustomSort(6, "xxx"));
                LCS.Add(new CustomSort(4, "xxx"));
                LCS.Add(new CustomSort(3, "aaa"));
                LCS.Add(new CustomSort(7, "bbb"));
                LCS.Add(new CustomSort(0, "pub"));
                LCS.Add(new CustomSort(2, "eee"));
                LCS.Add(new CustomSort(3, "www"));
                foreach (CustomSort cs in LCS) System.Diagnostics.Debug.WriteLine(cs.Name);
                LCS.Sort();
                foreach (CustomSort cs in LCS) System.Diagnostics.Debug.WriteLine(cs.Name);
            }
        }
        public class CustomSort : Object, INotifyPropertyChanged, IComparable<CustomSort>
        {
            public event PropertyChangedEventHandler PropertyChanged;
            public void OnPropertyChanged(PropertyChangedEventArgs e)
            {
                if (PropertyChanged != null) PropertyChanged(this, e);
            }
            private Int16 id;
            private string name;
            public Int16 ID { get { return id; } }
            public String Name { get { return name; } }
            public int CompareTo(CustomSort obj)
            {
                if (this.ID == 0) return -1;
                if (obj == null) return 1;
                if (obj is CustomSort)
                {
                    CustomSort comp = (CustomSort)obj;
                    if (comp.ID == 0) return 1;
                    return string.Compare(this.Name, comp.Name, true);
                }
                else
                {
                    return 1;
                }
            }
            public override bool Equals(Object obj)
            {
                // Check for null values and compare run-time types.
                if (obj == null) return false;
                if (!(obj is CustomSort)) return false;
                CustomSort comp = (CustomSort)obj;
                return (comp.ID == this.ID); 
            }
            public override int GetHashCode()
            {
                return (Int32)ID;
            }
            public CustomSort(Int16 ID, String Name)
            {
                id = ID;
                name = Name;
            }
        }
    }
    
  • paparazzo
    paparazzo almost 10 years
    This is getting up votes and I sure wish I could understand it. Please explain.
  • Sriram Sakthivel
    Sriram Sakthivel almost 10 years
    @Blam Added explanation, sorry for delay. It was my time to leave office :(
  • paparazzo
    paparazzo almost 10 years
    I did not vote it down. I am going to try and test it.
  • Sriram Sakthivel
    Sriram Sakthivel almost 10 years
    @Blam Thanks, I tested before posting, if anything let me know I'll look into it.
  • paparazzo
    paparazzo almost 10 years
    This works +1. Why do you think -2 on the question?
  • shole
    shole over 7 years
    does this work if I am using IQueryable instead of in-memory list?
  • Sriram Sakthivel
    Sriram Sakthivel over 7 years
    @shole No. Sort method isn't available on IQueryable it's available in List<T> only. You need to use OrderBy instead