case-insensitive - Case Insensitive structures for Scala

Actions StatusMaven Central Code of Conduct

case-insensitive provides a case-insensitive string type for Scala. Design goals are:

Case-insensitive strings are useful as map keys for case-insensitive lookups. They are also useful in case classes whose equality semantics are case insensitive for certain string fields.

Quick Start

To use case-insensitive in an existing SBT project with Scala 2.12 or a later version, add the following dependencies to your build.sbt depending on your needs:

libraryDependencies ++= Seq(
  "org.typelevel" %% "case-insensitive" % "1.4.2"
)

Basic Usage

This library provides a CIString type.

import org.typelevel.ci._

Construct case-insensitive strings with the apply method:

val hello = CIString("Hello")
// hello: CIString = Hello

More concisely, use the ci interpolator:

val name = "Otis"
// name: String = "Otis"
val greeting = ci"Hello, ${name}"
// greeting: CIString = Hello, Otis

Get the original string value with toString:

val original = hello.toString
// original: String = "Hello"

CIStrings are equal according to the rules of equalsIgnoreCase:

assert(CIString("hello") == CIString("Hello"))

This means that strings that change length when uppercased are not equal when wrapped in CIString:

assert("ß".toUpperCase == "SS")
assert(CIString("ß") != CIString("SS"))

It also means that comparisons are independent of the runtime's default locales:

import java.util.Locale

Locale.setDefault(Locale.ROOT)
assert("i".toUpperCase == "I")
assert(CIString("i") == CIString("I"))

Locale.setDefault(Locale.forLanguageTag("tr"))
assert("i".toUpperCase != "I")
assert(CIString("i") == CIString("I"))

We also implement Ordering, based on the rules of compareToIgnoreCase:

assert("a" > "B")
assert(CIString("a") < CIString("B"))

You can also match strings with the ci globbing matcher. It works like s:

val ci"HELLO, ${appellation}" = ci"Hello, Alice": @unchecked
// appellation: CIString = Alice

Cats integration

We provide instances of various Cats type classes. The most exciting of these are Eq and Monoid:

import cats.implicits._

assert(CIString("Hello") === CIString("Hello"))
val combined = CIString("case") |+| CIString("-") |+| CIString("insensitive")

Testing package

The case-insensitive-testing module provides instances of Scalacheck's Arbitrary and Cogen for property testing models that include CIString:

Add the following dependency:

libraryDependencies ++= Seq(
  "org.typelevel" %% "case-insensitive-testing" % "<version>"
)

Import the arbitraries:

import org.typelevel.ci.testing.arbitraries._
import org.scalacheck.Prop

And use them in your property tests:

Prop.forAll { (x: CIString) => x == x }.check()
// + OK, passed 100 tests.

FAQ

Why pay for a wrapper when there is equalsIgnoreCase?

This type integrates cleanly with various data structures that depend on universal equality and hashing, such as scala.Map and case classes. These data structures otherwise require specialized implementations for case-insensitivity.

Why pay for a wrapper when you can fold case before storing?

Sometimes it's useful to preserve the original value.

Also, the type of String doesn't change when folding case, making this an error-prone solution.

Why not polymorphic?

Haskell's Data.CaseInsensitive provides a polymorphic CI type similar to:

class CI[A](val original: A) {
  override def equals(that: Any) = ???
}

The only functions available to us on A are those that apply to Any, which precludes us from using optimized equality operations such as equalsIgnoreCase.
The best we can do is (lazily) store a case-folded version of A on construction and use a slower equals check on that.

Haskell has myriad string-like types, which provide a rich set of instances. In Scala, restricting ourselves to immutable types, it's hard to imagine any other than CI[String] and CI[Char].

Okay, then why is there no CIChar here?

I don't need it yet. If you do, pull requests welcome!