虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

KotlinでRuby/Railsっぽい便利なプロパティ(メソッド)を使えるようにしてみる!

こんにちは。虎の穴ラボの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というパッケージで、上記例の月末日時 の取得を実装します。
もちろんプロジェクトによって自由な階層で実装していただけます。

f:id:toranoana-lab:20211124165338p:plain
階層図

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では既定クラスを含む全てのクラスに対して自分で実装したプロパティを追加することができます。
例えばStringIntはもちろん、自分で定義したUserクラスなどにも追加できます。

dogwood008.github.io

その他の実装例

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.Durationjava.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.*のみで対応可能になり、取り回しやすくなります。

f:id:toranoana-lab:20211124185025p:plain
拡張関数/プロパティの階層例

P.S.

■ 採用情報
🔻募集職種
yumenosora.co.jp

🔻カジュアル面談も随時開催中です。お申し込みはこちら!
news.toranoana.jp

■ ToraLab.fmスタートしました!
🔻メンバーによるPodcastを配信中!是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm

■ Twitterもフォローしてくださいね!
🔻ツイッターでも随時情報発信をしています。
twitter.com