文字列をScalaで日付に変換する
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でから入手できます。