case-insensitive - Case Insensitive structures for Scala
case-insensitive
provides a case-insensitive string type for Scala.
Design goals are:
- light weight
- locale independence
- stability
- integration with Cats
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"
CIString
s 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!