Custom Comparer against a parameter failing - c#

I am trying to write a custom comparer to sort a list of search results based on similarity. I would like the term most like the entered search term to appear first in the list, followed by phrases that start with the search phrase, then all other values in alpha order.
Given this test code:
string searchTerm = "fleas";
List<string> list = new List<string>
{
"cat fleas",
"dog fleas",
"advantage fleas",
"my cat has fleas",
"fleas",
"fleas on my cat"
};
I'm trying to use this Comparer:
public class MatchComparer : IComparer<string>
{
private readonly string _searchTerm;
public MatchComparer(string searchTerm)
{
_searchTerm = searchTerm;
}
public int Compare(string x, string y)
{
if (x.Equals(_searchTerm) ||
y.Equals(_searchTerm))
return 0;
if (x.StartsWith(_searchTerm) ||
y.StartsWith(_searchTerm))
return 0;
if (x.Contains(_searchTerm))
return 1;
if (y.Contains(_searchTerm))
return 1;
return x.CompareTo(y);
}
Calling list.Sort(new MatchComparer(searchTerm) results in 'my cat has fleas' at the top of the list.
I think I must be doing something odd/weird here .. Is something wrong here or is there a better approach to what I'm trying to do?
Thanks!

You aren't using the all of the possible return values for CompareTo
-1 == x first
0 == are equal
1 == y first
you want something more like this
public class MatchComparer : IComparer<string>
{
private readonly string _searchTerm;
public MatchComparer(string searchTerm)
{
_searchTerm = searchTerm;
}
public int Compare(string x, string y)
{
if (x.Equals(y)) return 0; // Both entries are equal;
if (x.Equals(_searchTerm)) return -1; // first string is search term so must come first
if (y.Equals(_searchTerm)) return 1; // second string is search term so must come first
if (x.StartsWith(_searchTerm)) {
// first string starts with search term
// if second string also starts with search term sort alphabetically else first string first
return (y.StartsWith(_searchTerm)) ? x.CompareTo(y) : -1;
};
if (y.StartsWith(_searchTerm)) return 1; // second string starts with search term so comes first
if (x.Contains(_searchTerm)) {
// first string contains search term
// if second string also contains the search term sort alphabetically else first string first
return (y.Contains(_searchTerm)) ? x.CompareTo(y) : -1;
}
if (y.Contains(_searchTerm)) return 1; // second string contains search term so comes first
return x.CompareTo(y); // fall back on alphabetic
}
}

Related

How to use .OrderBy for multiple conditions, one only sometimes used?

Sorry if the title, is confusing, I had some trouble putting my problem into words.
I have a List, where every string is composed of 2 words, delimited by space.
For example:
{ "word1 word2", "wordA wordB", "dog cat", "mouse cat" }
I want to use OrderBy to sort the list by the 2nd word, if any words are equal, I then want to sort those by the 1st word. I'm having trouble figuring out how to handle the 2nd condition for this (sorting by 1st word only if 2nd words are equal).
I originally tried:
public List<string> SpecialSort(List<string> text)
{
return text.OrderBy(x => x.Split(' ')[1]).ThenBy(x => x.Split(' ')[0]);
}
but this seems to just sort first by the 2nd word, and then re-sort everything by the 1st word. Is there a way for me to do this where I only sort by 1st word if the 2nd words are equal?
Thanks!
My advice would be to split the text into words, while keeping the original text in a Select. Then sort the sequence and finally remove the split words.
Requirement
Input: a sequence of strings, every string has exactly one space.
This space is neither the first nor the last character.
The characters before this one and only space are defined as the first word.
The characters after the space are defined as the second word.
Output: Sort the sequence by 2nd word, then by 1st word.
IEnumerable<string> inputTexts = ...
const string splitChar = ' ';
// first add the split words
var sortedSequence = inputTexts.Select(txt => new
{
Original = txt,
Split = txt.Split(splitChar, StringSplitOptions.None),
})
// then sort by the split words
.OrderBy(splitTxt => splitTxt.Split[1])
.ThenBy(splitTxt => splitTxt.Split[0])
// finally remove the split words
.Select(splitTxt => splitTxt.Original);
Create intermediate results within an .OrderBy() statement can be painful, cause the comparer needs to possibly call them multiple times on each object. Also to make it better maintainable I would write a class that gets the original value, creates the desired elements and feeding these intermediate objects into a specific comparer that can sort them. At the end just get the original value out of the intermediate class and you're done.
A rough sketch for your example would look something like this:
using System;
using System.Collections.Generic;
using System.Linq;
public static class Program
{
private static void Main(string[] args)
{
var words = new List<string>{"word1 word2", "wordA wordB", "dog cat", "mouse cat"};
var ordered = words
.Select(SpecialComparerInstance.Create)
.OrderBy(special => special, SpecialComparer.Default)
.Select(special => special.Value);
foreach (var item in ordered)
{
Console.WriteLine(item);
}
}
}
public class SpecialComparerInstance
{
public static SpecialComparerInstance Create(string value) => new SpecialComparerInstance(value);
public SpecialComparerInstance(string value)
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException(nameof(value));
var elements = value.Split(' ');
if (elements.Length != 2)
throw new ArgumentException("Must contain exactly one space character", nameof(value));
Value = value;
FirstOrderValue = elements[1];
SecondOrderValue = elements[0];
}
public string Value { get; }
public string FirstOrderValue { get; }
public string SecondOrderValue { get; }
}
public class SpecialComparer : IComparer<SpecialComparerInstance>
{
public static readonly IComparer<SpecialComparerInstance> Default = new SpecialComparer(StringComparer.Ordinal);
private readonly StringComparer _comparer;
public SpecialComparer(StringComparer comparer)
{
_comparer = comparer;
}
public int Compare(SpecialComparerInstance x, SpecialComparerInstance y)
{
if (ReferenceEquals(x, y))
return 0;
if (ReferenceEquals(x, null))
return 1;
if (ReferenceEquals(y, null))
return -1;
var result = _comparer.Compare(x.FirstOrderValue, y.FirstOrderValue);
if (result == 0)
result = _comparer.Compare(x.SecondOrderValue, y.SecondOrderValue);
return result;
}
}

Order a list by id

I'm making a web application using Jquery, c # and ASP.Net in this application I insert id and make the id like next way:
the first two digits are the current day, the second two digits are the current month, the third two digits are the current year and a final digit is a consecutive number.
so if I generate the first id of the current day the id will be like this:
DDMMYY+CONSECUTIVE NUMBER
I want to order all id from oldest to newest based on date of id and the consecutive number
how could I make this using linq?
If you don't want to store equivalent versions of the ids as strongly typed objects, I would write a comparer like the following (you can tweak it for your exact requirements and add validation if needed).
public class IdComparer : IComparer<string>
{
public int Compare(string x, string y)
{
int xYear = GetYear(x);
int yYear = GetYear(y);
if (xYear != yYear)
{
return xYear.CompareTo(yYear);
}
int xMonth = GetMonth(x);
int yMonth = GetMonth(y);
if (xMonth != yMonth)
{
return xMonth.CompareTo(yMonth);
}
int xDay = GetDay(x);
int yDay = GetDay(y);
if (xDay != yDay)
{
return xDay.CompareTo(yDay);
}
int xUniqueId = GetUniqueIdentifier(x);
int yUniqueId = GetUniqueIdentifier(y);
if (xUniqueId != yUniqueId)
{
return xUniqueId.CompareTo(yUniqueId);
}
return 0;
}
private static int GetYear(string id)
{
return Int32.Parse(id.Substring(4, 2));
}
private static int GetMonth(string id)
{
return Int32.Parse(id.Substring(2, 2));
}
private static int GetDay(string id)
{
return Int32.Parse(id.Substring(0, 2));
}
private static int GetUniqueIdentifier(string id)
{
return Int32.Parse(id.Substring(6));
}
}
And then you just need to call idList.OrderBy(x => x, new IdComparer());
This feels much cleaner and easier to read than trying to do everything in one linq statement unless you have a specific requirement for doing this?
This class could then be unit tested and any issues / bugs would be easier to resolve than as part of a long linq statement.
So the rules for comparing these id's is:
If the year of one id is greater than the other, then that id is considered greater.
If the years are equal but the month of one id is greater than the other, then that id is considered greater.
If the years and months are equal but the day of one id is greater than the other, then that id is considered greater.
If the years, months, and days are equal but the incrementing portion of one id is greater than the other, then that id is considered greater.
So now we just need to break the id value into 4 parts, after which we can do a proper comparison:
The year part, which is Substring(4, 2)
The month part, which is Substring(2, 2)
The day part, which is Substring(0, 2)
The incrementing number part, which is Substring(7)
Here's one way this could be done, by creating a class that implement IComparer<Item> (where Item is your class). Note: This answer assumes that your id column is a string since you're creating it by concatenating different values together. If it's not a string, then the arguments should be int values and would need to be converted to strings inside the method first:
public class ItemComparer : IComparer<Item>
{
public int Compare(Item x, Item y)
{
return Compare(x?.Id, y?.Id);
}
private int Compare(string first, string second)
{
// Rudimentary argument validation - this could be improved
// Null check - consider null as less than non-null
if (first == null) return second == null ? 0 : -1;
if (second == null) return 1;
// Number check - if fails, return string comparison
if (!first.All(char.IsDigit) || !second.All(char.IsDigit))
return first.CompareTo(second);
// Length check - if fails, return int comparison
if (first.Length < 7 || second.Length < 7)
return int.Parse(first).CompareTo(int.Parse(second));
// Compare years
var result = int.Parse(first.Substring(4, 2))
.CompareTo(int.Parse(second.Substring(4, 2)));
if (result != 0) return result;
// Compare months part
result = int.Parse(first.Substring(2, 2))
.CompareTo(int.Parse(second.Substring(2, 2)));
if (result != 0) return result;
// Compare days part
result = int.Parse(first.Substring(0, 2))
.CompareTo(int.Parse(second.Substring(0, 2)));
if (result != 0) return result;
// Compare incrementing number part
return int.Parse(first.Substring(6)).CompareTo(int.Parse(second.Substring(6)));
}
}
Now that we have this method in place, we can order a list of items by the id. For an example, let's start with a simple class:
public class Item
{
public string Id { get; set; }
public string Value { get; set; }
}
We can populate a list of these items and use our comparer class to order them:
public static void Main(string[] args)
{
var items = new List<Item>
{
// Second-most oldest item on Oct. 14, 2018
new Item {Id = "14101801", Value = "Second"},
// Fifth-most oldest item on Nov. 14, 2019
new Item {Id = "14111901", Value = "Fifth"},
// First-most oldest item on Oct. 13, 2018
new Item {Id = "13101801", Value = "First"},
// Fourth-most oldest item on Nov. 14, 2018 (id 02)
new Item {Id = "14111802", Value = "Fourth"},
// Third-most oldest item on Nov. 14, 2018 (id 01)
new Item {Id = "14111801", Value = "Third"},
};
// Order our items by Id:
items = items.OrderBy(i => i, new ItemComparer()).ToList();
// Output our results
items.ForEach(Console.WriteLine);
GetKeyFromUser("\nDone! Press any key to exit...");
}
Output
If you change your ID to be YYMMDD + the extra number, LINQ's OrderBy(foo => foo.Id) will work exactly as you need. YYMMDD is naturally ordered.
If you don't have the capability to change the format of the ID, you will need to implement a comparer and provide that to OrderBy

Unexpected results for checking if a character is a symbol

I am creating a string extension to check if a string is all symbols or not however it is not working as I would expect it to, so far I have the following:
// Class for: String extensions
public static class StringExtension
{
// Method for: Determining if a string contains only symbols
public static bool ContainsOnlySymbols(this String inputString)
{
// Identifiers used are:
bool containsMore = false;
// Go through the characters of the input string checking for symbols
foreach (char character in inputString.ToCharArray())
{
// This line needs || Char.IsPunctuation(character) also
// Credit: #asantaballa
containsMore = Char.IsSymbol(character) ? false : true;
if (containsMore)
{
return containsMore;
}
}
// Return the results
return containsMore; // Edited after answer: <-- mistake here
}
}
Now if I use this extension on the following two strings I get the opposite of what I expect to see:
string testString = "!=";
I expect this to be all symbols, but
I expect: testString.ContainsOnlySymbols() => true
I get: testString.ContainsOnlySymbols() => false
Now if I use the next test string:
string testString = "Starts with";
I expect this to have no symbols
I expect: testString.ContainsOnlySymbols() => false
I get: testString.ContainsOnlySymbols() => true
A couple problems:
In your loop, you are really only getting the option related to the last character. And or clause should take care of it.
containsMore = containsMore || !(Char.IsSymbol(character) || Char.IsPunctuation(character));
Then, you need a not at the end. If it doesn't contain more, then its only symbols
return ! containsMore;
You might want a special case for how to handle empty strings too. Not sure how you want to handle that. That will be your choice if an empty string should return true or false.
You can accomplish this with a one-liner. See these examples.
string x = "##=";
string z = "1234";
string w = "1234#";
bool b = Array.TrueForAll(x.ToCharArray(), y => (Char.IsSymbol(y) || Char.IsPunctuation(y))); // true
bool c = Array.TrueForAll(z.ToCharArray(), y => (Char.IsSymbol(y) || Char.IsPunctuation(y))); // false
bool e = Array.TrueForAll(w.ToCharArray(), y => (Char.IsSymbol(y) || Char.IsPunctuation(y))); // false
Checking all chars if all isSymbol or Punctuation. we return true here.
public static bool ContainsOnlySymbols(this String inputString)
{
return inputString.ToCharArray().All(x => Char.IsSymbol(x) || Char.IsPunctuation(x));
}
Test:
string testString = "Starts with"; // => false
string testString = "!="; // => true
string testString = "##"; // => true
string testString = "!Starts with"; // => false
I believe the IsSymbol method checks for a very specific set of character. You may want to do:
containsMore = (Char.IsSymbol(character) || Char.IsPunctuation(character)) ? false : true;
Wrote a quick program to show results for character and does show symptom. Might even be that all you need for your app is IsPunctuation.
33/!: IsSymbol=False, IsPunctuation=True
Program
using System;
namespace csharptestchis
{
class Program
{
static void Main(string[] args)
{
for (int i = 0; i <= 255; i++)
{
char ch = (char)i;
bool isSymbol = Char.IsSymbol(ch);
bool isPunctuation = Char.IsPunctuation(ch);
Console.WriteLine($"{i}/{ch}: IsSymbol={isSymbol}, IsPunctuation={isPunctuation} ");
}
}
}
}
Firstly, the idea is simple: you loop your string, if you meet a character non-symbol, return false. Until the end of string and you don't meet a character non-symbol. VoilĂ , return true.
public static bool ContainsOnlySymbols(string inputString)
{
// Identifiers used are:
bool containsMore = false;
// Go through the characters of the input string checking for symbols
foreach (char character in inputString)
{
containsMore = Char.IsSymbol(character) ? false : true;
if(!containsMore)
return false;
}
// Return the results
return true;
}
Secondly, there is a problem with your code, IsSymbol returns true only if your character is in these groups
MathSymbol, CurrencySymbol, ModifierSymbol, and OtherSymbol.
And fortunately, ! don't be in these groups. That means "!=" returns false.
So you must include others conditions like:
public static bool ContainsOnlySymbols(string inputString)
{
// Go through the characters of the input string checking for symbols
return inputString.All(c => Char.IsSymbol(c) || Char.IsPunctuation(c));
}
Or you have to write your own method to determine what symbol is acceptable and what is not.
Or if a string doesn't contain digit and letter, it can be considered symbol. You can do
public static bool ContainsOnlySymbols(string inputString)
{
// Go through the characters of the input string checking for symbols
return !inputString.Any(c => Char.IsLetterOrDigit(c));
}

search string inside string

I'm working on application that's need search method I have listbox full with items every item have singer name and song title , I need be able to search the song title or singer name on same method that's what's I tried so far :
public void search_song()
{
for (int i = listbox_titles.Items.Count - 1; i >= 0; i--)
{
int char_count = listbox_titles.Items[i].ToString().Length;
if (listbox_titles.Items[i].ToString().ToLower().Contains(txt_to_search.Text) || listbox_titles.Items[i].ToString().StartsWith(txt_to_search.Text, StringComparison.Ordinal) || listbox_titles.Items[i].ToString().ToLower().Substring(0, char_count).Contains(txt_to_search.Text)) ;
{
//listbox_titles.SetSelected(i, true);
MessageBox.Show(listbox_titles.Items[i].ToString());
}
}
its work but only search from the beginning of items not middle
any ideas ??
this example of what's I want if item is **avicii waiting for love ** if I search waiting for love is should give me the item .
You just need to find the list box item that contains what you search for, so you don't need for StartsWith method, but since you're saying that your search method only works on the starting string, I can find that you're not converting the text to lower in StartsWith as in Contains, and that might what make the issue. So if your check is case insensitive you can just use the following:
public void search_song()
{
for (int i = listbox_titles.Items.Count - 1; i >= 0; i--)
{
int char_count = listbox_titles.Items[i].ToString().Length;
if (listbox_titles.Items[i].ToString().IndexOf(txt_to_search.Text, StringComparison.OrdinalIgnoreCase) >= 0)
{
//listbox_titles.SetSelected(i, true);
MessageBox.Show(listbox_titles.Items[i].ToString());
}
}
Make sure your code isn't like your original when you do get it working because you will always get the message box:
if (listbox_titles.Items[i].ToString().ToLower().Contains(txt_to_search.Text) ||
listbox_titles.Items[i].ToString().StartsWith(txt_to_search.Text, StringComparison.Ordinal) ||
listbox_titles.Items[i].ToString().ToLower().Substring(0, char_count).Contains(txt_to_search.Text)
) ;
{
//listbox_titles.SetSelected(i, true);
MessageBox.Show(listbox_titles.Items[i].ToString());
}
That green squiggle on the syntax highlighter is pointing you to a warning that it is an empty statement - you have a semi-colon at the end of the if, so your block of code is not conditional at all.
Edit:
public void search_song(string txt_to_search)
{
foreach(var t in listbox_titles.Items)
{
String s = t.ToString().ToLower();
if(s.Contains(txt_to_search.ToLower()))
{
//listbox_titles.SetSelected(i, true);
MessageBox.Show(s);
}
}
}
This works for me because it keeps the size of the lines down to a manageable level - obviously, you would need to index using a variable rather than foreach.
Edit:
If you need to know where the occurrences are you can always define an extension helper:
public void search_song(string txt_to_search)
{
foreach (var t in listbox_titles.Items)
{
if(txt_to_search.Occurrences(t.ToString(), false).Count > 0)
MessageBox.Show(t.ToString());
}
}
}
static class StringHelpers
{
public static List<int> Occurrences(this string pattern, string source, bool caseSensitive = true)
{
List<int> occurs = new List<int>();
if (String.IsNullOrEmpty(pattern) || String.IsNullOrWhiteSpace(pattern))
return occurs;
int index = 0;
if (!caseSensitive)
{
pattern = pattern.ToLower();
source = source.ToLower();
}
while (index < source.Length) // was (index < source.Length - 1)
{
if ((index = source.IndexOf(pattern, index)) < 0)
break;
occurs.Add(index);
++index;
}
return occurs;
}
}
Just capture the list and interrogate it.
Edit: just noticed that I had the scan stop short of the end (no idea why, old age brain fade perhaps). It probably won't make a major difference unless you are searching for single characters (which is what I happened to do!)

Issue with LINQ SequenceEquals extension in C#

I was trying out possibilities to check a string to be an palindrome with the following logic
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Anagram solver");
Console.WriteLine(IsPalindrome("HIMA", "AMHI").ToString());
Console.ReadKey();
}
static bool IsPalindrome(string s1, string s2)
{
return s1.OrderBy(c => c).SequenceEqual(s2.OrderBy(c => c));
}
}
My idea was to get character literals in a string, and compare with that of characters from another string to deduce for a possible palindrome. Is such a thing possible with LINQ SequenceEqual method ?
Looking from the sample above,
'H' shall be compared with 'A' (default equality comparison)
'I' shall be compared with 'M'
'M' shall be compared with 'H'
'A' shall be compared with 'I'
Can any one guide me here.
Thanks and Cheers
Srivatsa
If you want palindrome then you should not order them, just reverse and match -
static bool IsPalindrome(string s1, string s2)
{
return s1.SequenceEqual(s2.Reverse());
}
for case-insensitivity try -
static bool IsPalindrome(string s1, string s2)
{
return s1.ToLower().SequenceEqual(s2.ToLower().Reverse());
}
In your case, "HIMA" and "AMHI" are sorted by the OrderBy LINQ function, which results in two collections containing the characters "AHIM". If you call SequenceEqual this returns true.
For SequenceEqual to return true, both collections have to have the same amount of elements in exactly the same order. No elements are allowed to be duplicated or stored at another position.
If you want to determine if two words are anagrams, that is exactly the functionality you want.
For palindromes, you could use the following:
public bool CheckPalindrome(string first, string second)
{
if (first == null) throw new ArgumentNullException("first");
if (second == null) throw new ArgumentNullExcpetion("second");
return first.Reverse().SequenceEquals(second);
}
You could use this method:
public static bool IsPalindromWith(this string str1, string str2)
{
if(str1 == null || str2 == null) return false;
return str1.SequenceEqual(str2.Reverse());
}
Usage: bool isPalindrom = "HIMA".IsPalindromWith("AMIH");
However, it is a very simple approach which ignores many edge cases.
Here is a better version that takes at least the case into account:
public static bool IsPalindromWith(this string str1, string str2, StringComparison comparison = StringComparison.CurrentCultureIgnoreCase)
{
if(str1 == null || str2 == null) return false;
char[] str2Chars = str2.ToCharArray();
Array.Reverse(str2Chars);
return str1.Equals(new String(str2Chars), comparison);
}
To elaburate on the existing (and i my opinion corrent) answer by #feO2x
Try looking at it like this:
static bool IsAnagram(string s1, string s2)
{
var lst1 = s1.OrderBy(c => c); //will result in { 'A','H','I', 'M' }
var lst2 = s2.OrderBy(c => c); //will *also* result in { 'A','H','I', 'M' }
return lst1.SequenceEqual(lst2);
}
The OrderBy(...) destroys the original order which you are trying to test.
Simply removing them will solve your problem:
static bool IsAnagram(string s1, string s2)
{
var lst1 = s1.AsEnumerable();
var lst2 = s2.AsEnumerable();
return lst1.SequenceEqual(lst2);
}

Categories

Resources