1. 序章

日付を含む文字列の処理は、実際のアプリケーションコードを作成する際の一般的な問題です。 このチュートリアルでは、Scalaプログラムからその問題を処理するための、単純な構文解析、正規表現、および標準ライブラリを使用する3つの方法を紹介します。

2. 単純な構文解析

日付形式が事前にわかっている場合は、文字列を分割してから、コンポーネントを解析できます。

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
    }
  }
  // ...
}

そして、それをテストすることができます:

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)
    }
  }
  // ...
}

このアプローチには、留意する必要のあるいくつかの問題があります。

  • フォーマットが修正され、事前にわかっている場合にのみ機能します。間違いなく、これに依存するべきではありません。
  • 日付情報は数値だけでなくテキストであることが多いため( 17 / Feb / 2022 のように)、解析にはルックアップテーブルが必要になる場合があります。

3. 正規表現

文字列の形式がもう少し複雑な場合は、正規表現を使用した上記のバリエーションでうまくいく可能性があります。

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
            )
          )
      )
  }
}

それをテストしてみましょう:

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. 標準ライブラリ

日付形式が完全に管理されていない場合があります。 タイムスタンプまたはゾーン情報、さまざまな時間形式、ロケール固有の規則などが含まれる場合があります。

幸いなことに、状況を改善するためにできることが2つあります。

  • 時間には標準の表現を使用します。 たとえそれが複雑であっても、国際委員会は問題をよく理解しています。 この基準を採用して施行することで、私たちは自分たちに有利に働きます。 この点で頼りになるリファレンスは、当然、 ISO8601です。
  • ライブラリを受け入れる。 私たちのために重労働をするためにいくつかあります。 JavaとScalaの相互運用性を考えると、これ以上調べる必要はありません。Javaで利用できる優れたライブラリを使用できます。

4.1. Javaライブラリ

日付と時刻の言語サポートは、Java8より前では制限されていました。 おそらく、開発者にとって最良の選択肢は、素晴らしいJodaTimeライブラリを利用することでした。

幸い、バージョン8では、Java言語の標準ライブラリが最初に提案されたJodaTimeライブラリの概念を取り入れました。 それらはパッケージjava.timeの下に表示されます。

Java標準のDate/Timeライブラリを使用するときに最初に行うことは、ドキュメントでユースケースを特定することです。これは広範囲にわたっています。

4.2. ローカル対。 ゾーン化された時間

例として、24時間形式の日付と時刻があると仮定します。 午後8時30分に高級レストランでバレンタインデーを祝う予約をしたいと思います。 これには、フランスのパリのタイムゾーン情報が含まれます: 2022-02-14T20:30:00Z[ヨーロッパ/パリ]

文字列は特定の場所(パリ)の日付と時刻を表すため、ゾーン化された日付/時刻になります。 したがって、クラスZonedDateTimeを探しています。

一方、タイムゾーン、または異なるゾーンの時刻の違いに関心がない場合は、ローカルの日付/時刻(クラス LocalDateTime を使用して、壁掛け時計の日付/時刻に制限できます。 ])。

この例では、コードを書く必要さえありません。 ライブラリの使用方法を説明しましょう。

{
  "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) == 20)
      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. 結論

日付の解析は複雑な問題ですが、標準を採用し、日付と時刻に標準のJavaライブラリを使用することで、はるかに管理しやすくなります。

いつものように、パーサーとそのテストの完全なコードは、GitHubから入手できます。