ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

KotlinでDateの操作を簡単にするライブラリをつくってみる(その2)


開発者Gです。

前回は日付の操作を簡単にするライブラリを作成する動機について書きました。
今回はJavaで昔から使用されているSimpleDateFormatクラスについて、何が問題かを確認します。

なお、ソースコードはJUnitのテストメソッドの部分のみを抜粋して表示します。完全なソースコードは解説の後に掲載します。

シンプルそうでシンプルではないSimpleDateFormatの罠

KotlinあるいはJavaで文字列を解析してDateを取得する方法を検索すると、上位にSimpleDateFormatの使用方法を紹介する記事が出てきます。

SimpleDateFormatの使用例

ソースコード
    @Test
    fun `SimpleDateFormatで文字列をパースする`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        val date = sdf.parse("2021/09/01")
        println(date)
        println(sdf.format(date))
    }
実行結果
Wed Sep 01 00:00:00 JST 2021
2021/09/01


なるほど、シンプルと思うかもしれませんが、使っているといろいろと問題があることに気付きます。



SimpleDateFormatで存在しない日付をパースできてしまう例

ソースコード
    @Test
    fun `SimpleDateFormatで存在しない日付の文字列がパースできてしまう(デフォルト)`() {

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        val date = sdf.parse("2021/08/32")
        println(date)
        println(sdf.format(date))
    }
実行結果
Wed Sep 01 00:00:00 JST 2021
2021/09/01


"2021/08/32"を2021/09/01と解釈してしまいます。SimpleDateFormatのデフォルトの動作ですが、不要な仕様ですね。この挙動を禁止したい場合は次のようにisLenientプロパティをfalseに設定します。



SimpleDateFormatで存在しない日付の文字列をパースする(例外を発生させる)

ソースコード
    @Test
    fun `SimpleDateFormatで存在しない日付の文字列をパースする(例外を発生させる)`() {

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false   // あいまいな解釈をするオプションを無効化する
        sdf.parse("2021/08/32")  // 例外発生
    }
実行結果
Unparseable date: "2021/08/32"
java.text.ParseException: Unparseable date: "2021/08/32"

上記のようにisLenientプロパティをfalseに設定することで、存在しない日付を他の有効な日付に解釈してしまうという挙動は抑制することができます。


SimpleDateFormatで指定した書式ではない文字列(字足らず)がパースできてしまう

ソースコード
    @Test
    fun `SimpleDateFormatで指定した書式ではない文字列(字足らず)がパースできてしまう`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false

        val original = "2021/09/1"
        val date = sdf.parse(original)  // yyyy/MM/ddの書式ではないのにパースしてしまう(例外が発生しない)
        val reformat = sdf.format(date) // 再度yyyy/MM/ddにフォーマットする
        println("変換前文字列:$original")
        println("変換後文字列:$reformat")
    }
実行結果
変換前文字列:2021/09/1
変換後文字列:2021/09/01


この挙動は問題になる場合とならない場合がありますが、問題にならない場合はスルーでよいです。問題になる場合は変換前と変換後の文字列を比較し、必要ならばエラー処理をする必要があります。



SimpleDateFormatで指定した書式ではない文字列(字余り)がパースできてしまう

ソースコード
    @Test
    fun `SimpleDateFormatで指定した書式ではない文字列(字余り)がパースできてしまう`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false

        val original = "2021/09/01A"
        val date = sdf.parse(original)   // yyyy/MM/ddの書式ではないのにパースしてしまう(例外が発生しない)
        val reformat = sdf.format(date) // 再度yyyy/MM/ddにフォーマットする
        println("変換前文字列:$original")
        println("変換後文字列:$reformat")
    }
実行結果
変換前文字列:2021/09/01A
変換後文字列:2021/09/01

この挙動は問題になる場合とならない場合がありますが、問題にならない場合はスルーでよいです。問題になる場合は変換前と変換後の文字列を比較し、必要ならばエラー処理をする必要があります。

その他の問題

  • スレッドセーフで設計されていないため、マルチスレッド環境でSimpleDateFormatのインスタンスを再利用するとバグの原因になります。
  • 上記の対策として毎回インスタンスを生成し、isLenientをfalseに設定するのは面倒です。ルール化して運用を徹底しても、新参者を含めてプロジェクトメンバーの全員が常にルールを守るのは困難です。

サンプルコード全体

サンプルコード全体はこちらになります。

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.text.SimpleDateFormat

class SimpleDateFormat1 {

    @Test
    fun `SimpleDateFormatで文字列をパースする`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        val date = sdf.parse("2021/09/01")
        println(date)
        println(sdf.format(date))
    }

    @Test
    fun `SimpleDateFormatで存在しない日付の文字列がパースできてしまう(デフォルト)`() {

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        val date = sdf.parse("2021/08/32")
        println(date)
        println(sdf.format(date))
    }

    @Test
    fun `SimpleDateFormatで存在しない日付の文字列をパースする(例外を発生させる)`() {

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false   // あいまいな解釈をするオプションを無効化する
        sdf.parse("2021/08/32")  // 例外発生
    }

    @Test
    fun `SimpleDateFormatで指定した書式ではない文字列(字足らず)がパースできてしまう`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false

        val original = "2021/09/1"
        val date = sdf.parse(original)  // yyyy/MM/ddの書式ではないのにパースしてしまう(例外が発生しない)
        val reformat = sdf.format(date) // 再度yyyy/MM/ddにフォーマットする
        println("変換前文字列:$original")
        println("変換後文字列:$reformat")
    }

    @Test
    fun `SimpleDateFormatで指定した書式ではない文字列(字余り)がパースできてしまう`(){

        val sdf = SimpleDateFormat("yyyy/MM/dd")
        sdf.isLenient = false

        val original = "2021/09/01A"
        val date = sdf.parse(original)   // yyyy/MM/ddの書式ではないのにパースしてしまう(例外が発生しない)
        val reformat = sdf.format(date) // 再度yyyy/MM/ddにフォーマットする
        println("変換前文字列:$original")
        println("変換後文字列:$reformat")
    }
}

まとめ

  • KotlinあるいはJavaで日付のフォーマットや文字列からの日付のパースに最も使用されているであろうSimpleDateFormatについて、サンプルコードを使用してその使い方と問題点を紹介しました。
  • SimpleDateFormatはその名前に反して使い勝手がよくないだけでなく、落とし穴が複数存在します。
  • これから作成するオレオレライブラリではこれらの問題を克服し、使い勝手のよいものを目指します。

次回予告

SimpleDateFormat以外のユーティリティはどうなの?(仮)

乞うご期待。