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

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

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

f:id:ldi-contributor:20210916070736p:plain


開発者Gです。

前回はSimpleDateFormatに関する諸問題を回避する方法として Apache Commons、Date and Time API、Joda-Time などについて触れ、それぞれの特徴や考慮すべき点について確認しました。

いよいよ今回は日付に関するオレオレライブラリを具体化していきます。


なお、ソースコードは全部、または一部を抜粋して表示します。

コンセプト

作成するライブラリの方向性というか、解決したい課題を列挙してみました。
これに沿ってライブラリに実装する機能を考えていきます。



f:id:ldi-contributor:20210912171211p:plain

  • Date(java.uti.Date)のまま扱いが簡単になれば、それはそれで便利
  • Date and Time APIのパワフルな機能を利用する必要がある場合は、DateをLocalDateやLocalDateTimeに簡単に変換できると便利
  • 互換性が必要な状況ではLocalDateやLocalDateTimeからDateへの変換が簡単にできると便利

DateとCalendar

Dateの使い勝手を向上させる上で、まずは何が問題なのかを確認しましょう。

Date(java.util.Date)がとても不便な理由は、Dateのインスタンスは単に1970/01/01 00:00:00(GMT)からの経過ミリ秒を整数で表現しているだけであり、Dateの年月日や時分秒にアクセスするにはCalendar(java.util.Calendar)を、文字列をパースしてDateを取得するにはSimpleDateFormat等を使う必要があるというところです。
※ちなみに、C#ではDateTimeがJavaのDateに対応しますが、DateTimeはJavaのDate, Calendar, SimpleDateFormatを統合したような機能を提供しているのでとても使いやすいです。

Dateは値は持っているけど、これを操作するプロパティやメソッドはほとんど何も用意されておらず、他のクラスを使えという分離された設計になっています。

厳密に言うと、yearとか、monthとか、dayとかの名前のプロパティがあることはあるのですが、あまりにも実用性がない設計になっていたためか、現在はDepricated(非推奨)とされており、エディタ上では取り消し線が表示された状態になります。

f:id:ldi-contributor:20210911191257p:plain
Depricated(非推奨)のプロパティ

非推奨でも使用することはできますが、返ってくる値は一般人が期待するものではありません。

"2021/09/01 12:34:56.789"からDateのインスタンスを生成し、これらのプロパティが返す値を確認してみます。

f:id:ldi-contributor:20210911192704p:plain

実行結果
date.year=121
date.month=8
date.day=3
date.hours=12
date.minutes=34
date.seconds=56

"2021/09/01 12:34:56.789"をパースした結果は・・
121年8月3日 12時34分56秒・・・?

はい?何が表示されたんでしょうかコレ。。

yearが2021ではなく、121ですね。このプロパティは1900年からの差分が返ってくるそうです。2021-1970=121なので、実際そうなってますね。これは1998年とかだと、1998-1900=98が返ってくるので、西暦の下二桁を取得することを想定して設計されたんだろうなと想像しますが、2000年以降だと何の役にも立たないですね。Javaに2000年問題(Y2K問題)の名残を見ることができたことに、ある種の感動を覚えます。

monthはこれまた9ではなく、8になっていますね。monthは0から11の値を返し、0がJanuary、11がDecemberなんだそうです。コンピューターの世界では0から数えるのか、1から数えるのかを注意しなければならないことが多々ありますが、Javaの基盤を設計した人はなんで0から数えちゃったんですかね。

dayは1ではなく、3ですね。。。これはday of weekということで、週のインデックスが3の日(水曜日)だということですね。0がSunday、1がMonday、3がWednesdayです。一般人には用がないですね。どちらかというと欲しいのはday of monthのほうですが、それを取得するメソッドは無いようです。

f:id:ldi-contributor:20210911133121p:plain
Dateクラスのメソッド一覧(多くが非推奨となっている)


Dateの既存のプロパティを利用することはあきらめ、素直にCalendarを使いましょう。

