開発者Gです。
前回はSimpleDateFormatに関する諸問題を回避する方法として Apache Commons、Date and Time API、Joda-Time などについて触れ、それぞれの特徴や考慮すべき点について確認しました。
いよいよ今回は日付に関するオレオレライブラリを具体化していきます。
なお、ソースコードは全部、または一部を抜粋して表示します。
コンセプト
作成するライブラリの方向性というか、解決したい課題を列挙してみました。
これに沿ってライブラリに実装する機能を考えていきます。
- 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(非推奨)とされており、エディタ上では取り消し線が表示された状態になります。
非推奨でも使用することはできますが、返ってくる値は一般人が期待するものではありません。
"2021/09/01 12:34:56.789"からDateのインスタンスを生成し、これらのプロパティが返す値を確認してみます。
実行結果
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のほうですが、それを取得するメソッドは無いようです。
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を取得する機能を拡張関数で追加する(仮)
乞うご期待。