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

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

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

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


開発者Gです。

前回はDateに年月日時分秒ミリ秒を加算、減算するのに便利な拡張関数を追加しました。

今回はDate, LocalDate, LocalDateTimeの相互運用について考えてみます。

Date, LocalDate, LocalDateTimeの相互運用

前回まではKotlinの拡張関数と拡張プロパティを使用し、Date(java.util.Date)を大幅に機能強化しました。
これによりDateの使い勝手に関する不満の多くは解消されたと思います。

といっても、今後もDateの使用を継続することを勧めているわけではなく、LocalDateやLocalDateTimeへの移行期間中にDateを使用せざるを得ないシーンでの使用を想定しています。

移行するにあたっては、DateからLocalDateやLocalDateTimeへの変換や、その逆の変換が必要になります。

f:id:ldi-contributor:20210912171211p:plain
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

ここまでのまとめ

これまでに作成した拡張プロパティや拡張関数をおさらいしましょう。

f:id:ldi-contributor:20210919111210p:plain
これまでに作成した拡張プロパティと拡張関数


こうして図にしてみると、結構たくさんつくりましたね。

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の操作を簡単にするライブラリをつくってみる」の連載記事はいかがでしたでしょうか?

ここまで読んでくれた方、オレオレライブラリ作りたくなったんじゃありませんか?

作りましょう!オレオレライブラリ。
やりましょう!車輪の再発明。

自分でやってみないとわからないことがある。
きっと無駄になることはないでしょう。

ただし、業務ではなく趣味でじっくりやることをお勧めします。
そのほうがクリエイティブにやれると思います。

ではまた。