年度といえば「4月1日始まり」と考えがちですが、
世界にはさまざまな年度があります。
Fiscal year - Wikipedia
例えばアメリカ合衆国連邦政府の会計年度であれば、前の年の10月1日から始まるそうです。
2000年度は 1999-10-01 から 2000-09-30 までということです。
これらをうまく扱う API を作成しました。
ちなみに英語には会計年度(fiscal year / financial year)や学校年度(school year / academic year)というものはあってもそれらを総称する「年度」に相当する語はないようです。
そこでここでは、「business」を広い意味で取って、business year としました。
できること
- 異なる種類の年度は異なる具象型になるため、型安全です。
- 指定した日付の年度を取得できます。
- 年度の開始日と終了日を取得できます。
-
Comparable
インターフェイスを実装しているため、同じ種類の年度同士でどちらが古いか比較できます。 -
OpenEndRange<LocalDate>
インターフェイスおよびClosedRange<LocalDate>
インターフェイスを実装しているため、ある日付がその年度の範囲に含まれているかを判定できます。 - 年度に年数を加減できます。
- 年度同士の差を取ることができます。
- インスタンス生成処理を別の型に切り出してあるため、ある年度オブジェクトを元に別の年度オブジェクトを生成する処理を、全ての種類の年度に共通で実装することができます。
API
import java.time.LocalDate
/**
* 年度。
*
* @param T 年度の具象型。
*/
interface BusinessYear<T : BusinessYear<T>> : Comparable<T>, OpenEndRange<LocalDate>, ClosedRange<LocalDate> {
/** 年度の値 */
val value: Int
/** この年度の具象型 [T] を補助するオブジェクト */
val companion: BusinessYearCompanion<T>
// region オーバーライド
override fun compareTo(other: T): Int =
this.value.compareTo(other.value)
/** 年度の開始日 */
override val start: LocalDate
get() = companion.startOf(value)
/** 年度の終了日(この日を含まない) */
override val endExclusive: LocalDate
get() =
// 次の年度の開始日
companion.startOf(value + 1)
/** 年度の終了日(この日を含む) */
override val endInclusive: LocalDate
get() =
// 次の年度の開始日の前日
endExclusive.minusDays(1)
override fun contains(value: LocalDate): Boolean =
super<OpenEndRange>.contains(value)
override fun isEmpty(): Boolean =
super<OpenEndRange>.isEmpty()
// endregion オーバーライド
}
/**
* 年度の具象型を補助する型。
*
* @param T 年度の具象型。
*/
interface BusinessYearCompanion<T : BusinessYear<T>> {
/**
* 指定された年度の開始日を返す。
*/
fun startOf(businessYearValue: Int): LocalDate
/**
* 指定された年度のオブジェクトを返す。
*/
fun create(businessYearValue: Int): T
}
/**
* 指定された年数を加算して、この年度のコピーを返す。
*/
operator fun <T : BusinessYear<T>> T.plus(years: Int): T =
companion.create(value + years)
/**
* 指定された年数を減算して、この年度のコピーを返す。
*/
operator fun <T : BusinessYear<T>> T.minus(years: Int): T =
companion.create(value - years)
/**
* 2つの年度の差の年数を返す。
*/
operator fun <T : BusinessYear<T>> T.minus(other: T): Int =
this.value - other.value
/**
* 指定された日付を含む年度オブジェクトを返す。
*
* @param T 年度の具象型。
* @param C 年度の具象型 [T] を補助する型。
*/
fun <T : BusinessYear<T>, C : BusinessYearCompanion<T>> C.at(date: LocalDate): T {
val companion = this
val businessYearValue =
if (startOf(date.year) >= date) {
// 開始日が date と同じかそれより前の最初の年度
(date.year downTo Int.MIN_VALUE)
.first { startOf(it) <= date }
} else {
// 開始日が date より後の最初の年度
val nextBusinessYear = (date.year + 1..Int.MAX_VALUE)
.first { startOf(it) > date }
nextBusinessYear - 1
}
return companion.create(businessYearValue)
}
使い方
日本の学校年度を扱いたい場合であれば、日本の学校年度を表す具象型を次のように実装します。
/**
* 日本の学校年度。
*
* その年の4月1日から始まる。
*/
// データクラスとして実装するとよい。
data class JpSchoolYear(
override val value: Int,
) :
// 年度の具象型として this の型を指定する。
BusinessYear<JpSchoolYear> {
override val companion: BusinessYearCompanion<JpSchoolYear>
get() =
// コンパニオンオブジェクトを返す。
JpSchoolYear
// BusinessYearCompanion インターフェイスをコンパニオンオブジェクトとして実装するとよい。
companion object : BusinessYearCompanion<JpSchoolYear> {
override fun startOf(businessYearValue: Int): LocalDate =
// その年の4月1日から始まる。
LocalDate.of(businessYearValue, 4, 1)
override fun create(businessYearValue: Int): JpSchoolYear =
JpSchoolYear(businessYearValue)
}
}
アメリカ合衆国連邦政府の会計年度であれば次のようになります。
日本の学校年度の実装とは少し変えてみました。
/**
* アメリカ合衆国連邦政府の会計年度。
*
* 前の年の10月1日から始まる。
*/
// インラインクラスにするものあり。
@JvmInline
value class UsFederalGovernmentFiscalYear(
override val value: Int,
) : BusinessYear<UsFederalGovernmentFiscalYear> {
override val companion: BusinessYearCompanion<UsFederalGovernmentFiscalYear>
get() = UsFederalGovernmentFiscalYear
// データクラスでない場合はオーバーライドすべき。
override fun toString(): String =
"${UsFederalGovernmentFiscalYear::class.java.simpleName}(value=$value)"
companion object : BusinessYearCompanion<UsFederalGovernmentFiscalYear> {
// 月日が固定なら定数定義しておいてもよい。
val startMonthDay = MonthDay.of(10, 1)
override fun startOf(businessYearValue: Int): LocalDate =
// 前の年の10月1日から始まる。
startMonthDay.atYear(businessYearValue - 1)
override fun create(businessYearValue: Int): UsFederalGovernmentFiscalYear =
UsFederalGovernmentFiscalYear(businessYearValue)
}
}
このように使えます。
val jpSchoolYear2000 = JpSchoolYear(2000)
println("日本の学校年度での 2000 年度は ${jpSchoolYear2000.start} から ${jpSchoolYear2000.endInclusive} までです。")
val jpSchoolYearAt20000331 = JpSchoolYear.at(LocalDate.of(2000, 3, 31))
println("2000-03-31 は日本の学校年度での ${jpSchoolYearAt20000331.value} 年度です。")
val jpSchoolYearAt20000401 = JpSchoolYear.at(LocalDate.of(2000, 4, 1))
println("2000-04-01 は日本の学校年度での ${jpSchoolYearAt20000401.value} 年度です。")
val usFY2000 = UsFederalGovernmentFiscalYear(2000)
println("アメリカ合衆国連邦政府の会計年度での 2000 年度は ${usFY2000.start} から ${usFY2000.endInclusive} までです。")
val usFYAt19990930 = UsFederalGovernmentFiscalYear.at(LocalDate.of(1999, 9, 30))
println("1999-09-30 はアメリカ合衆国連邦政府の会計年度での ${usFYAt19990930.value} 年度です。")
val usFYAt199901001 = UsFederalGovernmentFiscalYear.at(LocalDate.of(1999, 10, 1))
println("1999-10-01 はアメリカ合衆国連邦政府の会計年度での ${usFYAt199901001.value} 年度です。")
上記を実行すると次のように出力されます。
日本の学校年度での 2000 年度は 2000-04-01 から 2001-03-31 までです。
2000-03-31 は日本の学校年度での 1999 年度です。
2000-04-01 は日本の学校年度での 2000 年度です。
アメリカ合衆国連邦政府の会計年度での 2000 年度は 1999-10-01 から 2000-09-30 までです。
1999-09-30 はアメリカ合衆国連邦政府の会計年度での 1999 年度です。
1999-10-01 はアメリカ合衆国連邦政府の会計年度での 2000 年度です。
/以上