5

I want to sort a C# list by word. Assume I have a C# list (of objects) which contains following words:

[{id:1, name: "ABC"},
 {id:2, name: "XXX"},
 {id:3, name: "Mille"},
 {id:4, name: "YYY"},
 {id:5, name: "Mill",
 {id:6, name: "Millen"},
 {id:7, name: "OOO"},
 {id:8, name: "GGGG"},
 {id:9, name: null},
 {id:10, name: "XXX"},
 {id:11, name: "mil"}]  

If user pass Mil as a search key, I want to return all the words starting with the search key & then all the words which does not match criteria & have them sort alphabetically.

Easiest way I can think of is to run a for loop over the result set, put all the words starting with search key into one list and put the renaming words into another list. Sort the second list and them combine both the list to return the result.

I wonder if there is a smarter or inbuilt way to get the desired result.

David L
  • 32,885
  • 8
  • 62
  • 93
SharpCoder
  • 18,279
  • 43
  • 153
  • 249

7 Answers7

8

Sure! You will sort by the presence of a match, then by the name, like this:

var results = objects.OrderByDescending(o => o.Name.StartsWith(searchKey))
                     .ThenBy(o => o.Name);

Note that false comes before true in a sort, so you'll need to use OrderByDescending.

As AlexD points out, the name can be null. You'll have to decide how you want to treat this. The easiest way would be to use o.Name?.StartsWith(searchKey) ?? false, but you'll have to decide based on your needs. Also, not all Linq scenarios support null propagation (Linq To Entities comes to mind).

Daniel
  • 1,695
  • 15
  • 33
  • `o.Name` can't be `null` if you write the class so that it can't be `null`. I'd do that - unless there's a use for a null word, take it out of the equation. – Scott Hannen Jul 01 '16 at 02:55
  • 1
    Well, there is a null value in the SharpCoder's example. – Kinetic Jul 01 '16 at 03:00
  • `o => o.Name?.StartsWith(searchKey)`, null-safe navigation, or, `o => !string.IsNullOrEmpty(o.Name) && o.Name.StartsWith(searchKey)` for C#5 or less – AndrewP Jul 01 '16 at 05:46
  • @user6407578, the way you have it written would place null values first. The method in my answer will order nulls correctly. – Daniel Jul 01 '16 at 14:04
5

This should do it, but there's probably a faster way, maybe using GroupBy somehow.

var sorted = collection
    .Where(x => x.Name.StartsWith(criteria))
    .OrderBy(x => x.Name)
    .Concat(collection
           .Where(x => !x.Name.StartsWith(criteria))
           .OrderBy(x => x.Name))
JamesFaix
  • 8,050
  • 9
  • 37
  • 73
  • 1
    so how this linq solution is different from `Easiest way I can think of is to run a for loop over the result set, put all the words starting with search key into one list and put the renaming words into another list. Sort the second list and them combine both the list to return the result.` – Yar Jun 30 '16 at 21:48
  • @yar - Also, LINQ is lazy and composable, so it can be used in more contexts than a loop solution. – JamesFaix Jun 30 '16 at 21:53
5

You can try GroupBy like this:

var sorted = collection
  .GroupBy(item => item.Name.StartsWith(criteria))
  .OrderByDescending(chunk => chunk.Key)
  .SelectMany(chunk => chunk
    .OrderBy(item => item.Name));
  • Separate items into two groups (meets and doesn't meet the criteria)
  • Order the groups as whole (1st that meets)
  • Order items within each group
  • Finally combine the items
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
  • Would it make sense to create a custom comparer (which would place prefixed strings first) and have just `collection.OrderBy()`? – AlexD Jun 30 '16 at 21:56
  • @AlexD: it depends on the `criteria`; in the current solution `criteria` computes just once per each item (say, `N` times); in case of custom comparer it should be computed at least `N * Log (N)` times. So if you have a complex `criteria` then group by, in case of simple one — custom comparer is a good choice. – Dmitry Bychenko Jun 30 '16 at 22:00
  • @DmitryBychenko It looks like your solution is O(N^2) - one loop for GroupBy and another for SelectMany. Doing a custom comparer which is intelligent enough to recognize the searched term and otherwise sort alphabetically would be O(n log n). That does seem like it would be an improvement unless I'm misreading something. – Tim Copenhaver Jun 30 '16 at 22:21
  • My original comment was wrong, it's O(N) since they lists are in parallel. I hate Big O and all of its obscurity. – Tim Copenhaver Jun 30 '16 at 22:38
  • @TimCopenhaver It cannot be `O(N)`. We cannot be better than `O(N log N)`. – AlexD Jun 30 '16 at 23:10
  • I meant to say the two loops I was originally commenting on did not make it O(n^2). Those two iterations would be O(n) which makes the O(n log n) of the OrderBy the dominant value for the overall algorithm. It all makes my brain hurt, but ultimately it still doesn't look like the most efficient, regardless of Big O – Tim Copenhaver Jun 30 '16 at 23:14
  • @TimCopenhaver I think you are giving LINQ too much credit. List insert alone can be O(n). And I am not buying that is just two iterations. – paparazzo Jun 30 '16 at 23:25
  • According to [this answer](http://stackoverflow.com/questions/2799427/what-guarantees-are-there-on-the-run-time-complexity-big-o-of-linq-methods) GroupBy is O(n) . SelectMany seems likely to also be O(n). But in any case, we're pretty far off topic now. – Tim Copenhaver Jun 30 '16 at 23:28
  • @AlexD: `GroupBy` does *grouping* not *sorting*: it separates (groups) the items into chunks without sorting either chunks' keys or items within chunk; in case of efficient hash function, grouping is `O(N)` – Dmitry Bychenko Jul 01 '16 at 07:26
  • @DmitryBychenko Oh, sorry, indeed, the groups themselves are not sorted. Then `O(N)` is possible. Deleting misleading comments... – AlexD Jul 01 '16 at 08:07
1

There's nothing C#-specific to solve this, but it sounds like you're really looking for algorithm design guidance.

You should sort the list first. If this is a static list you should just keep it sorted all the time. If the list is large, you may consider using a different data structure (Binary Search Tree, Skip List, etc.) which is more optimized for this scenario.

Once it's sorted, finding matching elements becomes a simple binary search. Move the matching elements to the beginning of the result set, then return.

Tim Copenhaver
  • 3,282
  • 13
  • 18
1

Add an indicator of a match into the select, and then sort on that:

void Main()
{
    word[] Words = new word[11]
    {new word {id=1, name= "ABC"},
    new word {id=2, name= "XXX"},
    new word {id=3, name= "Mille"},
    new word {id=4, name= "YYY"},
    new word {id=5, name= "Mill"},
    new word {id=6, name= "Millen"},
    new word {id=7, name= "OOO"},
    new word {id=8, name= "GGGG"},
    new word {id=9, name= null},
    new word {id=10, name= "XXX"},
    new word {id=11, name= "mil"}};

    var target = "mil";
    var comparison = StringComparison.InvariantCultureIgnoreCase;

    var q =  (from w in Words
              where w.name != null
              select new {
                           Match = w.name.StartsWith(target, comparison)?1:2, 
                           name = w.name})
                .OrderBy(w=>w.Match).ThenBy(w=>w.name);
    q.Dump();       
}

public struct word
{
    public int id;
    public string name;
}
James Curran
  • 101,701
  • 37
  • 181
  • 258
  • Side side remark: The reason being that the CLR optimizes `ToUpper` over `ToLower` for this sort of thing. Also, `ToUpperInvariant` should be preferred over normal `ToUpper` to avoid localization issues. – JamesFaix Jun 30 '16 at 22:42
  • I decided to go with the StringComparison version of StartsWith (and then rewrite the whole query) – James Curran Jun 30 '16 at 22:48
0

It is probably not easier but you could create a class that implements IComparable Interface and have a property Mil that is used by CompareTo.

Then you could just call List.Sort(). And you can pass an IComparer to List.Sort.

It would probably be the most efficient and you can sort in place rather than producing a new List.

On average, this method is an O(n log n) operation, where n is Count; in the worst case it is an O(n ^ 2) operation.

 public int CompareTo(object obj) 
 {
    if (obj == null) return 1;

    Temperature otherTemperature = obj as Temperature;
    if (otherTemperature != null)
    {
        if(string.IsNullOrEmpty(Mil) 
            return this.Name.CompareTo(otherTemperature.Name); 
        else if(this.Name.StartsWith(Mill) && otherTemperature.Name.StartsWith(Mill)
            return this.Name.CompareTo(otherTemperature.Name);
        else if(!this.Name.StartsWith(Mill) && !otherTemperature.Name.StartsWith(Mill)
            return this.Name.CompareTo(otherTemperature.Name); 
        else if(this.Name.StartsWith(Mill))
            return 1;
        else 
            return 0;
    }
    else
       throw new ArgumentException("Object is not a Temperature");
 }

You will need to add how you want null Name to sort

paparazzo
  • 44,497
  • 23
  • 105
  • 176
0

First create a list of the words that match, sorted. Then add to that list all of the words that weren't added to the first list, also sorted.

public IEnumerable<Word> GetSortedByMatches(string keyword, Word[] words)
{
    var result = new List<Word>(words.Where(word => word.Name.StartsWith(keyword))
        .OrderBy(word => word.Name));
    result.AddRange(words.Except(result).OrderBy(word => word.Name));
    return result;
} 

Some of the comments suggest that it should be case-insensitive. That would be

public IEnumerable<Word> GetSortedByMatches(string keyword, Word[] words)
{
    var result = new List<Word>(
        words.Where(word => word.Name.StartsWith(keyword, true)) //<-- ignoreCase
        .OrderBy(word => word.Name));
    result.AddRange(words.Except(result).OrderBy(word => word.Name));
    return result;
} 
Scott Hannen
  • 27,588
  • 3
  • 45
  • 62