1. Introduction

Dealing with a string that contains a date is a common problem when writing real-life application code. In this tutorial, we showcase three ways to handle that problem from a Scala program: using simple parsing, regular expressions, and standard libraries.

2. Simple Parsing

If the date format is known up-front, we can split the string, then parse the components:

class DateParser {
  // ...
  def simpleParse(dateString: String): Option[GregorianCalendar] = {
    dateString.split("/").toList match {
      case yyyy :: mm :: dd :: Nil =>
        Try(
          new GregorianCalendar(
            yyyy.toInt,
            mm.toInt - 1,
            dd.toInt - 1
          )
        ).toOption
      case _ => None
    }
  }
  // ...
}

And we can test it:

class DateParserSpec extends AnyWordSpec with Matchers {
  val parser = new DateParser
  "a simple parser" should {
    "retrieve elements of a string when it matches the predefined format" in {
      val maybeDate = parser.simpleParse("2022/02/14")
      // the format is "yyyy/mm/dd"
      assert(maybeDate.isDefined)
      assert(maybeDate.get.get(Calendar.YEAR) == 2022)
      // in the case class, elements are 0-based:
      assert(maybeDate.get.get(Calendar.MONTH) == 2 - 1)
      assert(maybeDate.get.get(Calendar.DAY_OF_MONTH) == 14 - 1)
    }
    "fail to retrieve elements if date includes unexpected time" in {
      val maybeDateTime = parser.simpleParse("2022/02/14T20:30:00")
      assert(maybeDateTime.isEmpty)
    }
  }
  // ...
}

This approach brings several problems that we need to keep in mind:

  • It only works if the format is fixed and known in advance. Arguably, we should never rely upon that.
  • Parsing might need lookup tables because date information is frequently textual, not only numeric (as in 17/Feb/2022).

3. Regular Expressions

If the string format is a bit more complex, a variation of the above using regular expressions can do the trick:

class DateParser {
  // ...
  def regexParse(regex: Regex, dateString: String): Option[GregorianCalendar] = {
    val groupsIteratorOption = Try(
      regex.findAllIn(dateString).matchData
    ).toOption
    groupsIteratorOption
      .map(_.next())
      .flatMap(iterator =>
        if (iterator.groupCount < 3) None
        else
          Some(
            new GregorianCalendar(
              iterator.group(1).toInt,
              iterator.group(2).toInt - 1,
              iterator.group(3).toInt - 1
            )
          )
      )
  }
}

Let’s test it:

class DateParserSpec extends AnyWordSpec with Matchers {
  val parser = new DateParser
  // ...
  "a regular expression parser" should {
    // Note that this is a very naive regular expression,
    // just to show a point. Don't use it for real.
    val naiveDateRegExp = "^([0-9]{4})/([0-1]?[0-9])/([1-3]?[0-9]).*".r
    "retrieve date elements when it matches the regular expression" in {
      val maybeDate = parser.regexParse(naiveDateRegExp, "2022/02/14")
      assert(maybeDate.isDefined)
      assert(maybeDate.get.get(Calendar.YEAR) == 2022)
      // in the case class, elements are 0-based:
      assert(maybeDate.get.get(Calendar.MONTH) == 2 - 1)
      assert(maybeDate.get.get(Calendar.DAY_OF_MONTH) == 14 - 1)
    }
    "retrieve date elements even if it includes unexpected elements (time)" in {
      val maybeDate = parser.regexParse(naiveDateRegExp, "2022/02/14T20:30:00")
      assert(maybeDate.isDefined)
      assert(maybeDate.get.get(Calendar.YEAR) == 2022)
      // in the case class, elements are 0-based:
      assert(maybeDate.get.get(Calendar.MONTH) == 2 - 1)
      assert(maybeDate.get.get(Calendar.DAY_OF_MONTH) == 14 - 1)
    }
  }
// ...
}

4. Standard Libraries

Sometimes, the date format isn’t completely under our control. It might contain timestamp or zone information, various time formats and locale-specific conventions, and so forth.

Luckily for us, there are two things we can do to improve the situation:

  • Use standard representations for time. International committees understand the problem well, even if it’s complex. We’d be doing ourselves a favor by adopting and enforcing the standard. The go-to reference in this regard is, naturally, ISO 8601.
  • Embrace a library. There are several to do the heavy lifting for us. Given the interoperability between Java and Scala, we don’t have to look any further: we can use the great libraries available for Java.

4.1. Java Libraries

The language support of date and time was limited before Java 8. Possibly, the best option for developers was availing themselves of the fantastic Joda Time library.

Fortunately, with version 8, the Java language standard libraries assimilated the Joda Time library’s concepts first proposed. They appear under the package java.time.

The first thing to do when using the Java standard Date/Time library is to identify our use case in the documentation, which is extensive.

4.2. Local vs. Zoned Time

For the sake of example, let’s assume that we have a date, with time, in 24-hour format. We want to book a reservation to celebrate Valentine’s Day in a fancy restaurant at 8:30 pm. That includes time zone information for Paris, France: 2022-02-14T20:30:00Z[Europe/Paris].

Since the string represents a date and time for a specific place (Paris), it’s a zoned date/time. Therefore, we’re looking for the class ZonedDateTime.

On the other hand, if we weren’t interested in time zones, or differences between the time in different zones, we could limit ourselves to a wall clock date/time by using the local date/time (class LocalDateTime).

For this example, we don’t even need to write code. Let’s illustrate how to use the library:

{
  "a library-based parser" should {
    "retrieve date elements when a complex date/time string is passed" in {
      val attemptedParse = Try(ZonedDateTime.parse("2022-02-14T20:30:00.00Z[Europe/Paris]"))
      assert(attemptedParse.isSuccess)
      val zdt = attemptedParse.get
      assert(zdt.get(ChronoField.YEAR) == 2022)
      assert(zdt.get(ChronoField.MONTH_OF_YEAR) == 2)
      assert(zdt.get(ChronoField.DAY_OF_MONTH) == 14)
      assert(zdt.get(ChronoField.HOUR_OF_DAY) == 21)
      assert(zdt.get(ChronoField.MINUTE_OF_HOUR) == 30)
      assert(zdt.getZone == ZoneId.of("Europe/Paris"))
    }
    "fail to retrieve date elements when an invalid date/time is passed" in {
      val attemptedParse =
        Try(ZonedDateTime.parse("2022-02-14"))
      assert(attemptedParse.isFailure)
      assert(
        attemptedParse.failed.get.getMessage.contains("could not be parsed")
      )
    }
  }
}

5. Conclusion

Although parsing dates is a complex problem, it becomes much more manageable by adopting standards and using the standard Java library for date and time.

As usual, the full code for the parser and its tests are available over on GitHub.