1

I have two methods that I need to fix so that when a user searches for "Javascript", the methods will return values if they're spelled like "Javascript" or like "JavaScript" - capitalization won't matter. Importing data from the csv file (where the method performs its search) has already been done with LoadData(). The full code for the three files that I'm using (JobData.cs, Program.cs, job_data.csv) can be found here.

The first method that I need to fix is this one (found in JobData.cs, line 143):

 public static List<Dictionary<string, string>> FindByValue(string searchTerm)
    {
        LoadData();
        //set up a list of jobs that we're going to use to return from this method
        List<Dictionary<string, string>> jobs = new List<Dictionary<string, string>>();
        //row is a Dictionary<string, string>
        foreach (Dictionary<string, string> row in AllJobs)
        {
            //item is a KeyValuePair
            foreach (KeyValuePair<string, string> field in row)
            {
                string aValue = field.Value;
                if (aValue.Contains(searchTerm))
                {
                    jobs.Add(row);
                    break;
                }
            }
        }
        return jobs;
    }  

Maybe one way to do this is to break the searchTerms and values down so that they automatically become lowercase when the user searches. That way, even if the user types JAVAScript, it will automatically turn into javascript, and it will match the characters in the string field, which will also become lowercase. Of course we will still return the original string in field, whether it's "Javascript" or "JavaScript".

Another way to do this would be to automatically turn searchTerm case insensitive, so that it will match up with the field.Value regardless of capitalization.

Would doing that look like this?

  public static List<Dictionary<string, string>> FindByValue(string searchTerm)
    {
        LoadData();
        //set up a list of jobs that we're going to use to return from this method
        List<Dictionary<string, string>> jobs = new List<Dictionary<string, string>>();
        //row is a Dictionary<string, string>
        foreach (Dictionary<string, string> row in AllJobs)
        {
            //item is a KeyValuePair
            foreach (KeyValuePair<string, string> field in row)
            {
                string aValue = field.Value;
                //create new, case-insensitive searchTerm

                culture.CompareInfo.IndexOf(searchTerm, aValue, CompareOptions.IgnoreCase) >= 0

                if (aValue.Contains(searchTerm))
                {
                    jobs.Add(row);
                    break;
                }
            }
        }
        return jobs;
    }   

I'm trying to use this example of case insensitive string comparison. But using that line gives me error messages:

The name "culture" does not exist in the current context
The name "CompareOptions" does not exist in the current context

Any other ideas for how I can turn searchTerms case-insensitive when compared against field.aValue?

Community
  • 1
  • 1
SpeakInCode43
  • 580
  • 4
  • 11
  • 23
  • 1
    Look at the second answer to the question you linked to. – hatchet - done with SOverflow Mar 08 '17 at 21:44
  • 1
    Sean, the `ToUpper()` and `ToLower()` functions do not actually modify the contents of your string. It returns a copy of the string in all uppercase or lowercase letters that you can use for comparison purposes... If you declare a variable like `string sVal = searchTerm.ToLower();`, `sVal`'s characters will all be lowercase. Vice Versa for `ToUpper()`. The fact remains, however, that the `ToUpper()` or `ToLower()` methods will NOT modify your original string (`searchTerm` will not be changed). – Uchiha Itachi Mar 08 '17 at 22:39
  • @UchihaItachi So then could I have done this? https://gist.github.com/4b84a2e280c332e10e28193364cdd978 – SpeakInCode43 Mar 08 '17 at 22:46
  • 1
    @Sean essentially yes. Don't forget that it would be `string newValue = aValue.toUpper();` and `string newsearchTerm = searchTerm.toUpper();`... They're methods - not properties. – Uchiha Itachi Mar 08 '17 at 22:51
  • That also comes with the caveat that you need to do the same thing when you populate your `Dictionary`s... Otherwise when you go searching for keys or values in all uppercase, they may not (and likely wont) be there if they're getting populated via user input... – Uchiha Itachi Mar 08 '17 at 22:52

