開発者Gです。
前回はDateに年月日時分秒ミリ秒を加算、減算するのに便利な拡張関数を追加しました。
今回はDate, LocalDate, LocalDateTimeの相互運用について考えてみます。
- Date, LocalDate, LocalDateTimeの相互運用
- LocalDateTimeのnanoについて
- 文字列をパースしてLocalDate, LolcalDateTimeを取得する
- ここまでのまとめ
- オレオレDateライブラリの使用例
- おわりに
Date, LocalDate, LocalDateTimeの相互運用
前回まではKotlinの拡張関数と拡張プロパティを使用し、Date(java.util.Date)を大幅に機能強化しました。
これによりDateの使い勝手に関する不満の多くは解消されたと思います。
といっても、今後もDateの使用を継続することを勧めているわけではなく、LocalDateやLocalDateTimeへの移行期間中にDateを使用せざるを得ないシーンでの使用を想定しています。
移行するにあたっては、DateからLocalDateやLocalDateTimeへの変換や、その逆の変換が必要になります。
DateとLocalDateTimeはミリ秒精度までなら情報が欠落することなく相互に変換できます。LocalDateTimeはナノ秒まで使用できますが、ミリ秒よりも高い精度を使用した場合はDateTImeへの変換で情報が欠落するので注意が必要です。
DateからLocalDateへの変換はLocalDateがdayまでの精度しか保持しないため、時分秒ミリ秒の情報は欠落します。もっとも、LocalDateは年月日を扱いやすくするのが目的ですので、これは問題ではありません。
LocalDateからDateへの変換は情報の欠落はなく、特に問題ありません。
LocalDateTimeにはすでにtoLocalDate()が用意されていますので、それ以外の相互変換用に以下の拡張関数を追加しましょう。
対象のクラス | 拡張関数 | 説明 |
---|---|---|
Date |
toLocalDate() |
LocalDateへ変換します |
Date |
toLocalDateTime() |
LocalDateTimeへ変換します |
LocalDate |
toDate() |
Dateへ変換します |
LocalDate |
toLocalDateTime() |
LocalDateTimeへ変換します |
LocalDateTime |
toDate() |
Dateへ変換します |
これらの拡張関数を実装した3つのソースコードファイルを以下に示します。
型を変換するだけなので、特別な解説は不要かと思います。
DateInteropExtension.kt
DateをLocalDate/LocalDateTimeへ変換します。
package extensions.date.interop import extensions.date.property.* import java.time.LocalDate import java.time.LocalDateTime import java.util.* /** * DateをLocalDateに変換します。 */ fun Date.toLocalDate(): LocalDate { return LocalDate.of(this.yearValue, this.monthValue, this.dayValue) } /** * DateをLocalDateTimeに変換します。 */ fun Date.toLocalDateTime(): LocalDateTime { return LocalDateTime.of( this.yearValue, this.monthValue, this.dayValue, this.hourValue, this.minuteValue, this.secondValue, this.millisecondValue * 1000000 // nano ) }
使用例
package extensions.date.interop import extensions.date.format.format import extensions.string.date.toDate import org.junit.Test class DateInteropExtensionTest { @Test fun toLocalDate() { val date = "2021/09/01".toDate() val localDate = date.toLocalDate() println("date=${date.format()}") println("localDate=$localDate") println("localDate.year=${localDate.year}") println("localDate.monthValue=${localDate.monthValue}") println("localDate.dayOfMonth=${localDate.dayOfMonth}") } @Test fun toLocalDateTime() { val date = "2021/09/01 12:34:56.789".toDate() val localDateTime = date.toLocalDateTime() println() println("date=${date.format()}") println("localDateTime=${localDateTime}") println("localDateTime.year=${localDateTime.year}") println("localDateTime.monthValue=${localDateTime.monthValue}") println("localDateTime.dayOfMonth=${localDateTime.dayOfMonth}") println("localDateTime.hour=${localDateTime.hour}") println("localDateTime.minute=${localDateTime.minute}") println("localDateTime.second=${localDateTime.second}") println("localDateTime.nano=${localDateTime.nano}") } }
実行結果
date=2021/09/01 localDate=2021-09-01 localDate.year=2021 localDate.monthValue=9 localDate.dayOfMonth=1 date=2021/09/01 12:34:56.789 localDateTime=2021-09-01T12:34:56.789 localDateTime.year=2021 localDateTime.monthValue=9 localDateTime.dayOfMonth=1 localDateTime.hour=12 localDateTime.minute=34 localDateTime.second=56 localDateTime.nano=789000000
LocalDateInteropExtension.kt
LocalDateをDate/LocalDateTimeへ変換します。
package extensions.localdate.interop import extensions.string.date.toDate import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* /** * LocalDateをDateに変換します。 */ fun LocalDate.toDate(): Date { val pattern = "yyyy/MM/dd" val dateString = DateTimeFormatter.ofPattern(pattern).format(this) return dateString.toDate(pattern = pattern) } /** * LocalDateをLocalDateTimeに変換します。 */ fun LocalDate.toLocalDateTime(): LocalDateTime { return LocalDateTime.of(this.year, this.monthValue, this.dayOfMonth, 0, 0, 0) }
使用例
package extensions.localdate.interop import extensions.date.format.format import org.junit.Test import java.time.LocalDate class LocalDateExtensionTest { @Test fun toDate() { val localDate = LocalDate.parse("2021-09-01") val date = localDate.toDate() println("localDate=$localDate") println("date=${date.format()}") } @Test fun toLocalDateTime() { val localDate = LocalDate.parse("2021-09-01") val localDateTime = localDate.toLocalDateTime() println() println("localDate=$localDate") println("localDateTime.year=${localDateTime.year}") println("localDateTime.monthValue=${localDateTime.monthValue}") println("localDateTime.dayOfMonth=${localDateTime.dayOfMonth}") println("localDateTime.hour=${localDateTime.hour}") println("localDateTime.minute=${localDateTime.minute}") println("localDateTime.second=${localDateTime.second}") println("localDateTime.nano=${localDateTime.nano}") } }
実行結果
localDate=2021-09-01 date=2021/09/01 localDate=2021-09-01 localDateTime.year=2021 localDateTime.monthValue=9 localDateTime.dayOfMonth=1 localDateTime.hour=0 localDateTime.minute=0 localDateTime.second=0 localDateTime.nano=0
LocalDateTimeInteropExtension.kt
LocalDateTimeをDateへ変換します。
package extensions.localdatetime.interop import extensions.string.date.toDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* /** * LocalDateTimeをDateに変換します。 */ fun LocalDateTime.toDate(): Date { val pattern = "yyyy/MM/dd HH:mm:ss.SSS" val dateTimeString = DateTimeFormatter.ofPattern(pattern).format(this) return dateTimeString.toDate(pattern = pattern) }
使用例
package extensions.localdatetime.interop import extensions.date.format.format import extensions.date.property.* import org.junit.Test import java.time.LocalDateTime class LocalDateTimeExtensionTest { @Test fun toDate() { val localDateTime = LocalDateTime.parse("2021-09-01T12:34:56.789") val date = localDateTime.toDate() println("lodalDateTime=$localDateTime") println("date=${date.format()}") println("date.year=${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.millisecondValue=${date.millisecondValue}") } }
実行結果
lodalDateTime=2021-09-01T12:34:56.789 date=2021/09/01 12:34:56.789 date.year=2021 date.monthValue=9 date.dayValue=1 date.hourValue=12 date.minuteValue=34 date.secondValue=56 date.millisecondValue=789
LocalDateTimeのnanoについて
LocalDateTimeでは秒未満の精度がナノ秒になっています。
ミリ秒を直接取得する方法は用意されていません。
ちなみにミリ秒の1/1000がマイクロ秒、マイクロ秒の1/1000がナノ秒なので
ミリ秒の1/1000000がナノ秒となります。
特定用途のアプリケーションならナノ秒の精度を必要とするのかもしれませんが、個人的にはオーバースペックですし、
Dateの精度はミリですから、相互運用する上ではミリ秒に統一したいです。
なので、LocalDateTimeにミリ秒を取得するプロパティを追加してみましょう。
LocalDateTimePropertyExtension.kt
package extensions.localdatetime.property import java.time.LocalDateTime /** * ミリ秒を取得します。 */ val LocalDateTime.millisecondValue: Int get() { return this.nano / 1000000 }
使用例
package extensions.localdatetime.property import org.junit.Test import java.time.LocalDateTime class LocalDateTimePropertyExtensionTest { @Test fun millisecondValue() { val localDateTime = LocalDateTime.now() println("localDateTime=$localDateTime") println("localDateTime.nano=${localDateTime.nano}") println("localDateTime.millisecondValue=${localDateTime.millisecondValue}") } }
実行結果
localDateTime=2021-09-19T11:02:07.342918 localDateTime.nano=342918000 localDateTime.millisecondValue=342
LocalDateTImeは型の定義上はナノ秒まで格納できるようになっていますが、手元のMacだと".342918"となり、マイクロ秒精度までしか取得できないようです。
nanoプロパティを取得すると342918000になります。
millisecondValueプロパティを取得すると342となります。単純に1000000で割っているので切り捨てになっていることが確認できます。
文字列をパースしてLocalDate, LolcalDateTimeを取得する
LocalDateについても文字列に対してtoDate()のように使用できる拡張関数を追加しましょう。
StringLocalDateExtension.kt
package extensions.string.localdate import java.time.LocalDate import java.time.format.DateTimeFormatter import java.time.format.ResolverStyle /** * 文字列をLocalDateに変換します。変換できない場合は例外を発生させます。 * * length -> pattern * 8 -> "yyyyMMdd" * 10 -> "yyyy/MM/dd" * else -> "yyyy/MM/dd" */ fun String.toLocalDate(pattern: String? = null): LocalDate { val p = pattern ?: when (this.length) { 8 -> "yyyyMMdd" 10 -> "yyyy/MM/dd" else -> "yyyy-MM-dd" }.replace("y", "u") try { val dtf = DateTimeFormatter.ofPattern(p).withResolverStyle(ResolverStyle.STRICT) return LocalDate.parse(this, dtf) } catch (t: Throwable) { throw IllegalArgumentException("LocalDateに変換できません。(this=$this, pattern=$pattern)", t) } } /** * 文字列をLocalDateに変換します。変換できない場合はnullを返します。 * * length -> pattern * 8 -> "yyyyMMdd" * 10 -> "yyyy/MM/dd" * else -> "yyyy/MM/dd" */ fun String.toLocalDateOrNull(pattern: String? = null): LocalDate? { try { return this.toLocalDate(pattern = pattern) } catch (t: Throwable) { return null } }
使用例
package extensions.string.localdate import org.junit.Test class StringLocalDateExtensionTest { @Test fun toLocalDate() { println() println("toLocalDate()") run { val text = "20210901" val localDate = text.toLocalDate() println("$text -> $localDate") } run { val text = "2021/09/01" val localDate = text.toLocalDate() println("$text -> $localDate") } try { "2021".toLocalDate() } catch (t: Throwable) { println(t) } } @Test fun toLocalDateOrNull() { println() println("toLocalDateOrNull()") run { val text = "20210901" val localDate = text.toLocalDateOrNull() println("$text -> $localDate") } run { val text = "2021/09/01" val localDate = text.toLocalDateOrNull() println("$text -> $localDate") } run { val text = "2021" val localDate = text.toLocalDateOrNull() println("$text -> $localDate") } } }
実行結果
toLocalDate() 20210901 -> 2021-09-01 2021/09/01 -> 2021-09-01 java.lang.IllegalArgumentException: LocalDateに変換できません。(this=2021, pattern=null) toLocalDateOrNull() 20210901 -> 2021-09-01 2021/09/01 -> 2021-09-01 2021 -> null
同様にLocalDateTimeについても文字列に対してtoDate()のように使用できる拡張関数を追加しましょう。
StringLocalDateTimeExtension.kt
package extensions.string.localdatetime import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.format.ResolverStyle /** * 文字列をLocalDateTimeに変換します。変換できない場合はnullを返します。 * * length -> pattern * 14 -> "yyyyMMddHHmmss" * 17 -> "yyyyMMddHHmmssSSS" * 19 -> "yyyy/MM/dd HH:mm:ss" * 23 -> "yyyy/MM/dd HH:mm:ss.SSS" * else -> "yyyy/MM/dd" */ fun String.toLocalDateTime(pattern: String? = null): LocalDateTime { val p = pattern ?: when (this.length) { 14 -> "yyyyMMddHHmmss" 17 -> "yyyyMMddHHmmssSSS" 19 -> "yyyy/MM/dd HH:mm:ss" 23 -> "yyyy/MM/dd HH:mm:ss.SSS" else -> "yyyyMMddHHmmss" }.replace("y", "u") try { val dtf = DateTimeFormatter.ofPattern(p).withResolverStyle(ResolverStyle.STRICT) return LocalDateTime.parse(this, dtf) } catch (t: Throwable) { throw IllegalArgumentException("LocalDateTimeに変換できません。(this=$this, pattern=$pattern)", t) } } /** * 文字列をLocalDateTimeに変換します。変換できない場合はnullを返します。 * * length -> pattern * 14 -> "yyyyMMddHHmmss" * 17 -> "yyyyMMddHHmmssSSS" * 19 -> "yyyy/MM/dd HH:mm:ss" * 23 -> "yyyy/MM/dd HH:mm:ss.SSS" * else -> "yyyy/MM/dd" */ fun String.toLocalDateTimeOrNull(pattern: String? = null): LocalDateTime? { try { return this.toLocalDateTime(pattern = pattern) } catch (t: Throwable) { return null } }
使用例
package extensions.string.localdatetime import extensions.string.localdate.toLocalDate import org.junit.Test class StringLocalDateTimeExtensionTest { @Test fun toLocalDateTime() { println() println("toLocalDateTime()") run { val text = "20210901123456" val localDateTime = text.toLocalDateTime() println("$text -> $localDateTime") } run { val text = "20210901123456789" val localDateTime = text.toLocalDateTime() println("$text -> $localDateTime") } run { val text = "2021/09/01 12:34:56" val localDateTime = text.toLocalDateTime() println("$text -> $localDateTime") } run { val text = "2021/09/01 12:34:56.789" val localDateTime = text.toLocalDateTime() println("$text -> $localDateTime") } try { "2021".toLocalDate() } catch (t: Throwable) { println(t) } } @Test fun toLocalDateTimeOrNull() { println() println("toLocalDateTimeOrNull()") run { val text = "2021/09/01 12:34:56.789" val localDateTime = text.toLocalDateTimeOrNull() println("$text -> $localDateTime") } run { val text = "2021" val localDateTime = text.toLocalDateTimeOrNull() println("$text -> $localDateTime") } } }
実行結果
toLocalDateTime() 20210901123456 -> 2021-09-01T12:34:56 20210901123456789 -> 2021-09-01T12:34:56.789 2021/09/01 12:34:56 -> 2021-09-01T12:34:56 2021/09/01 12:34:56.789 -> 2021-09-01T12:34:56.789 java.lang.IllegalArgumentException: LocalDateに変換できません。(this=2021, pattern=null) toLocalDateTimeOrNull() 2021/09/01 12:34:56.789 -> 2021-09-01T12:34:56.789 2021 -> null
ここまでのまとめ
これまでに作成した拡張プロパティや拡張関数をおさらいしましょう。
こうして図にしてみると、結構たくさんつくりましたね。
Dateを中心にString、LocalDate、LocalDateTimeとの相互変換が容易になるように拡張関数を実装しています。
また、Dateの機能不足を補うために拡張プロパティや拡張関数を実装しています。
Date単体だとこれでもまだまだ機能不足なのですが、そこはLocalDateやLocalDateTimeへ変換してからそれらの高度な機能を利用すればよいと思います。
さいごに、これらの使用例をまとめたサンプルコードでしめたいと思います。
オレオレDateライブラリの使用例
package extensions.sample import extensions.date.format.format import extensions.date.interop.toLocalDate import extensions.date.interop.toLocalDateTime import extensions.date.plusminus.* import extensions.date.property.* import extensions.localdate.interop.toDate import extensions.localdate.interop.toLocalDateTime import extensions.localdatetime.interop.toDate import extensions.string.date.toDate import extensions.string.localdate.toLocalDate import extensions.string.localdatetime.toLocalDateTime import org.junit.Test import java.time.LocalDate class OleOleDateLibrarySample1 { @Test fun string2date2string() { run { println("# 拡張関数(String <-> Date) ※日時パターン指定なし") val string = "20210901" val date = string.toDate() val format = date.format() println("string: $string") println("string.toDate(): $date") println("date.format(): $format") println() } run { println("# 拡張関数(String <-> Date) ※日時パターン指定あり") val string = "2021.09.01" val pattern = "yyyy.MM.dd" val date = string.toDate(pattern = pattern) val format = date.format(pattern = pattern) println("string: $string") println("pattern: $pattern") println("string.toDate(): $date") println("date.format(): $format") println() } } @Test fun dateProperties() { println("# dateの拡張プロパティ") val string = "2021/09/01 12:34:56.789" val date = string.toDate() println("string: $string") 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.millisecondValue: ${date.millisecondValue}") println() } @Test fun datePlusMinusFunctions() { println("# dateの拡張メソッド(plus/minus)") val date = "2021/09/01 12:34:56.789".toDate() val datePlus = date .plusYears(1) .plusMonths(1) .plusDays(1) .plusHours(1) .plusMinutes(1) .plusSeconds(1) .plusMilliseconds(1) val dateMinus = datePlus .minusYears(1) .minusMonths(1) .minusDays(1) .minusHours(1) .minusMinutes(1) .minusSeconds(1) .minusMilliseconds(1) println("date: ${date.format()}") println("datePlus: ${datePlus.format()}") println("dateMinus: ${dateMinus.format()}") println() } @Test fun date2LocalDate2date() { println("# 拡張関数(Date <-> LocalDate)") val date = "2021/09/01".toDate() println("date: $date") println("date.toLocalDate(): ${date.toLocalDate()}") println("localDate.toDate(): ${date.toLocalDate().toDate()}") println() } @Test fun date2LocalDateTime2date() { println("# 拡張関数(Date <-> LocalDateTime)") val date = "2021/09/01 12:34:56.789".toDate() println("date: ${date.format()}") println("date.toLocalDateTime(): ${date.toLocalDateTime()}") println("localDateTime.toDate(): ${date.toLocalDateTime().toDate().format()}") println() } @Test fun localDate2localDateTime2localDate() { println("# 拡張関数(LocalDate <-> LocalDateTime)") val localDate = LocalDate.of(2021, 9, 1) val localDateTime = localDate.toLocalDateTime() println("localDate: $localDate") println("localDate.toLocalDateTime(): ${localDate.toLocalDateTime()}") println("localDateTime.toLocalDate(): ${localDate.toLocalDateTime().toLocalDate()}") println() } @Test fun string2localDate_localDateTime() { println("# 拡張関数(String -> LocalDate, String -> LocalDateTime)") val string1 = "2021/09/01" val string2 = "2021/09/01 12:34:56.789" println("\"2021/09/01\".toLocalDate(): ${string1.toLocalDate()}") println("\"2021/09/01 12:34:56.789\".toLocalDateTime(): ${string2.toLocalDateTime()}") println() } }
実行結果
# dateの拡張メソッド(plus/minus) date: 2021/09/01 12:34:56.789 datePlus: 2022/10/02 13:35:57.790 dateMinus: 2021/09/01 12:34:56.789 # 拡張関数(LocalDate <-> LocalDateTime) localDate: 2021-09-01 localDate.toLocalDateTime(): 2021-09-01T00:00 localDateTime.toLocalDate(): 2021-09-01 # 拡張関数(String <-> Date) ※日時パターン指定なし string: 20210901 string.toDate(): Wed Sep 01 00:00:00 JST 2021 date.format(): 2021/09/01 # 拡張関数(String <-> Date) ※日時パターン指定あり string: 2021.09.01 pattern: yyyy.MM.dd string.toDate(): Wed Sep 01 00:00:00 JST 2021 date.format(): 2021.09.01 # 拡張関数(Date <-> LocalDateTime) date: 2021/09/01 12:34:56.789 date.toLocalDateTime(): 2021-09-01T12:34:56.789 localDateTime.toDate(): 2021/09/01 12:34:56.789 # dateの拡張プロパティ string: 2021/09/01 12:34:56.789 date.yearValue: 2021 date.monthValue: 9 date.dayValue: 1 date.hourValue: 12 date.minuteValue: 34 date.secondValue: 56 date.millisecondValue: 789 # 拡張関数(Date <-> LocalDate) date: Wed Sep 01 00:00:00 JST 2021 date.toLocalDate(): 2021-09-01 localDate.toDate(): Wed Sep 01 00:00:00 JST 2021 # 拡張関数(String -> LocalDate, String -> LocalDateTime) "2021/09/01".toLocalDate(): 2021-09-01 "2021/09/01 12:34:56.789".toLocalDateTime(): 2021-09-01T12:34:56.789
おわりに
7回シリーズでお届けしました「KotlinでDateの操作を簡単にするライブラリをつくってみる」の連載記事はいかがでしたでしょうか?
ここまで読んでくれた方、オレオレライブラリ作りたくなったんじゃありませんか?
作りましょう!オレオレライブラリ。
やりましょう!車輪の再発明。
自分でやってみないとわからないことがある。
きっと無駄になることはないでしょう。
ただし、業務ではなく趣味でじっくりやることをお勧めします。
そのほうがクリエイティブにやれると思います。
ではまた。