添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Dateに拡張関数formatを追加する

SimpleDateFormatやDateFormatUtilsを使用するとDateを指定したフォーマットで文字列に出力できますが、いつも同じフォーマットで良いのであれば毎回指定するのは面倒ですし、デフォルトの動作を決めた方が記述ミスも防げます。

以下のような仕様の拡張関数を追加しましょう。

  • ミリ秒が0以外の値であれば "yyyy/MM/dd HH:mm.ss.SSS" で出力する
  • ミリ秒が0 かつ 時分秒のいずれかが0以外の値であれば "yyyy/MM/dd HH:mm.ss" で出力する
  • 上記以外ならば "yyyy/MM/dd" で出力する
  • オプションでタイムゾーンを出力する
  • DateFormatExtension.kt
    package extensions.date.format
    import extensions.date.property.hourValue
    import extensions.date.property.millisecondValue
    import extensions.date.property.minuteValue
    import extensions.date.property.secondValue
    import java.text.SimpleDateFormat
    import java.util.*
     * Dateをフォーマットして文字列を出力します。
     *  @param [pattern] SimpleDateFormatのpattern
     *  @param [tz] SimpleDateFormatのpatternのうちタイムゾーン部分だけを指定したい場合にz,Z,Xを指定できます
    fun Date.format(pattern: String? = null, tz: String? = null): String {
        var sdfFormat = (
                if (pattern.isNullOrBlank().not())
                    pattern!!
                else if (this.millisecondValue != 0)
                    "yyyy/MM/dd HH:mm:ss.SSS"
                else if (this.hourValue != 0 || this.minuteValue != 0 || this.secondValue != 0)
                    "yyyy/MM/dd HH:mm:ss"
                    "yyyy/MM/dd"
        if (tz != null) {
            when (tz) {
                "z" -> sdfFormat += " z"
                else -> sdfFormat += tz
        val sdf = SimpleDateFormat(sdfFormat)
        return sdf.format(this)
        
    package extensions.date.format
    import org.apache.commons.lang3.time.DateUtils
    import org.junit.Test
    class DateFormatExtensionTest {
        @Test
        fun format() {
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val output = date.format()  // 2021/09/01 12:34:56.789
                println("date.format(): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56", "yyyy/MM/dd HH:mm:ss")
                val output = date.format()  // 2021/09/01 12:34:56
                println("date.format(): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01", "yyyy/MM/dd")
                val output = date.format()  // 2021/09/01
                println("date.format(): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val pattern = "yyyy/MM/dd HH:mm:ss.SSSz"
                val output = date.format(pattern)   // 2021/09/01 12:34:56.789JST
                println("date.format(\"$pattern\"): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val pattern = "yyyy/MM/dd HH:mm:ss.SSSZ"
                val output = date.format(pattern)   // 2021/09/01 12:34:56.789+0900
                println("date.format(\"$pattern\"): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val pattern = "yyyy/MM/dd HH:mm:ss.SSSX"
                val output = date.format(pattern)   // 2021/09/01 12:34:56.789+09
                println("date.format(\"$pattern\"): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val output = date.format(tz = "z")  // 2021/09/01 12:34:56.789 JST
                println("date.format(tz=\"z\"): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val output = date.format(tz = "Z")  // 2021/09/01 12:34:56.789+0900
                println("date.format(tz=\"Z\"): $output")
            run {
                val date = DateUtils.parseDate("2021/09/01 12:34:56.789", "yyyy/MM/dd HH:mm:ss.SSS")
                val output = date.format(tz = "X")  // 2021/09/01 12:34:56.789+09
                println("date.format(tz=\"X\"): $output")
        
    date.format(): 2021/09/01 12:34:56.789
    date.format(): 2021/09/01 12:34:56
    date.format(): 2021/09/01
    date.format("yyyy/MM/dd HH:mm:ss.SSSz"): 2021/09/01 12:34:56.789JST
    date.format("yyyy/MM/dd HH:mm:ss.SSSZ"): 2021/09/01 12:34:56.789+0900
    date.format("yyyy/MM/dd HH:mm:ss.SSSX"): 2021/09/01 12:34:56.789+09
    date.format(tz="z"): 2021/09/01 12:34:56.789 JST
    date.format(tz="Z"): 2021/09/01 12:34:56.789+0900
    date.format(tz="X"): 2021/09/01 12:34:56.789+09


    なお、日時パターンの記述方法についてはSimpleDateFormatのヘルプを参照してください。
    SimpleDateFormat (Java Platform SE 8)


    文字列をパースしてDateを取得する

    どのような文字列をパースして日付を得たいかはケースバイケースですが、やはりデフォルトの挙動を決め打ちにすることで、フォーマットを都度指定する手間を省き、ミスを防止することができます。

    以下のような仕様の拡張関数を追加しましょう。

  • オプションでタイムゾーンを指定できる。指定しない場合はシステムデフォルトを適用する
  • タイムゾーンを除く部分について
  • 文字列長が8ならば"yyyyMMdd"としてパースする
  • 文字列長が10ならば"yyyy/MM/dd"としてパースする
  • 文字列長が14ならば"yyyyMMddHHmmss"としてパースする
  • 文字列長が17ならば"yyyyMMddHHmmssSSS"としてパースする
  • 文字列長が19ならば"yyyy/MM/dd HH:mm.ss"としてパースする
  • 文字列長が23ならば"yyyy/MM/dd HH:mm.ss.SSS"としてパースする
  • 上記以外ならば"yyyyMMdd"としてパースする
  • StringDateExtension.kt
    package extensions.string.date
    import java.text.SimpleDateFormat
    import java.time.ZoneId
    import java.util.*
     * 文字列をDateに変換します。変換できない場合は例外を発生させます。
     * length -> format
     * 8 -> "yyyyMMdd"
     * 10 -> "yyyy/MM/dd"
     * 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.toDate(pattern: String? = null, tz: String? = null, strict: Boolean = true): Date {
        val tokens = this.replace("-", "|-").replace("+", "|+").split("|")
        val datePart = tokens[0]
        val zonePart = if (tokens.count() == 2) tokens[1] else ""
        val zonePattern =
            if (tz != null) tz
            else if (tokens.count() == 2) "X"
            else ""
        val sdfPattern = pattern
            ?: when (datePart.length) {
                8 -> "yyyyMMdd"
                10 -> "yyyy/MM/dd"
                14 -> "yyyyMMddHHmmss"
                17 -> "yyyyMMddHHmmssSSS"
                19 -> "yyyy/MM/dd HH:mm:ss"
                23 -> "yyyy/MM/dd HH:mm:ss.SSS"
                else -> "yyyy/MM/dd"
            } + zonePattern
        try {
            val sdf = SimpleDateFormat(sdfPattern)
            sdf.isLenient = false
            if (zonePart.isNotBlank()) {
                sdf.timeZone = TimeZone.getTimeZone(ZoneId.of(zonePart))
            val date = sdf.parse(this)
            if (strict) {
                val reformat =
                    sdf.format(date).replace("Z", "").replace(zonePart, "").replace(zonePart.replace("00", ""), "")
                val thisWithoutZonePart = this.replace(zonePart, "")
                if (thisWithoutZonePart != reformat) {
                    throw IllegalArgumentException("strict=$strict が指定されています。")
            return date
        } catch (t: Throwable) {
            throw IllegalArgumentException("Dateに変換できません。(this=$this, pattern=$pattern)", t)
     * 文字列をDateに変換します。変換できない場合はnullを返します。
     * length -> format
     * 8 -> "yyyyMMdd"
     * 10 -> "yyyy/MM/dd"
     * 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.toDateOrNull(pattern: String? = null): Date? {
        try {
            return this.toDate(pattern = pattern)
        } catch (t: Throwable) {
            return null
        
    package extensions.string.date
    import extensions.date.format.format
    import org.junit.Test
    class StringDateExtensionTest {
         * 文字列をDateに変換します。変換できない場合は例外を発生させます。
         * length -> format
         * 8 -> "yyyyMMdd"
         * 10 -> "yyyy/MM/dd"
         * 14 -> "yyyyMMddHHmmss"
         * 17 -> "yyyyMMddHHmmssSSS"
         * 19 -> "yyyy/MM/dd HH:mm:ss"
         * 23 -> "yyyy/MM/dd HH:mm:ss.SSS"
         * else -> "yyyy/MM/dd"
        @Test
        fun toDate() {
            println()
            println("toDate()")
            run {
                val text = "20210901"
                val date = text.toDate()
                println("(length=8)  $text -> ${date.format()}")
            run {
                val text = "2021/09/01"
                val date = text.toDate()
                println("(length=10) $text -> ${date.format()}")
            run {
                val text = "20210901123456"
                val date = text.toDate()
                println("(length=14) $text -> ${date.format()}")
            run {
                val text = "20210901123456789"
                val date = text.toDate()
                println("(length=17) $text -> ${date.format()}")
            run {
                val text = "2021/09/01 12:34:56"
                val date = text.toDate()
                println("(length=19) $text -> ${date.format()}")
            run {
                val text = "2021/09/01 12:34:56.789"
                val date = text.toDate()
                println("(length=23) $text -> ${date.format()}")
            run {
                val text = "2021/09/01 12:34:56.789+00"
                val date = text.toDate()
                println("(length=23+TZ) $text -> ${date.format()}")
            run {
                val text = "2021/09/01 12:34:56.789+09"
                val date = text.toDate()
                println("(length=23+TZ) $text -> ${date.format()}")
            run {
                val text = "2021/09/01 12:34:56.789+0900"
                val date = text.toDate()
                println("(length=23+TZ) $text -> ${date.format()}")
            try {
                "2021".toDate()
            } catch (t: Throwable) {
                println(t)
        @Test
        fun toDateOrNull() {
            println()
            println("toDateOrNull()")
            run {
                val text = "20210901"
                val date = text.toDateOrNull()
                println("(length=8)  $text -> ${date?.format()}")
            run {
                val text = "2021/09/31"
                val date = text.toDateOrNull()
                println("$text -> $date")
        
    toDate()
    (length=8)  20210901 -> 2021/09/01
    (length=10) 2021/09/01 -> 2021/09/01
    (length=14) 20210901123456 -> 2021/09/01 12:34:56
    (length=17) 20210901123456789 -> 2021/09/01 12:34:56.789
    (length=19) 2021/09/01 12:34:56 -> 2021/09/01 12:34:56
    (length=23) 2021/09/01 12:34:56.789 -> 2021/09/01 12:34:56.789
    (length=23+TZ) 2021/09/01 12:34:56.789+00 -> 2021/09/01 21:34:56.789
    (length=23+TZ) 2021/09/01 12:34:56.789+09 -> 2021/09/01 12:34:56.789
    (length=23+TZ) 2021/09/01 12:34:56.789+0900 -> 2021/09/01 12:34:56.789
    java.lang.IllegalArgumentException: Dateに変換できません。(this=2021, pattern=null)
    toDateOrNull()
    (length=8)  20210901 -> 2021/09/01
    2021/09/31 -> null


    上記のように"20210901".toDate()を実行すると、文字列長が8なので"yyyyMMdd"のフォーマットが自動的に決定され、パースが実行されます。

    自動決定されるフォーマットがプロダクトの要件にマッチする場合は、上記のように生産性を向上することができます。