3 Answers3

1

Let's start by saying that you can't compare strings and NOT have them be case sensitive. They always will be because uppercase and lowercase letters have different Unicode values. Here's your solution:

If you want to blanket check every single string, you can use either the ToUpper() or ToLower() methods. For Example:

string s1 = "JavaScript";
string s2 = "Javascript";

if (s1.ToLower() == s2.ToLower())
{
    //Do something
}

It also works the same with the ToUpper() because:

//s1.ToLower() == "javascript";
//s1.ToUpper() == "JAVASCRIPT";

So, as it pertains to your situation, assuming that your Dictionary is filled with all lowercase strings, you can simply say:

if (aValue.Contains(searchTerm.ToLower()))
{
    //Do something
}

For more information about these functions, check out

https://msdn.microsoft.com/en-us/library/e78f86at(v=vs.110).aspx
and https://msdn.microsoft.com/en-us/library/system.string.toupper(v=vs.110).aspx

Uchiha Itachi
  • 1,251
  • 1
  • 16
  • 42
  • You should normalize strings to how your programming uses them most frequently. Upper or Lower is a matter of preference - not good practice – Uchiha Itachi Mar 08 '17 at 21:54
  • @Sefe tell me if this variable will return true or false (without any extra methods like those you mentioned in your answer): `bool bAreEqual = "JavaScript" == "javaSCRIPT";` – Uchiha Itachi Mar 08 '17 at 22:16
  • @Ðаn your code works. As does @Sefe's. I'm not disputing that. Yours is just a really long and round-about-way of doing string comparisons that is made unnecessary through .NET's core functionality. The root of the case-insensitive thing is that even though `J` and `j` are both J's, they are read differently. They have different Unicode values. That's why if you simply say `if ("J" == "j")` without any helper functions, it will return false. I really don't get why you guys are arguing with me about this... – Uchiha Itachi Mar 08 '17 at 22:47
1

I've played around with this a bit over the years, and the most robust solution I've been able to come up with is to create a String-like wrapper which does everything in a case-insensitive manner. Although depending on your needs, this may be over-kill ...

Usage is:

using StringOrdinalIgnoreCase = JDanielSmith.System.String<JDanielSmith.System.OrdinalIgnoreCase>;

[TestMethod]
public void StringOrdinalIgnoreCaseDictionary()
{
    var d = new Dictionary<StringOrdinalIgnoreCase, int>() { { "abc", 1 }, { "def", 2 } };
    Assert.IsTrue(d.ContainsKey("ABC"));

    try
    {
        d.Add("DEF", 2);
        Assert.Fail();
    }
    catch (ArgumentException) { }
}

String.cs

using System;

using CodeAnalysis = System.Diagnostics.CodeAnalysis;

