tags:

views:

163

answers:

4

I have a list in C# of Vendors that all have a Name property. I want to allow a user to filter that list by searching for a Name. The filter string can be a partial or complete match. However, if the resulting list contains an exact match, it should be in position zero in the list with all partial matches after that.

I can get the sub-list pretty easily with linq and lambdas but I'm having to resort to a hack of creating a second list if an exact match exists, adding it, and then adding the rest of the matches without the exact one. It feels inelegant. Is there an easier way? My current code (done from memory so it may not compile):

List<Vendor> temp = vendors.Where(v => v.Name.ToUpper().Contains(vendorNameSearch)).ToList();
Vendor exactMatch = vendors.Single(v => v.Name.ToUpper().Equals(vendorNameSearch));

if(null == exactMatch){return temp;}
else
{
    List<Vendor> temp1 = new List<Vendor>();
    temp1.Add(exactMatch);
    temp1.AddRange(temp.Remove(exactMatch));
    return temp1;
}
+1  A: 

How about developing a StringComparer that computes the Levenshtein distance -- for exact matches this ought to be zero -- and ordering the results by this in ascending order. This way you get your exact match first AND the rest of the results are ordered by the similarity (at least one measure of it) to the search string.

var list = vendors.Where( v => v.Name.ToUpper().Contains( vendorNameSearch ) )
                  .OrderBy( v => ComputeLevenshtein( v.Name.ToUpper(),
                                                     vendorNameSearch ) );

Or you could make a comparer that orders things with exact matches being int.MinValue and all other values being the result of CompareTo(). This would also order the exact match(s) first.

tvanfosson
A: 

I have no idea how efficient this is but here is another option,

List<String> strings = new List<String> {"Cat","Dog","Pear","Apple","Catalog"};

var results = (from st in strings
    where st == "Cat"
    select new {Priority = 1,st}).Union(

    from st in strings
    where st.Contains("Cat")
    select new {Priority = 2, st}).OrderBy(x => x.Priority).Select(x=> x.st).Distinct();
Darrel Miller
+1  A: 

Order by the absolute difference in string lengths. The exact match will be the only one with the same length (abs. diff. = 0), while the others will all be greater than 0:

var list = vendors.Where(v => v.Name.ToUpper().Contains(vendorNameSearch))
                  .OrderBy(v => Math.Abs(v.Name.ToUpper().Length - vendorNameSearch.Length)));

It'll be an otherwise arbitrary ordering, but it fulfills your main goal.

But of course, there's nothing wrong with a two-step solution like your original code, if it does the job.

kevingessner
+2  A: 

Firstly, I like the approach in tvanfosson's answer from a usability perspective.

Otherwise you can achieve your desired behavior using a composite sort:

vendors.Where(v => v.Name.ToUpper().Contains(vendorNameSearch))
       .OrderBy(v => !string.Equals(v.Name, vendorNameSearch))
       .ThenBy(v => v.Name)
       .ToList();

Ordering by the boolean result of the exact match comparison will ensure that the exact match is listed first. Due to the way that boolean values are sorted, the vendor with an exact name match should have a value of false. You could also achieve this using OrderByDescending rather than inverting the string equality.

Nathan Baulch