Understanding Scala Extractors: An Easy-to-Follow Example

Let's implement a simple case class that represents a Time object that contains hours and minutes. For simplicity, we will use 24-hour clock.

case class Time(hour: Hour, minutes: Minute)
case class Hour(hour: Int)
case class Minute(minute: Int)

Now let's try to implement a code that parses a string into Time using custom extractor

object Time {
  def unapply(stringTime: String): Option[Time] = {
    stringTime.split(":", 2) match {
      case Array(h, m) =>
        Some(Time(h, m))
      case _ => None
    }
  }
}

The problem with this code is that hours and minutes are not validated, therefore strings like "30:-1" will still be considered as valid time.

To solve this problem we can embed conditions into time extractor directly or better define separate Hour and Minute extractors with their own validation rules.

object Hour {
  def unapply(stringHour: String): Option[Hour] = {
    stringHour.toIntOption match {
      case Some(h) if h >= 0 && h <= 23 => Some(Hour(h))
      case _ => None
    }
  }
}

object Minute {
  def unapply(stringMinute: String): Option[Minute] = {
    stringMinute.toIntOption match {
      case Some(m) if m >= 0 && m <= 59 => Some(Minute(m))
      case _ => None
    }
  }
}

These extractors allow to extract minutes and hours from string only if they are valid. Now adjust the Time extractor to use extractors for hours and minutes

object Hour {
  def unapply(stringHour: String): Option[Hour] = {
    stringHour.toIntOption match {
      case Some(h) if h >= 0 && h <= 23 => Some(Hour(h))
      case _ => None
    }
  }
}

object Minute {
  def unapply(stringMinute: String): Option[Minute] = {
    stringMinute.toIntOption match {
      case Some(m) if m >= 0 && m <= 59 => Some(Minute(m))
      case _ => None
    }
  }
}
object Time {
  def unapply(stringTime: String): Option[Time] = {
    stringTime.split(":",2) match {
      case Array(Hour(h), Minute(m)) =>
        Some(Time(h, m))
      case _ => None
    }
  }
}

Let's test the code on a few unit tests

object TimeExtractors extends App {

  List("12:30", "-1:23", "a:b", "4:59", "1:2:3", "12:77").foreach {
    case strTime @ Time(t) => println(s"${strTime} converted to ${t}")
    case strTime => println(s"unknown time ${strTime}")
  }

}

Output from the code above

12:30 converted to Time(Hour(12),Minute(30)) 
unknown time -1:23 
unknown time a:b 
4:59 converted to Time(Hour(4),Minute(59)) 
unknown time 1:2:3 
unknown time 12:77
💡
We can chain any number of extractors as in case Some(Array(Animal(name, Some(breed)))) => ... to make code more readable
💡
Symbol @ in a case match is called "Pattern Binder". It allows to store raw value in a local variable, this is very useful because it is not always possible/cheap to construct the object back

This article demonstrates how to implement a simple Time case class with validation for hours and minutes using extractors in Scala and how to combine multiple extractors to increase readability

Did you find this article valuable?

Support Maksim Martianov's engineering blog by becoming a sponsor. Any amount is appreciated!