I am trying to query using EF. The user can use up to 3 search words but they are not required. How do I write an EF query that will work as an AND for all of the search words that are used but be able to remove an AND for any search word that is empty?
Example, I want the following to return the first two elements in the array for s1='mobile', s2='', and s3='laptop'. It's not returning any. It should return the first two if s2 is changed to s2='burke'.
Example:
using System;
using System.Linq;
public class Simple {
public static void Main() {
string[] names = { "Burke laptop mobile", "laptop burke mobile", "Computer Laptop",
"Mobile", "Ahemed", "Sania",
"Kungada", "David","United","Sinshia" };
//search words
string s1 = "mobile";
string s2 = "";
string s3 = "laptop";
var query = from s in names
where (!string.IsNullOrEmpty(s1) && s.ToLower().Contains(s1))
&& (!string.IsNullOrEmpty(s2) && s.ToLower().Contains(s2))
&& (!string.IsNullOrEmpty(s3) && s.ToLower().Contains(s3))
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
Instead of trying to write a single where statement, you can optionally extend an existing IQueryable<T> (or IEnumerable<T> like our code sample would be using) by chaining Where statements.
var query = names;
if (!string.IsNullOrEmpty(s1))
{
query = query.Where(x => x.Contains(s1));
}
if (!string.IsNullOrEmpty(s2))
{
query = query.Where(x => x.Contains(s2));
}
// ... (or make a foreach loop if s1,s2,s3 were an array.
var results = query.OrderBy(x => x).Select(x => x.ToUpper());
Chaining Where like this is equivalent to "anding" all where predicates together.
EDIT:
To update why your specific implementation doesn't work is because your && operators are incorrect for the given use-case.
(string.IsNullOrEmpty(s1) || s.ToLower().Contains(s1)) &&
(string.IsNullOrEmpty(s2) || s.ToLower().Contains(s2)) &&
(string.IsNullOrEmpty(s3) || s.ToLower().Contains(s3))
Remember that && requires both left and right statements to be true, so in your case !string.IsNullOrEmpty(s2) && s.ToLower().Contains(s2) this is saying that s2 must always be not-empty/null.
Please consider this:
using System;
using System.Linq;
public class Simple {
public static void Main() {
string[] names = { "Burke laptop mobile", "laptop burke mobile", "Computer Laptop",
"Mobile", "Ahemed", "Sania",
"Kungada", "David","United","Sinshia" };
//search words
string s1 = "mobile";
string s2 = "";
string s3 = "laptop";
var query = from s in names
where (s1 != null && s.ToLower().Contains(s1))
&& (s2 != null && s.ToLower().Contains(s2))
&& (s3 != null && s.ToLower().Contains(s3))
orderby s
select s.ToUpper();
foreach (string item in query)
Console.WriteLine(item);
}
}
If you use an array or list instead of multiple strings, you could do something like
List<string> searchWords = new List<string>
{
"mobile",
"",
"laptop"
};
var query = names
.Where(n => searchWords
.Where(s => !string.IsNullOrEmpty(s))
.All(s => n.ToLower().Contains(s)))
.Select(n => n.ToUpper())
.OrderBy(n => n);
This also is more flexible, as you can have any number of search words, without changing the query.
Related
The Problem:
My customers want to be able to search for first and last names in my site, using any version of ', `, or those goofy microsoft 'start' and 'end' quotes.
I would like to be able to write a single statement that looks for all possible permutations of that evil symbol, like such:
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList();
and if the user was searching for "O'Brien", I would show them results for "O'Brien", and "O`Brien", and any other version of that stupid thing.
What's the best way to do that? Is there a way to do it (without a massive recursion where I do things like
if (firstName.Contains("'")
{
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList();
firstName = firstName.Replace("'", "`");
customers.AddRange(_sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Contains(firstName)
&& c.LastName.Contains(lastName)
&& !c.IsEmployee)
.ToList());
}
etc...
Update
private static readonly string[] Quotes = { "#", "'", "‘", "’", "`" };
private IEnumerable<customer> CustomersByFirstName(string firstName, bool showInactive = true)
{
firstName = firstName.Replace(Quotes[1], Quotes[0]);
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => (c.FirstName.Replace(Quotes[1], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[2], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[3], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[4], Quotes[0]).Contains(firstName))
&& !c.IsEmployee)
.ToList();
if (!showInactive)
{
customers = customers.Where(c => !c.Disabled).ToList();
}
return customers;
}
This is almost it... Almost
Final Update
This turns out to be exactly the solution, tho not as elegant and small as I might like:
private IEnumerable<customer> CustomersByFirstName(string firstName, bool showInactive = true)
{
firstName = firstName.Replace(Quotes[1], Quotes[0]);
firstName = firstName.Replace(Quotes[2], Quotes[0]);
firstName = firstName.Replace(Quotes[3], Quotes[0]);
firstName = firstName.Replace(Quotes[4], Quotes[0]);
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => (c.FirstName.Replace(Quotes[1], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[2], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[3], Quotes[0]).Contains(firstName) ||
c.FirstName.Replace(Quotes[4], Quotes[0]).Contains(firstName))
&& !c.IsEmployee)
.ToList();
if (!showInactive)
{
customers = customers.Where(c => !c.Disabled).ToList();
}
return customers;
}
Sorry, I posted an answer without fully understanding the question. Here's the corrected answer.
I would define your criteria for a match in a separate function. Here's how I implemented it:
// Add quote characters as needed.
List<char> quotes = new List<char> { '\'', '‘', '’' };
public string CleanName(string name)
{
StringBuilder cleanName = new StringBuilder(name);
foreach (char quote in quotes)
{
cleanName = cleanName.Replace(quote, '\'');
}
return cleanName.ToString();
}
public bool IsMatch(string n0, string n1)
{
return String.Compare(CleanName(n0), CleanName(n1)) == 0;
}
Then, search for matching names using the IsMatch function, as demonstrated in this unit test:
[TestMethod]
public void TestQuoteSearch()
{
string searchName = "O'Brien";
var matchingNames = names.Where(name => IsMatch(name, searchName));
Assert.AreEqual(4, matchingNames.Count());
Assert.IsTrue(matchingNames.Contains("O'Brien"));
Assert.IsTrue(matchingNames.Contains("O‘Brien"));
Assert.IsTrue(matchingNames.Contains("O’Brien"));
Assert.IsFalse(matchingNames.Contains("Jones"));
}
You could do something like :
char[] all_quotes= new char[] { '\'', '‘', '’' /* any other quote you like */ };
Then replace them in your names with a special char (lets say *), or cut them out all together, and then search for those matches then.
have a look at this answer for an example of how to do it, or brew your own:
var customers = _sms.CurrentSession.Query<customer>()
.Where(c => c.FirstName.Replace(all_quotes, "*")
.Contains(firstName.Replace(all_quotes, "*"))
&& // same for others)
.ToList();
Regarding Final Update:
I wouldn't have the one to replace as part as the quotes. I'd do something like :
string inplace = "#";
private static readonly string[] Quotes = {"'", "‘", "’", "`" };
foreach (var q in Quotes)
firstName = firstName.Replace(q, inplace);
you can wrap the foreach in { } if you'd like. It's shorter, and if you end up adding a quote to the array, you don't need another line of "replace"
Create Store Procedure and import to Entity Modal then call like method.
I want to find a string of fixed length with specific substring. But I need to do it like we can do in SQL queries.
Example:
I have strings like -
AB012345
AB12345
AB123456
AB1234567
AB98765
AB987654
I want to select strings that have AB at first and 6 characters afterwards. Which can be done in SQL by SELECT * FROM [table_name] WHERE [column_name] LIKE 'AB______' (6 underscores after AB).
So the result will be:
AB012345
AB123456
AB987654
I need to know if there is any way to select strings in such way with C#, by using AB______.
You can use Regular Expressions to filter the result:
List<string> sList = new List<string>(){"AB012345",
"AB12345",
"AB123456",
"AB1234567",
"AB98765",
"AB987654"};
var qry = sList.Where(s=>Regex.Match(s, #"^AB\d{6}$").Success);
Considering you have an string array:
string[] str = new string[3]{"AB012345", "A12345", "AB98765"};
var result = str.Where(x => x.StartsWith("AB") && x.Length == 8).ToList();
The logic is if it starts with AB, and its length is 8. It is your best match.
this should do it
List<string> sList = new List<string>(){
"AB012345",
"AB12345",
"AB123456",
"AB1234567",
"AB98765",
"AB987654"};
List<string> sREsult = sList.Where(x => x.Length == 8 && x.StartsWith("AB")).ToList();
first x.Length == 8 determines the length and x.StartsWith("AB") determines the required characters at the start of the string
This can be achieved by using string.Startwith and string.Length function like this:
public bool CheckStringValid (String input)
{
if (input.StartWith ("AB") && input.Length == 8)
{
return true;
}
else
{
return false;
}
}
This will return true if string matches your criteria.
Hope this helps.
var strlist = new List<string>()
{
"AB012345",
"AB12345",
"AB123456",
"AB1234567",
"AB98765",
"AB987654"
};
var result = strlist.Where(
s => (s.StartsWith("AB") &&(s.Length == 8))
);
foreach(var v in result)
{
Console.WriteLine(v.ToString());
}
I have a search with optional arguments:
string id1 = HttpContext.Current.Request["id1"];
string id2 = HttpContext.Current.Request["id2"];
List<Foo> list = context.Foo.Where(l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1)) &&
(string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)))
.Take(10)
.ToList());
I would like to extend it so that if the string starts with a *, the EndsWith() - method should be used.
For example, if the first searchstring is *123, I want to do a l.Id1.EndsWith("123").
Is there any way to extend my current code, or should I use a different approach?
I'm quite sure if this is what you were intending, but you can break your logic up by using IQueryable(T). The query won't be executed until you try to use the collection.
public IList<Foo> Search(DbContext<Foo> context, string id1, string id2)
{
Func<Foo, bool> predicate = l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1))
&& (string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)));
IQueryable<Foo> list = context.Foo.Where(predicate);
if(id1.StartsWith("*") && id1.Length > 1)
{
var searchTerm = id1.Substring(1, id1.Length);
list = list.Where(l => l.EndsWith(searchTerm));
}
return list.Take(10).ToList(); // Execution occurs at this point
}
Building up the query:
public void BasicSearch(IQueryable<foo> list, string id1, string id2)
{
Func<Foo, bool> predicate = l =>
(string.IsNullOrEmpty(id1) || l.Id1.Contains(id1))
&& (string.IsNullOrEmpty(id2) || l.Id2.Contains(id2)));
list.Where(predicate);
}
public void WildcardSearch(IQueryable<Foo> list, string id1)
{
if(!id1.StartsWith("*") || id1.Length <= 1) return;
var searchTerm = id1.Substring(1, id1.Length);
list.Where(l => l.EndsWith(searchTerm));
}
IQueryable<Foo> list = context.Foo;
BasicSearch(list, id1, id2);
WildcardSearch(list, id1);
var result = list.Take(10); // Execution occurs at this point
I have a delimited string that I need sorted. First I need to check if 'Francais' is in the string, if so, it goes first, then 'Anglais' is next, if it exists. After that, everything else is alphabetical. Can anyone help me? Here's what I have so far, without the sorting
private string SortFrench(string langs)
{
string _frenchLangs = String.Empty;
string retval = String.Empty;
_frenchLangs = string.Join(" ; ",langs.Split(';').Select(s => s.Trim()).ToArray());
if (_frenchLangs.Contains("Francais"))
retval += "Francais";
if (_frenchLangs.Contains("Anglais"))
{
if (retval.Length > 0)
retval += " ; ";
retval += "Anglais";
}
//sort the rest
return retval;
}
Someone liked my comment, so figured I'd go ahead and convert that into your code:
private string SortFrench(string langs)
{
var sorted = langs.Split(';')
.Select(s => s.Trim())
.OrderByDescending( s => s == "Francais" )
.ThenByDescending( s => s == "Anglais" )
.ThenBy ( s => s )
.ToArray();
return string.Join(" ; ",sorted);
}
My syntax may be off slightly as I've been in the Unix world for awhile now and haven't used much LINQ lately, but hope it helps.
Here's what I came up with. You could change the .Sort() for a OrderBy(lang => lang) after the Select, but I find it's cleaner that way.
public string SortLanguages(string langs)
{
List<string> languages = langs.Split(';').Select(s => s.Trim()).ToList();
languages.Sort();
PlaceAtFirstPositionIfExists(languages, "anglais");
PlaceAtFirstPositionIfExists(languages, "francais");
return string.Join(" ; ", languages);
}
private void PlaceAtFirstPositionIfExists(IList<string> languages, string language)
{
if (languages.Contains(language))
{
languages.Remove(language);
languages.Insert(0, language);
}
}
You should use a custom comparer class
it will allow you to use the built in collection sorting functions, or the linq OrderBy using your own criteria
Try this:
private string SortFrench(string langs)
{
string _frenchLangs = String.Empty;
List<string> languages = langs
.Split(';')
.Select(s => s.Trim())
.OrderBy(s => s)
.ToList();
int insertAt = 0;
if (languages.Contains("Francais"))
{
languages.Remove("Francais");
languages.Insert(insertAt, "Francais");
insertAt++;
}
if(languages.Contains("Anglais"))
{
languages.Remove("Anglais");
languages.Insert(insertAt, "Anglais");
}
_frenchLangs = string.Join(" ; ", languages);
return _frenchLangs;
}
All can be done in single line
private string SortFrench(string langs)
{
return string.Join(" ; ", langs.Split(';').Select(s => s.Trim())
.OrderBy(x => x != "Francais")
.ThenBy(x => x != "Anglais")
.ThenBy(x=>x));
}
Sorting alphabetically is simple; adding .OrderBy(s => s) before that .ToArray() will do that. Sorting based on the presence of keywords is trickier.
The quick and dirty way is to split into three:
Strings containing "Francais": .Where(s => s.Contains("Francais")
Strings containing "Anglais": .Where(s => s.Contains("Anglais")
The rest: .Where(s => !francaisList.Contains(s) && !anglaisList.Contains(s))
Then you can sort each of these alphabetically, and concatenate them.
Alternatively, you can implement IComparer using the logic you described:
For strings A and B:
If A contains "Francais"
If B contains "Francais", order alphabetically
Else
If B contains "Francais", B goes first
Else
If A contains "Anglais"
If B contains "Anglais", order alphabetically
Else, A goes first
Else, order alphabetically
There may be room for logical re-arrangement to simplify that. With all that logic wrapped up in a class that implements IComparer, you can specify that class for use by .OrderBy() to have it order your query results based on your custom logic.
You can also use Array.Sort(yourStringArray)
This code creates a list of the languages, sorts them using a custom comparer, and then puts the sorted list back together:
const string langs = "foo;bar;Anglais;Francais;barby;fooby";
var langsList = langs.Split(';').ToList();
langsList.Sort((s1, s2) =>
{
if (s1 == s2)
return 0;
if (s1 == "Francais")
return -1;
if (s2 == "Francais")
return 1;
if (s1 == "Anglais")
return -1;
if (s2 == "Anglais")
return 1;
return s1.CompareTo(s2);
});
var sortedList = string.Join(";", langsList);
Console.WriteLine(sortedList);
This way you can set any list of words in front:
private static string SortFrench(string langs, string[] setStartList)
{
string _frenchLangs = String.Empty;
List<string> list = langs.Split(';').Select(s => s.Trim()).ToList();
list.Sort();
foreach (var item in setStartList){
if (list.Contains(item))
{
list.Remove(setFirst);
}
}
List<string> tempList = List<string>();
tempList.AddRange(setStartList);
tempList.AddRange(list);
list = tempList;
_frenchLangs = string.Join(" ; ", list);
return _frenchLangs;
}
I stuck in an easy scenario. I have a List<string> object, all of its items has the body of:
item_1_2_generatedGUID //generatedGUID is Guid.NewGuid()
but there may be much more numbers
item_1_2_3_4_5_generatedGUID etc
now, I'm wondering how to change that loop into the LINQ's query. Any ideas ?
string one = "1"; //an exaplme
string two = "2"; //an exaplme
foreach (var item in myStringsList)
{
string[] splitted = item.Split(new char[] { '_' },
StringSplitOptions.RemoveEmptyEntries);
if(splitted.Length >= 3)
{
if(splitted[1] == one && splitted[2] == two)
{
resultList.Add(item);
}
}
}
var result = from s in lst
let spl = s.Split('_')
where spl.Length >= 3 && spl[1] = one && spl[2] == two
select s;
Try this:
var query = from item in myStringsList
let splitted = item.Split(new[] { '_' }, SSO.RemoveEmptyEntries)
where splitted.Length >= 3
where splitted[1] == one && splitted[2] == two
select item;
var resultList = query.ToList();
This is a different approach:
var items = myStringsList.
Where(x => x.Substring(x.IndexOf("_")).StartsWith(one+"_"+two+"_"));
You probably will need to add a +1 in the IndexOf, but I'm not sure.
What it does is:
Removes the first item (that's the substring for). In your example, it should be "1_2_3_4_5_generatedGUID"
Checks the string starts with what you are expecting. In your example: 1_2_
Edited: Added the pattern for anything at the first "position"
var result = items.Where(i => Regex.IsMatch(i, "^[^_]_1_2_"));