開発者Gです。
前回は日付の操作を簡単にするオレオレライブラリのコンセプトを示し、まずDateとCalendarに関する問題を解決する拡張プロパティを作りました。
今回はさらにDateを文字列にフォーマットする機能と、文字列をパースしてDateを取得する機能を拡張関数で追加してみます。
なお、ソースコードは全部、または一部を抜粋して表示します。
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" else "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"のフォーマットが自動的に決定され、パースが実行されます。
自動決定されるフォーマットがプロダクトの要件にマッチする場合は、上記のように生産性を向上することができます。
次回予告
日付の加算・減算を簡単にする拡張関数を追加する(仮)
乞うご期待。