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 1Book 2
Title: Book Title Only
Desc: There's an ABC in the description of book 2Book 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 1Book 3
Title: Book Title ABC
Desc: ABC is in the beginningBook 2
Title: Book Title Only
Desc: There's an ABC in the description of book 2
How do I achieve this using LINQ?
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.
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")));
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.
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With