Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sort a collection and rank the result based on certain criteria

Tags:

c#

sorting

linq

Say I have the following

var searches = new ObservableCollection<Book>();

searches contains the book objects

public class Book
{
    public string Title { get; set;}
    public string Desc {get; set;}    
}

I want to sort searches by a matching string. First it checks the Title than rank them based on how close the search string from the beginning of Title. Next it checks Desc and rank them by how close the search string appears from the beginning of the `Desc.

For example if I have

Book 1
Title: ABC Book Title
Desc: The description of book 1

Book 2
Title: Book Title Only
Desc: There's an ABC in the description of book 2

Book 3
Title: Book Title ABC
Desc: ABC is in the beginning

So let say the search keyword is ABC, I want searches to be sorted so that I get the following. The result place higher priority to items that contains the search string in the title.

Book 1
Title: ABC Book Title
Desc: The description of book 1

Book 3
Title: Book Title ABC
Desc: ABC is in the beginning

Book 2
Title: Book Title Only
Desc: There's an ABC in the description of book 2

How do I achieve this using LINQ?

like image 222
PutraKg Avatar asked May 08 '14 09:05

PutraKg


3 Answers

You can use a rank function to define a "score" for each book and then sort by score.

i.e.

var searchString = "ABC";
var results = books.Select(b => new { Book = b, Rank = RankBook(b, searchString) })
                   .OrderBy(r => r.Rank)
                   .Select(r => r.Book.Title);

And the rank function:

private int RankBook(Book b, string searchString)
{
    int rank = 0;
    if (b.Title.Contains(searchString)) rank += 10;

    if (b.Desc.Contains(searchString)) rank += 5;

    return rank;
}

This is saying: found in title=10 point, found in desc=5 points, so uo get the most relevant books with higher scores.

like image 125
Stefano Altieri Avatar answered Nov 01 '22 14:11

Stefano Altieri


You can use OrderBy and ThenBy

var searches = new ObservableCollection<Book>();

searches.Add(new Book()
{
    Desc = "The description of book 1",
    Title = "ABC Book Title"
});

searches.Add(new Book()
{
    Desc = "Book Title Only",
    Title = "There's an ABC in the description of book 2"
});

searches.Add(new Book()
{
    Desc = "Book Title ABC",
    Title = "ABC is in the beginning"
});

var ordered = new ObservableCollection<Book>(searches.OrderBy(book => book.Title).ThenBy(book => book.Desc.Contains("ABC")));

Update

I have added a ranking system which will hopefully assist you with what you are looking for. All I use is IndexOf to determine the location of your criteria and store it in a property within the Book object. The other suggestion I have is that you create a standalone collection (using inheritance) for your books, this way you could customise it to your needs without having to write too much code outside the context of the object itself

public class BookCollection : ObservableCollection<Book> // Notice the Inheritance to ObservableCollection
{
    public void SetCriteria(string search)
    {
        if(string.IsNullOrEmpty(search))
            return;

        foreach (var book in this)
        {
            if(book.Title.Contains(search))
                book.TitleRank = book.Title.IndexOf(search, StringComparison.InvariantCulture);

            if(book.Desc.Contains(search))
                book.DescRank = book.Desc.IndexOf(search, StringComparison.InvariantCulture);
        }

        var collection = new List<Book>(base.Items.OrderBy(book => book.Title)
                                                  .ThenBy(book => book.Desc)
                                                  .ThenBy(book => book.TitleRank)
                                                  .ThenBy(book => book.DescRank));
        Items.Clear();

        collection.ForEach(Add);
        collection.Clear();
    }
}

public class Book
{
    public string Title { get; set; }
    public string Desc { get; set; }
    public int TitleRank { get; internal set; }
    public int DescRank { get; internal set; }
}

Now to use this new collection, all you have to do is call it like this.

var collection = new BookCollection();
collection.Add(new Book { Desc = "Book Title ABC", Title = "ABC is in the beginning" });
// Add your other books here........
collection.SetCriteria("ABC");
// your new collection is now sorted and ready to use, no need to write any extra sorting code here

Remember that if you need to add more conditions to your sorting, the only place you have to do this is in the SetCriteria method. Hope this helps.

like image 32
Mo Patel Avatar answered Nov 01 '22 16:11

Mo Patel


Thanks to suggestion by @M Patel and Stefano, I've arrived at the following solution

var sorted = searches.Select(tile => new { TileViewModel = tile, Rank = rankResult(tile, text) })
                    .OrderByDescending(r => r.Rank)
                    .Select(r => r.TileViewModel);

SearchResultsTilesVM = new ObservableCollection<TileViewModel>(sorted);

The method that take the position of the keyword. I added extra point if a match is found in the title.

    private int rankResult(TileViewModel vm, string keyword)
    {
        double rank = 0;

        //Added 100 to give stronger weight when keyword found in title
        int index = vm.Title.IndexOf(keyword, StringComparison.InvariantCultureIgnoreCase);

        if (index >= 0 )
        {
            rank = (double)(vm.Title.Length - index) / (double)vm.Title.Length * 100 + 100;
        }         

        int index2 = vm.Information.IndexOf(keyword, StringComparison.InvariantCultureIgnoreCase);

        if (index2 >= 0)
        {
            rank += (double)(vm.Information.Length - index2) / (double)vm.Information.Length * 100;
        }

        return Convert.ToInt32(rank);
    }
like image 2
PutraKg Avatar answered Nov 01 '22 16:11

PutraKg