こんにちは。虎の穴ラボのH.Kです。
この記事はKotlinでも「Ruby, Railsで使えるような便利メソッドやプロパティ」を追加してみようというものです。
また、本記事は虎の穴ラボアドベントカレンダー2021の10日目の記事になります。
9日目の記事は弊社エンジニアが富山にてワーケーションをしてきた感想記である『エンジニアがワーケーションをしてみて感じたこと』です。
この記事でできるようになること
具体的な例として月末日時の取得をやってみます。
Kotlinでは以下のように書けます。
LocalDateTime.now().with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX)
これに対して、Ruby on Railsの場合は以下のような簡潔で直感的な書き方ができます。
Time.zone.now.end_of_month
これをKotlinでも以下のように書けるようにする、という記事です。
LocalDateTime.now().endOfMonth
※この記事では拡張関数/プロパティの細かい仕様についてのお話はしません。実装アイデアの紹介となります。
動機
もともと私はとらのあな通販サイトでJava、Kotlinで開発業務に携わっていたのですが、Fantiaなどの開発チームに移動してRuby on Railsに触れました。
そこでRuby on Railsには便利なメソッドがたくさん存在することに驚きました。
これがJavaなどでも使えるようになればなぁと思ったところ、Kotlinには拡張関数や拡張プロパティが使えることを思い出したので実装してみることにしました。
実装方法のサンプル
事前準備
今回はsrc/main/kotlin/extensions
というパッケージで、上記例の月末日時
の取得を実装します。
もちろんプロジェクトによって自由な階層で実装していただけます。
Kotlinファイルを新規作成し、拡張プロパティを定義する
DateTimeExtension.kt
という名前でKotlinファイルを作成します。
この中で拡張プロパティというものを定義して、.endOfMonth
というプロパティを呼び出せるようにします。
DateTimeExtension.kt
の実装は以下の通りです。
package extensions import java.time.LocalDateTime import java.time.temporal.TemporalAdjusters val LocalDateTime.endOfMonth: LocalDateTime get() = with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX)
実際に使ってみる
使う際はLocalDateTime
インスタンスに対してendOfMonth
プロパティを呼び出すだけです。
Main.kt
にmain関数を用意して、実行するサンプルコードです。
import extensions.* import java.time.LocalDateTime fun main() { println(LocalDateTime.now().endOfMonth) // 2021-11-30T23:59:59.999999999 }
拡張プロパティとは何か
Kotlinでは既定クラスを含む全てのクラスに対して自分で実装したプロパティを追加することができます。
例えばString
やInt
はもちろん、自分で定義したUser
クラスなどにも追加できます。
その他の実装例
null判定
Rubyではnull(nil)の判定をnil?
で行うことができます。
nullableなAny型に対して判定するプロパティを実装することにより実現できます。
val Any?.isNull: Boolean get() = this === null val Any?.isPresent: Boolean get() = this !== null
実行例
val nullUser: User? = null println(nullUser.isNull) // true println(nullUser.isPresent) // false
注意点としてはKotlinで実装されているString.take(Int)などのnullableではないメソッド呼び出しのNullガードとしては利用できないので、StringであればisNullOrBlank()
やisNullOrEmpty()
を使ったほうが良いです。
val nullString: String? = null if(nullString.isPresent){ nullString.take(1) // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? }
配列やリストの特定の値を取得する
Rubyでは配列に対して以下のようにfirst
など、順番を表すプロパティで値を取得することができます。
[1, 2, 3, 4].second # 2
Rubyでは1〜5と42が定義されているので、これに倣って実装します。
val <T> Array<T>.first: T? get() = getOrNull(0) val <T> Array<T>.second: T? get() = getOrNull(1) val <T> Array<T>.third: T? get() = getOrNull(2) val <T> Array<T>.fourth: T? get() = getOrNull(3) val <T> Array<T>.fifth: T? get() = getOrNull(4) val <T> Array<T>.forty_two: T? get() = getOrNull(41)
またListについても以下のように実装しておけばRailsの記述を再現できます。(List
でもMutableList
でも使えます)
val <T> List<T>.first: T? get() = getOrNull(0) // 以下省略
実行例
println(arrayOf(1, 2, 3, 4).second) // 2
ちなみにRailsで42が定義されている理由ですが、「生命、宇宙、そして万物についての究極の疑問の答え」であるからのようです。
以下は参考のコミットログです。
github.com
月初日や前日、翌日を取得する
以下はRuby on Railsの例です。
Time.zone.now.beginning_of_month # 月初日を取得 Time.zone.now.yesterday # 前日を取得 Time.zone.now.tomorrow # 翌日を取得
これをKotlinで実現するとこうなります。
// 月初 val LocalDateTime.beginningOfMonth: LocalDateTime get() = with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN) val LocalDate.beginningOfMonth: LocalDate get() = with(TemporalAdjusters.firstDayOfMonth()) // 前日 val LocalDateTime.yesterday: LocalDateTime get() = minusDays(1) val LocalDate.yesterday: LocalDate get() = minusDays(1) // 翌日 val LocalDateTime.tomorrow: LocalDateTime get() = plusDays(1) val LocalDate.tomorrow: LocalDate get() = plusDays(1)
実行例
println(LocalDate.now().beginningOfMonth) // 今月1日の日付を取得
Intから日数や月数を取得する
Ruby on Railsでは特定の日数や月数を、数値.days
等の書き方で取得できます。
例えば以下のようにすると、3日後の日付が取得できます。
Time.zone.now + 3.days
これもKotlinで再現しようと思います。
import java.time.Duration import java.time.Period // n秒間 fun Int.seconds(): Duration = Duration.ofSeconds(this.toLong()) fun Int.second(): Duration = seconds() // n分間 fun Int.minutes(): Duration = Duration.ofMinutes(this.toLong()) fun Int.minute(): Duration = minutes() // n時間 fun Int.hours(): Duration = Duration.ofHours(this.toLong()) fun Int.hour(): Duration = hours() // n日間 fun Int.days(): Period = Period.ofDays(this) fun Int.day(): Period = days() // n週間 fun Int.weeks(): Period = Period.ofWeeks(this) fun Int.week(): Period = weeks() // nヶ月間 fun Int.months(): Period = Period.ofMonths(this) fun Int.month(): Period = months() // n年間 fun Int.years(): Period = Period.ofYears(this) fun Int.year(): Period = years()
今回、拡張プロパティではなく、拡張関数を使って実装しました。
これはKotlin側の実装ですでにInt.hours
等のプロパティが定義済であるためです。
ただし、このInt.hours
等のプロパティは戻り値の型がkotlin.time.Duration
であり、experimental(試験的)な実装となっています。
そのため、java.time.Duration
やjava.time.Period
を使った拡張関数を追加で定義して対応します。
拡張関数はfun 定義対象の型.関数名
という形で定義できます。
実行例
println(LocalDate.now().plus(10.months())) // 10ヶ月後の日付を取得
日付フォーマットを統一する
Javaだとユーティリティクラスを作って実装しなければいけないような処理も拡張プロパティで解決できます。
例えば2021/12/10 10:00
のような形式をアプリケーションの標準としたい場合は以下のように拡張プロパティを実装します。
private val YYYY_MM_DD_HH_MM_WITH_SLASH_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm") val LocalDateTime.yyyyMmDdHhMm: String get() = format(YYYY_MM_DD_HH_MM_WITH_SLASH_FORMAT) val LocalDateTime.defaultFormat: String get() = yyyyMmDdHhMm
実行例
println(LocalDateTime.now().defaultFormat) // 2021/12/10 10:00
まとめ
ここまでの拡張プロパティや拡張関数を定義することで、日付周りを中心にかなり直感的なコーディングができるようになったと思います。
自分用の拡張関数/プロパティ郡を定義したモジュールを一つ準備しておくだけで開発体験が向上すると考えています。
他にも「こんなプロパティや関数を定義して便利に使っている」という例がありましたら、コメント等で教えていただけると、嬉しいです!!
ちなみにですが、私は拡張関数やプロパティを以下のようなディレクトリにKotlinファイルとして定義しています。
こうすることにより、呼び出し側はimport extensions.*
のみで対応可能になり、取り回しやすくなります。
P.S.
■ 採用情報
🔻募集職種
yumenosora.co.jp
🔻カジュアル面談も随時開催中です。お申し込みはこちら!
news.toranoana.jp
■ ToraLab.fmスタートしました!
🔻メンバーによるPodcastを配信中!是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm
■ Twitterもフォローしてくださいね!
🔻ツイッターでも随時情報発信をしています。
twitter.com