Calendarを使った年月日時分秒の取得

    @Test
    fun calendar() {

        val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
        val calendar = Calendar.getInstance()
        calendar.time = date

        val year = calendar.get(Calendar.YEAR)
        val month = calendar.get(Calendar.MONTH)
        val day = calendar.get(Calendar.DAY_OF_MONTH)
        val hour = calendar.get(Calendar.HOUR_OF_DAY)
        val minute = calendar.get(Calendar.MINUTE)
        val second = calendar.get(Calendar.SECOND)
        val millisecond = calendar.get(Calendar.MILLISECOND)

        println("year=$year")
        println("month=$month")
        println("day=$day")
        println("hour=$hour")
        println("second=$second")
        println("minute=$minute")
        println("milliseond=$millisecond")
    }
実行結果
year=2021
month=8
day=1
hour=12
second=56
minute=34
milliseond=789


日付や時刻に関する情報がミリ秒単位まで取得できます。

ただ、monthに関しては9ではなく8が返ってきますので、利用時に注意が必要です(この仕様はバグを誘発する原因になっていると思います)。


しかし、カレンダーの初期化で2行必要とし、year等の値を取得するのにインデックスを指定しなければならず、使い勝手がよくありません。
やはり、calendar.year で取得したいところです。

Dateの操作を簡単にする拡張プロパティを作る

Kotlinをはじめとする最近のプログラミング言語では、拡張プロパティや拡張関数(拡張メソッド)を使用することができます。

これを使用すると既存のクラスを直接変更することなく、後付けでプロパティやメソッド(関数)を追加して拡張することができます(実際にはあたかも元のクラスにプロパティやメソッドを追加したような感覚で使えるだけで、追加はしていない)。

同名のプロパティやメソッドが競合しないように注意する必要がありますが、とても便利な機能です。

拡張プロパティを使用して、Dateから直接Calendarの機能を利用できるようにしてみました。

Dateへの拡張プロパティの追加

package extensions.date.property

import java.util.*

/**
 * DateからCalendarを取得します。
 */
val Date.calendar: Calendar
    get() {
        val c = Calendar.getInstance()
        c.time = this
        return c
    }

/**
 * Dateの年の値を取得します。
 */
val Date.yearValue: Int
    get() {
        return this.calendar.get(Calendar.YEAR)
    }

/**
 * Dateの月の値を取得します。
 */
val Date.monthValue: Int
    get() {
        return this.calendar.get(Calendar.MONTH) + 1
    }

/**
 * Dateの日の値を取得します。
 */
val Date.dayValue: Int
    get() {
        return this.calendar.get(Calendar.DAY_OF_MONTH)
    }

/**
 * Dateの時の値を取得します。
 */
val Date.hourValue: Int
    get() {
        return this.calendar.get(Calendar.HOUR_OF_DAY)
    }

/**
 * Dateの分の値を取得します。
 */
val Date.minuteValue: Int
    get() {
        return this.calendar.get(Calendar.MINUTE)
    }

/**
 * Dateの秒の値を取得します。
 */
val Date.secondValue: Int
    get() {
        return this.calendar.get(Calendar.SECOND)
    }

/**
 * Dateのミリ秒の値を取得します。
 */
val Date.millisecondValue: Int
    get() {
        return this.calendar.get(Calendar.MILLISECOND)
    }
使用例

作ったユーティリティを使ってみましょう。

package extensions.date.property

import org.apache.commons.lang3.time.DateUtils
import org.junit.Test

class DatePropertyExtensionTest {

    @Test
    fun properties() {

        val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")

        println("date.yearValue=${date.yearValue}")
        println("date.monthValue=${date.monthValue}")
        println("date.dayValue=${date.dayValue}")
        println("date.hourValue=${date.hourValue}")
        println("date.minuteValue=${date.minuteValue}")
        println("date.secondValue=${date.secondValue}")
        println("date.milliseoncdValue=${date.millisecondValue}")
    }

}
実行結果
date.yearValue=2021
date.monthValue=9
date.dayValue=1
date.hourValue=12
date.minuteValue=34
date.secondValue=56
date.milliseoncdValue=789


Dateのインスタンスから直接、年月日時分秒ミリ秒を取得できるようになり、コードが極めてコンパクトになりました。

なお、monthValueは補正しているので、8ではなく9が取得できます。


次回予告

文字列をパースしてDateを取得する機能を拡張関数で追加する(仮)

乞うご期待。