Edward Tanguay
Edward Tanguay

Reputation: 193442

How can I make this LINQ search method handle more than two terms?

The following search method works fine for up to two terms.

How can I make it dynamic so that it is able to handle any number of search terms?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestContains82343
{
    class Program
    {
        static void Main(string[] args)
        {
            List<string> tasks = new List<string>();
            tasks.Add("Add contract to Customer.");
            tasks.Add("New contract for customer.");
            tasks.Add("Create new contract.");
            tasks.Add("Go through the old contracts.");
            tasks.Add("Attach files to customers.");

            var filteredTasks = SearchListWithSearchPhrase(tasks, "contract customer");

            filteredTasks.ForEach(t => Console.WriteLine(t));
            Console.ReadLine();
        }

        public static List<string> SearchListWithSearchPhrase(List<string> tasks, string searchPhrase)
        {
            string[] parts = searchPhrase.Split(new char[] { ' ' });
            List<string> searchTerms = new List<string>();
            foreach (string part in parts)
            {
                searchTerms.Add(part.Trim());
            }

            switch (searchTerms.Count())
            {
                case 1:
                    return (from t in tasks
                            where t.ToUpper().Contains(searchTerms[0].ToUpper()) 
                            select t).ToList();
                case 2:
                    return (from t in tasks
                            where t.ToUpper().Contains(searchTerms[0].ToUpper()) && t.ToUpper().Contains(searchTerms[1].ToUpper())
                            select t).ToList();
                default:
                    return null;
            }

        }
    }
}

Upvotes: 3

Views: 2662

Answers (5)

Bahamut
Bahamut

Reputation: 1939

why not use a foreach and AddRange() after splitting the terms and saving it into a list.

List<ItemsImLookingFor> items = new List<ItemsImLookingFor>();

// search for terms
foreach(string term in searchTerms)
{
   // add users to list
   items.AddRange(dbOrList(
         item => item.Name.ToLower().Contains(str)).ToList()
   );
}

that should work for any amount of terms.

Upvotes: 0

Jon Skeet
Jon Skeet

Reputation: 1503469

Just call Where repeatedly... I've changed the handling of searchTerms as well to make this slightly more LINQ-y :)

public static List<string> SearchListWithSearchPhrase
    (List<string> tasks, string searchPhrase)
{
    IEnumerable<string> searchTerms = searchPhrase.Split(' ')
                                                  .Select(x => x.Trim());
    IEnumerable<string> query = tasks;
    foreach (string term in searchTerms)
    {
        // See edit below
        String captured = term;
        query = query.Where(t => t.ToUpper().Contains(captured));
    }
    return query.ToList();
}

You should note that by default, ToUpper() will be culture-sensitive - there are various caveats about case-insensitive matching :( Have a look at this guidance on MSDN for more details. I'm not sure how much support there is for case-insensitive Contains though :(

EDIT: I like konamiman's answer, although it looks like it's splitting somewhat differently to your original code. All is definitely a useful LINQ operator to know about...

Here's how I would write it though:

return tasks.Where(t => searchTerms.All(term => t.ToUpper().Contains(term)))
            .ToList();

(I don't generally use a query expression when it's a single operator applied to the outer query.)

EDIT: Aargh, I can't believe I fell into the captured variable issue :( You need to create a copy of the foreach loop variable as otherwise the closure will always refer to the "current" value of the variable... which will always be the last value by the time ToList is executed :(

EDIT: Note that everything so far is inefficient in terms of uppercasing each task several times. That's probably fine in reality, but you could avoid it by using something like this:

IEnumerable<string> query = tasks.Select
    (t => new { Original = t, Upper = t.ToUpper });
return query.Where(task => searchTerms.All(term => task.Upper.Contains(term)))
            .Select(task => task.Original)
            .ToList();

Upvotes: 3

Rob Fonseca-Ensor
Rob Fonseca-Ensor

Reputation: 15621

Replace the switch statement with a for loop :)

    [TestMethod]
    public void TestSearch()
    {
        List<string> tasks = new List<string>
            {
                "Add contract to Customer.",
                "New contract for customer.",
                "Create new contract.",
                "Go through the old contracts.",
                "Attach files to customers."
            };

        var filteredTasks = SearchListWithSearchPhrase(tasks, "contract customer new");

        filteredTasks.ForEach(Console.WriteLine);
    }

    public static List<string> SearchListWithSearchPhrase(List<string> tasks, string searchPhrase)
    {
        var query = tasks.AsEnumerable();

        foreach (var term in searchPhrase.Split(new[] { ' ' }))
        {
            string s = term.Trim();
            query = query.Where(x => x.IndexOf(s, StringComparison.InvariantCultureIgnoreCase) != -1);
        }

        return query.ToList();
    }

Upvotes: 0

Konamiman
Konamiman

Reputation: 50323

Can't test code right now, but you could do something similar to this:

from t in tasks
let taskWords=t.ToUpper().Split(new char[] { ' ' });
where searchTerms.All(term => taskWords.Contains(term.ToUpper()))
select t

Upvotes: 1

Buu
Buu

Reputation: 50395

How about replacing

switch (searchTerms.Count())
{
    case 1:
        return (from t in tasks
                where t.ToUpper().Contains(searchTerms[0].ToUpper())
                select t).ToList();
    case 2:
        return (from t in tasks
                where t.ToUpper().Contains(searchTerms[0].ToUpper()) && t.ToUpper().Contains(searchTerms[1].ToUpper())
                select t).ToList();
    default:
        return null;
}

By

(from t in tasks
 where searchTerms.All(term => t.ToUpper().Contains(term.ToUpper()))
 select t).ToList();

Upvotes: 5

Related Questions