Baeldung Pro – Scala – NPI EA (cat = Baeldung on Scala)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

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 for dates and times was limited before Java 8. Possibly, the best option for developers was using 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.

Let’s look at how we can parse a string in ISO date format to LocalDate using java.time package:

val dateStr = "2024-09-19"
LocalDate.parse(dateStr) shouldBe LocalDate.of(2024, 9, 19)

We can use the parse() method on LocalDate to parse the string to the LocalDate instance. If the date string is in a different format, we can provide the format during parsing:

val dateStr = "19.09.2024"
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
LocalDate.parse(dateStr, formatter) shouldBe LocalDate.of(2024, 9, 19)

Furthermore, if the string contains a time component, we can parse it into a LocalDateTime instance:

val dateStr = "19.09.2024 10:20:30"
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss")
LocalDateTime.parse(dateStr, formatter) shouldBe LocalDateTime.of(2024, 9, 19, 10, 20, 30)

More features of the Java standard Date/Time library are available in the official documentation.

Additionally, we can use the nscala-time library, which is a wrapper over java-time and joda-time APIs to make the date handling easier.

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.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.