namespace MyNS.System
{
    /// <summary>
    /// Provide a case-insensitive wrapper around System.String.
    /// 
    /// This is especially useful when using strings as keys in collections, where the key is something like a Windows file-system pathname;
    /// it can be easy to forget to pass an IEqualityComparer<> in the constructor.
    /// 
    /// Some hints from: http://stackoverflow.com/questions/33039324/how-can-system-string-be-properly-wrapped-for-case-insensitivy
    /// </summary>
    [CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "String")]
    [CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1036:OverrideMethodsOnComparableTypes")]
    public sealed class String<TComparerAndComparison> : IComparable, ICloneable,
        IComparable<String<TComparerAndComparison>>, IEquatable<String<TComparerAndComparison>>,
        IComparable<String>, IEquatable<String>
        where TComparerAndComparison : StringComparerAndComparison, new()
    {
        static readonly StringComparerAndComparison _comparerAndComparison = new TComparerAndComparison();
        static readonly StringComparer _comparer = _comparerAndComparison.Comparer;
        static readonly StringComparison _comparisonType = _comparerAndComparison.Comparison;

        public string Value { get; }

        public String(string value)
        {
            // matching the behavior of System.String is more straight-forward if "Value" is never null
            Value = value ?? String.Empty;
        }

        // easily convert to/from System.String
        public static implicit operator String<TComparerAndComparison>(string source) => new String<TComparerAndComparison>(source);
        public static implicit operator string(String<TComparerAndComparison> source) => source?.Value;

        #region Equals, IEquatable
        public override bool Equals(object obj)
        {
            if (Object.ReferenceEquals(obj, null))
                return false; // this != null

            var other = obj as String<TComparerAndComparison>;
            if (!Object.ReferenceEquals(other, null))
                return Equals(other); // call Equals(String<TStringComparerAndComparison>)

            var s_other = obj as string;
            if (!Object.ReferenceEquals(s_other, null))
                return Equals(s_other); // call Equals(string)

            return _comparer.Equals(obj);
        }
        public bool Equals(String<TComparerAndComparison> other)
        {
            if (Object.ReferenceEquals(other, null))
                return false; // this != null
            return Equals(other.Value); // call Equals(string)
        }
        public bool Equals(string other) => _comparer.Equals(Value, other);

        public override int GetHashCode()
        {
            return _comparer.GetHashCode(Value);
        }
        #endregion

        public override string ToString() => Value;

        public object Clone() => new String<TComparerAndComparison>(Value);

        #region IComparable
        public int CompareTo(object obj)
        {
            // https://msdn.microsoft.com/en-us/library/4d7sx9hd(v=vs.110).aspx
            if (Object.ReferenceEquals(obj, null))
                return 1; // If other is not a valid object reference, this instance is greater.

            // obj must be either StringOrdinalIgnoreCase or String
            var other = obj as String<TComparerAndComparison>;
            if (Object.ReferenceEquals(other, null))
            {
                var s_other = obj as string;
                if (Object.ReferenceEquals(s_other, null))
                    throw new ArgumentException("Object must be of type " + nameof(String<TComparerAndComparison>) + " or String.");

                return CompareTo(s_other); // call CompareTo(string)
            }

            return CompareTo(other); // call CompareTo(StringOrdinalIgnoreCase)
        }
        public int CompareTo(String<TComparerAndComparison> other)
        {
            // https://msdn.microsoft.com/en-us/library/4d7sx9hd(v=vs.110).aspx
            if (Object.ReferenceEquals(other, null))
                return 1; // If other is not a valid object reference, this instance is greater.

            if (Object.ReferenceEquals(Value, other.Value))
                return 0;

            return CompareTo(other.Value); // call CompareTo(string)
        }
        public int CompareTo(string other)
        {
            // https://msdn.microsoft.com/en-us/library/4d7sx9hd(v=vs.110).aspx
            if (Object.ReferenceEquals(other, null))
                return 1; // If other is not a valid object reference, this instance is greater.

            return _comparer.Compare(Value, other);
        }

        public static bool operator ==(String<TComparerAndComparison> x, String<TComparerAndComparison> y)
        {
            if (Object.ReferenceEquals(x, null))
                return Object.ReferenceEquals(y, null); // null == null, null != something
            return x.Equals(y); // know x != null
        }
        public static bool operator ==(String<TComparerAndComparison> x, string y)
        {
            if (Object.ReferenceEquals(x, null))
                return Object.ReferenceEquals(y, null); // null == null, null != something
            return x.Equals(y); // know x != null
        }
        public static bool operator ==(string x, String<TComparerAndComparison> y) => y == x; // == is commutative, x == y
        public static bool operator !=(String<TComparerAndComparison> x, String<TComparerAndComparison> y) => !(x == y);
        public static bool operator !=(string x, String<TComparerAndComparison> y) => !(x == y);
        public static bool operator !=(String<TComparerAndComparison> x, string y) => !(x == y);
        #endregion

        #region IndexOf, LastIndexOf, StartsWith, EndsWith
        public bool EndsWith(string value) => Value.EndsWith(value, _comparisonType);
        public int IndexOf(string value) => Value.IndexOf(value, _comparisonType);
        public int IndexOf(string value, int startIndex) => Value.IndexOf(value, startIndex, _comparisonType);
        public int IndexOf(string value, int startIndex, int count) => Value.IndexOf(value, startIndex, count, _comparisonType);
        public int LastIndexOf(string value) => Value.LastIndexOf(value, _comparisonType);
        public int LastIndexOf(string value, int startIndex) => Value.LastIndexOf(value, startIndex, _comparisonType);
        public int LastIndexOf(string value, int startIndex, int count) => Value.LastIndexOf(value, startIndex, count, _comparisonType);
        public bool StartsWith(string value) => Value.StartsWith(value, _comparisonType);
        #endregion

    }
}

