How can System.String be properly wrapped for case-insensitivy?

I would create an immutable struct that hold a string, converting the string in the constructor to a standard case (e.g. lowercase). Then you could also add the implicit operator to simplify the creation and override the compare operators. I think this is the simplest way to achieve the behaviour, plus you get only a small overhead (the conversion is only in the constructor).

Here's the code:

public struct CaseInsensitiveString
{
    private readonly string _s;

    public CaseInsensitiveString(string s)
    {
        _s = s.ToLowerInvariant();
    }

    public static implicit operator CaseInsensitiveString(string d)
    {
        return new CaseInsensitiveString(d);
    }

    public override bool Equals(object obj)
    {
        return obj is CaseInsensitiveString && this == (CaseInsensitiveString)obj;
    }

    public override int GetHashCode()
    {
        return _s.GetHashCode();
    }

    public static bool operator ==(CaseInsensitiveString x, CaseInsensitiveString y)
    {
        return x._s == y._s;
    }

    public static bool operator !=(CaseInsensitiveString x, CaseInsensitiveString y)
    {
        return !(x == y);
    }
}

Here is the usage:

CaseInsensitiveString a = "STRING";
CaseInsensitiveString b = "string";

// a == b --> true

This works for collections as well.


So you want a something that converts a string to an object, and if you convert two strings to two of those objects, you want to be able to compare these objects for equality with your own set of rules about the equality of the two objects.

In your example it is about upper and lower case, but it could also be about forward slashes and backward slashes, maybe you even want to define that the "word" USD equals to $.

Suppose you divide the collection of all possible strings in subcollections of strings that you'd define to be equal. In that case "Hello" would be in the same subcollection as "HELLO" and "hElLO". Maybe "c:\temp" would be in the same collection as "c:/TEMP".

If you could find something to identify your subcollection, then you could say that all strings that belong to the same subcollection would have the same identifier. Or in other words: all strings that you defined equal would have the same identifier.

If that would be possible, then it would be enough to compare the subcollection identifier. If two strings have the same subcollection identifier, then they belong to the same subcollection and thus are considered equal according to our equality definition.

Let's call this identifier the normalized value of the string. The constructor of your CaseInsensitiveString could convert the input string into the normalized value of the string. To check two objects for equality all we have to do is check if they have the same normalized value.

An example of the normalization of a string would be:

  • Make the string lower case
  • make all slashes backward slashes
  • convert all words USD to $
  • remove all thousand separators in numbers without thousand seperator
  • etc, depending on when you want the strings to be equal.

According to the above the following Strings would all lead to the same normalized string:

  • White House $ 1,000,000
  • White House $ 1000000
  • white house USD 1000000

We can define anything as a normalized string, as long as all strings that we define equal have the same normalized string. A good example would be

  • white house $ 1000000

Note: I'm not going into detail about how to find words like USD and thousand separator. The importance is that you understand the meaning of normalized string.

Having said this, the only difficult part is to find the stringIdentifier. The rest of the class is fairly straightforward:

Code for the construction. The constructor takes a string and determines the subcollection it belongs to. I also added a default constructor.

public class CaseInsensitiveString : IEquatable<CaseInsensitiveString>
{
    private string normalized = "";

    public CaseInsensitiveString(string str)
    {
        this.normalized = Normalize(str);
    }

    public CaseInsensitiveString()
    {
         this.Normalize = Normalize(null);
    }
}

Equality: by definition, two objects are the same if they have the same normalized value

See MSDN How to Define Value Equality for a Type

public bool Equals (CaseInsensitiveString other)
{
    // false if other null
    if (other != null) return false;

    // optimization for same object
    if (object.ReferenceEquals(this, other) return true;

    // false if other a different type, for instance subclass
    if (this.Gettype() != other.Gettype()) return false;

    // if here: everything the same, compare the stringIdentifier
    return this.normalized==other.normalized;
}

Note that this last line is the only code where we do actual equality checking!

All other equality functions only use the Equals function defined above:

public override bool Equals(object other)
{
    return this.Equals(other as CaseInsensitiveString);
}

public override int GetHashCode()
{
    return this.Normalized.GetHashCode();
}

public static bool operator ==(CaseInsensitiveString x, CaseInsensitiveString y)
{
    if (object.ReferenceEquals(x, null)
    {   // x is null, true if y also null
        return y==null;
    }
    else
    {   // x is not null
        return x.Equals(y);
    }
}

public static bool operator !=(CaseInsensitiveString x, CaseInsensitiveString y)
{
    return !operator==(x, y);
}

So now you can do the following:

var x = new CaseInsensitiveString("White House $1,000,000");
var y = new CaseInsensitiveString("white house $1000000");
if (x == y)
    ...

Now the only thing we have to implement is the Normalize function. Once you know when two strings are considered equal you know how to normalize.

Suppose consider two strings equal if they are equal case insensitive and forward slashes are the same as backward slashes. (bad English)

If the normalize function returns the same string in lower case with all backward slashes, then two strings that we consider equal will have the same normalized value

private string Normalize(string str)
{
    return str.ToLower().Replace('/', '\');
}

Tags:

C#

.Net

String