tags:

views:

324

answers:

3

I've got a number of classes with fields that are meant to be case insensitive, and I'd like to put the instances of these classes into HashMaps and look them up by string case insensitive.

Instead of using toLowerCase every time I want to index an instance by its string, or look up an instance by its string, I've instead tried encapsulate this logic in a CaseInsensitiveString class:

/** Used to enable us to easily index objects by string, case insensitive
 * 
 * Note: this class preservse the case of your string!
 */
class CaseInsensitiveString ( val _value : String ) {
  override def hashCode = _value.toLowerCase.hashCode
  override def equals(that : Any) = that match {
    case other : CaseInsensitiveString => other._value.toLowerCase ==_value.toLowerCase
    case other : String => other.toLowerCase == _value.toLowerCase
    case _ => false
  }
  override def toString = _value
}

object CaseInsensitiveString {
  implicit def CaseInsensitiveString2String(l : CaseInsensitiveString) : String = if ( l ==null ) null else l._value
  implicit def StringToCaseInsensitiveString(s : String) : CaseInsensitiveString = new CaseInsensitiveString(s)

  def apply( value : String ) = new CaseInsensitiveString(value)
  def unapply( l : CaseInsensitiveString) = Some(l._value)
}

Can anyone suggest a cleaner or better approach?

One drawback I've come across is when using junit's assertEquals like this:

assertEquals("someString", instance.aCaseInsensitiveString)

It fails, saying it expected "someString" but got CaseInsensitiveString<"someString">.

If I reverse the order of the variables in the assertEquals, then it works, probably because its then calling the equals function on the class CaseInsensitiveString. I currently work around this by keeping the order the same (so the expected one is actually the expected one) but call .toString on the CaseInsensitiveString:

assertEquals("someString", instance.aCaseInsensitiveString.toString)

This works too:

assertEquals(CaseInsensitiveString("someString"), instance.aCaseInsensitiveString)

Is it possible for me to add an implicit equals to String to solve this?

A: 

It seems to me that Java's String.equalsIgnoreCase is what you need to use in order to solve the equality problem. Since JUnit is expecting a String, make sure that you're class is derived from String, that way it will solve the problem. Furthermore, remember the symmetric property of equality, if a == b then b == a, the implication that this has for programing is that if you have two objects, obj1 and obj2, then obj1.equals(obj2) == obj2.equals(obj1)

Make sure you're code meets these constraints.

Michael
Re equalsIgnoreCase, yes that is the type of functionality I want, how do you suggest I use it? Re derived from String, from what I can tell String is final/sealed and does not allow you to extend it.
Alex Black
+2  A: 

In Scala 2.8, you want to define an Ordering[String], and override the compare method to do case-insensitive comparison. Then you can pass that around (or define an implicit val) to any function that needs to do comparison -- all of the standard collections accept an Ordering[T] for their comparisons.

Ken Bloom
Thanks Ken, sounds interesting, I'm still on 2.7.7 for now though.
Alex Black
+5  A: 

Here is a cleaner way of implementing using the "Proxy" and "Ordered" traits:

// http://www.scala-lang.org/docu/files/api/scala/Proxy.html
// http://www.scala-lang.org/docu/files/api/scala/Ordered.html


case class CaseInsensitive(s: String) extends Proxy with Ordered[CaseInsensitive] {
  val self: String = s.toLowerCase
  def compare(other: CaseInsensitive) = self compareTo other.self
  override def toString = s
  def i = this // convenience implicit conversion
}

No help on the ("string" == CaseInsensitive("String")) issue.

You can implicitly convert like so:

  implicit def sensitize(c: CaseInsensitive) = c.s
  implicit def desensitize(s: String) = CaseInsensitive(s)

Which should allow easy comparisons:

  assertEquals("Hello"i, "heLLo"i)
Mitch Blevins
Interesting.. I hadn't heard of Proxy, that looks powerful, I will look into this, thx.
Alex Black
How is Proxy implemented, does it use Reflection? Is it still worth using?
Alex Black
No magic to Proxy class. See how simple the source is: http://scala-tools.org/scaladocs/scala-library/2.7.1/Proxy.scala.html
Mitch Blevins
ah, ok, so Proxy forwards hashCode, equals and toString to the inner class. For some reason I thought it did more than that.
Alex Black
Note that the "toString" method works on the lowercase version, which doesn't match your implementation above. You would need to override this.
Mitch Blevins
Thx, noticed that when it failed my test :) This method of implementing also doesn't support equals against String, which the other one does.. but given it only works one way (e.g. CaseInsensitiveString == String, and not String == CaseInsensitiveString) its probably not a good idea.
Alex Black
Can you tell me a bit about Ordered[String]? 1. When does it get used? I assume I should probably add it to my existing class if I don't switch to your suggestion. 2. Why is it Ordered[String] and not Ordered[CaseInsensitive]? The compare(other: String) = self compareTo Other" doesn't seem to sit quite right to me..
Alex Black
Ordered[T] is probably irrelevant to your situation and should be ignored. But, you are right that I should have shown as Ordered[CaseInsensitive]. It is only convenience trait for comparison operators.
Mitch Blevins
Edited answer to show Ordered[CaseInsensitive] and add an idea for a convenience converter
Mitch Blevins
Ordered[CaseInsensitive]: cool makes more sense now. I like the implicit conversion...
Alex Black
I tried the "def i = this", perhaps I missed something, but "Hello"i doesn't compile, says: value i is not a member of java.lang.String
Alex Black
Can be sensitive to the surrounding context for syntax. Try surrounding in parenthesis ("Hello"i) or being more explicit about the method call "Hello".i
Mitch Blevins
hmm, not having any luck. got any links to examples of this type of thing?
Alex Black
Here's a link to a complete, compilable example that outputs: "yay".http://gist.github.com/236656The idea was stolen from the RichString class using "blah".r to turn into a Regex. I'd also seen call-site usage as "myRegex"r
Mitch Blevins
Oh, I'm also using scala 2.8, but I wouldn't think that would make a difference. It is just a plain-jane implicit coercion.
Mitch Blevins
From the error msg: "value i is not a member of java.lang.String", it sounds like the implicit conversion def is not in scope.
Mitch Blevins
Got it working, thanks Mitch. I needed to add an import for the class name, e.g. import foo.bar.CaseInsensitive._
Alex Black