StringComparerAndComparison.cs

using System;

using StringComparer = System.StringComparer;
using StringComparison = System.StringComparison;

namespace JDanielSmith.System
{
    /// <summary>
    /// Pass around System.StringComparer and System.StringComparison together.
    /// Also, provides a base class for generics.
    /// </summary>
    public abstract class StringComparerAndComparison
    {
        internal StringComparer Comparer { get; }
        internal StringComparison Comparison { get; }
        internal StringComparerAndComparison(StringComparer comparer, StringComparison comparison)
        {
            if (comparer == null) throw new ArgumentNullException(nameof(comparer));

            Comparer = comparer;
            Comparison = comparison;
        }
    }

    public sealed class CurrentCulture : StringComparerAndComparison
    {
        public CurrentCulture() : base(StringComparer.CurrentCulture, StringComparison.CurrentCulture) { }
    }

    public sealed class CurrentCultureIgnoreCase : StringComparerAndComparison
    {
        public CurrentCultureIgnoreCase() : base(StringComparer.CurrentCultureIgnoreCase, StringComparison.CurrentCultureIgnoreCase) { }
    }

    public sealed class InvariantCulture : StringComparerAndComparison
    {
        public InvariantCulture() : base(StringComparer.InvariantCulture, StringComparison.InvariantCulture) { }
    }

    public sealed class InvariantCultureIgnoreCase : StringComparerAndComparison
    {
        public InvariantCultureIgnoreCase() : base(StringComparer.InvariantCultureIgnoreCase, StringComparison.InvariantCultureIgnoreCase) { }
    }

    public sealed class Ordinal : StringComparerAndComparison
    {
        public Ordinal() : base(StringComparer.Ordinal, StringComparison.Ordinal) { }
    }

    public sealed class OrdinalIgnoreCase : StringComparerAndComparison
    {
        public OrdinalIgnoreCase() : base(StringComparer.OrdinalIgnoreCase, StringComparison.OrdinalIgnoreCase) { }
    }
}
Ðаn
  • 10,934
  • 11
  • 59
  • 95
1

Your idea of using IndexOf instead of Contains is correct, you just have to use the right overload (the one that takes a StringComparison option). There are multiple options for a case-insensitive comparison. I'm using OrdinalIgnoreCase, but you can use the one that suits you best.

Instead of this:

aValue.Contains(searchTerm)

Write this:

aValue.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0
Ðаn
  • 10,934
  • 11
  • 59
  • 95
Sefe
  • 13,731
  • 5
  • 42
  • 55
  • Thanks for your response. So you're saying that I should get rid of the line above the if statment, and then my if statement should look like: 'if (aValue.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0 )'? Doing that gave me a build error: The name 'StringComparison' does not exist in the current context. – SpeakInCode43 Mar 08 '17 at 22:22
  • Have you included the `System` namespace? Try `if (aValue.IndexOf(searchTerm, System.StringComparison.OrdinalIgnoreCase) >= 0 )` – Sefe Mar 08 '17 at 22:27