5

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?

waterlooalex
  • 13,642
  • 16
  • 78
  • 99
  • Have you thought of making a trait that will work, so it can handle the logic of fetching the string from the hashmap? – James Black Nov 17 '09 at 00:51
  • I hadn't thought of this, but unfortunately I don't really see yet what you're suggesting. Can you explain in a bit more detail James? Would I mix this trait into the HashMap at instantation time? – waterlooalex Nov 17 '09 at 00:53

5 Answers5

7

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
  • 13,186
  • 3
  • 44
  • 32
  • Interesting.. I hadn't heard of Proxy, that looks powerful, I will look into this, thx. – waterlooalex Nov 17 '09 at 01:45
  • How is Proxy implemented, does it use Reflection? Is it still worth using? – waterlooalex Nov 17 '09 at 01:48
  • 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 Nov 17 '09 at 02:06
  • ah, ok, so Proxy forwards hashCode, equals and toString to the inner class. For some reason I thought it did more than that. – waterlooalex Nov 17 '09 at 02:10
  • 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 Nov 17 '09 at 02:14
  • 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. – waterlooalex Nov 17 '09 at 02:19
  • 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.. – waterlooalex Nov 17 '09 at 02:22
  • 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 Nov 17 '09 at 03:13
  • Edited answer to show Ordered[CaseInsensitive] and add an idea for a convenience converter – Mitch Blevins Nov 17 '09 at 03:16
  • Ordered[CaseInsensitive]: cool makes more sense now. I like the implicit conversion... – waterlooalex Nov 17 '09 at 04:00
  • 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 – waterlooalex Nov 17 '09 at 04:15
  • 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 Nov 17 '09 at 04:24
  • hmm, not having any luck. got any links to examples of this type of thing? – waterlooalex Nov 17 '09 at 04:42
  • Here's a link to a complete, compilable example that outputs: "yay". http://gist.github.com/236656 The 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 Nov 17 '09 at 04:57
  • 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 Nov 17 '09 at 05:01
  • 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 Nov 17 '09 at 05:15
  • Got it working, thanks Mitch. I needed to add an import for the class name, e.g. import foo.bar.CaseInsensitive._ – waterlooalex Nov 17 '09 at 14:55
3

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
  • 57,498
  • 14
  • 111
  • 168
1

I ran into this issue today. This is how I chose to solve it:

First, I declared an object of the Ordering type to do the sorting:

import scala.math.Ordering.StringOrdering
object CaseInsensitiveStringOrdering extends StringOrdering {
  override def compare(x: String, y: String) = {
    String.CASE_INSENSITIVE_ORDER.compare(x,y)
  }
}

Next when I created my TreeMap I used this object as follows:

val newMap = new TreeMap[String,AnyRef]()(CaseInsensitiveStringOrdering)

This was with Scala 2.11.8 BTW.

Mike C
  • 31
  • 3
0

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
  • 1,626
  • 11
  • 23
  • 1
    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. – waterlooalex Nov 17 '09 at 01:43
0

Here is an example of using Ordering (since 2.8)

val s = List( "a", "d", "F", "B", "e")

res0: List[String] = List(B, F, a, d, e)

object CaseInsensitiveOrdering extends scala.math.Ordering[String] {
    def compare(a:String, b:String) = a.toLowerCase compare b.toLowerCase
}

defined object CaseInsensitiveOrdering

val orderField = CaseInsensitiveOrdering

orderField: CaseInsensitiveOrdering.type = CaseInsensitiveOrdering$@589643bb

s.sorted(orderField)

res1: List[String] = List(a, B, d, e, F)

Ariel
  • 327
  • 2
  • 6
  • Avoid `toLowerCase` as this fails "the Turkey test" (Google it and see what happens to the letter `'i'`). Probably use `String.CASE_INSENSITIVE_ORDER.compare` instead, which would also avoid the expensive string copying – Luke Usherwood Dec 27 '15 at 21:31
  • Oops, `String.CASE_INSENSITIVE_ORDER` also fails the Turkey Test :o) Then maybe `Collator.getInstance()` for a correct comparison in the user's locale. – Luke Usherwood Dec 27 '15 at 21:52