KINTO Technologies Advent Calendar 2021 - Qiita の20日目の記事です。
概要
当社が企画/開発を行う、トヨタ車のサブスク「KINTO」は月々定額のサブスクリプションサービスとなり、1ヶ月単位のご利用料の期間は民法第143条が定める「暦による期間」に従って計算されています。
この「暦による期間」の計算、ちょっと複雑であり、なんかコード書いてみたいなという気分になったため書いてみる次第です。
民法第143条が定める「暦による期間」の計算とは?
WIKIBOOKSによると、民法第143条の条文は以下のとおりです。
第143条の条文
- 週、月又は年によって期間を定めたときは、その期間は、暦に従って計算する。
- 週、月又は年の初めから期間を起算しないときは、その期間は、最後の週、月又は年においてその起算日に応当する日の前日に満了する。ただし、月又は年によって期間を定めた場合において、最後の月に応当する日がないときは、その月の末日に満了する。
1行目はいいとして、2行目については文字数が多すぎて頭に入ってこないですね。
条文を理解する
週、月又は年の初めから期間を起算しないときは、その期間は、最後の週、月又は年においてその起算日に応当する日の前日に満了する。
月でいうと起算日が11/21だった場合、応当する日は12/21になるってことですかね。「応当する日の前日に満了する」とあるので満了日は21 - 1 = 20
-> 12/20 ということになりそうです。単純っすね。
しかし起算日が月の初日だった場合、満了日は応当する日 - 1日 = 月末
になるため、月によって30日になったり31日になったりと日付が変動しそうです。
ただし、月又は年によって期間を定めた場合において、最後の月に応当する日がないときは、その月の末日に満了する。
月でいうと起算日が3/31だった場合、応当する日は4/31日か... 思いっきり存在しない日付になりましたね。「最後の月に応当する日がないときは、その月の末日に満了」なので満了日は4月の末日である4/30になりそうです。うるう年があることなどを考えるとちょっとめんどくさそうです。
ここまででなんとなく内容は理解できたので実装してみましょー!
実装
以下のような内容で実装していきたいと思います。今回はJava11を使います。
- 与えられた起算日、満了日から、暦に従って計算された1ヶ月単位の期間の一覧を作成する
- 1ヶ月単位の期間については起算日から見て何ヶ月目か・期間開始日・期間終了日で構成する
こんな感じですかね。
- 起算日:2021/3/15
- 満了日:2022/3/14
起算日から見て何ヶ月目か | 期間開始日 | 期間終了日 |
---|---|---|
1 | 2021/3/15 | 2021/4/14 |
2 | 2021/4/15 | 2021/5/14 |
... | ... | ... |
11 | 2022/1/15 | 2022/2/14 |
12 | 2022/2/15 | 2022/3/14 |
期間計算のベース部分
最後の月に応当する日があるケース・ないケースなどがあるため、期間終了日はケースに応じた計算が必要そうです。しかし、期間開始日については以下とすることで、都度複雑な計算をしなくてよさそうです。
起算日から見て1ヶ月目(初月の場合)は起算日、それ以外の場合は前月の期間終了日 + 1日
コードにすると以下の通りです。期間終了日はケースに応じた計算をしたいため、計算結果を外から関数で渡すことにしました。
private List<MonthlyPeriod> calcMonthlyPeriods(UnaryOperator<LocalDate> endDateFunc) {
List<MonthlyPeriod> calculatedPeriods = new ArrayList<>();
for (int i = 0; i < numberOfMonths; i++) {
var startDate = i == 0 ? this.from : calculatedPeriods.get(i - 1).getTo().plusDays(1);
var endDate = endDateFunc.apply(startDate);
calculatedPeriods.add(
new MonthlyPeriod(
i + 1,
startDate,
endDate
)
);
}
return calculatedPeriods;
}
期間終了日のケースに応じた計算
期間終了日の計算について、以下の3通りに分けてみました
- 起算日が月の初日
- 期間終了日は必ず期間開始日がある月の末日
- 起算日に応当する日が最後の月にない
- 期間終了日は最後の月の末日
- 起算日に応当する日が最後の月にある
- 期間終了日は応当する日 - 1日
private List<MonthlyPeriod> calcMonthlyPeriods() {
if (from.getDayOfMonth() == 1) {
// 1.
return calcMonthlyPeriods(
monthlyPeriodFrom -> monthlyPeriodFrom.withDayOfMonth(monthlyPeriodFrom.lengthOfMonth())
);
} else if (from.getDayOfMonth() >= 29) {
// 2.
return calcMonthlyPeriods(
monthlyPeriodFrom -> {
var lastMonth = monthlyPeriodFrom.getDayOfMonth() == 1
? monthlyPeriodFrom
: monthlyPeriodFrom.plusMonths(1);
return lastMonth
.withDayOfMonth(Math.min(to.getDayOfMonth(), lastMonth.lengthOfMonth()));
}
);
}
// 3.
return calcMonthlyPeriods(
monthlyPeriodFrom -> monthlyPeriodFrom.plusMonths(1).minusDays(1)
);
}
1.
、3.
について特記することはないですが、2.
については期間開始日が月の初日に変動してしまうケースがあるため、それを考慮した内容にしています。
起算日から見て何ヶ月目か | 期間開始日 | 期間終了日 | 備考 |
---|---|---|---|
1 | 2021/1/31 | 2021/2/28 | 応当する日がないため、月の末日が期間終了日 |
2 | 2021/3/1 | 2021/3/30 | 前月の期間終了日 + 1日から開始のため、月の初日になる |
3 | 2021/3/31 | 2021/4/30 | |
... | ... | ... |
実行結果
条件満たせてそうです
2021-01-01 2021-06-30
> Task :Main.main()
1
2021-01-01
2021-01-31
2
2021-02-01
2021-02-28
3
2021-03-01
2021-03-31
4
2021-04-01
2021-04-30
5
2021-05-01
2021-05-31
6
2021-06-01
2021-06-30
2021-01-31 2021-07-30
> Task :Main.main()
1
2021-01-31
2021-02-28
2
2021-03-01
2021-03-30
3
2021-03-31
2021-04-30
4
2021-05-01
2021-05-30
5
2021-05-31
2021-06-30
6
2021-07-01
2021-07-30
2021-01-15 2021-07-14
> Task :Main.main()
1
2021-01-15
2021-02-14
2
2021-02-15
2021-03-14
3
2021-03-15
2021-04-14
4
2021-04-15
2021-05-14
5
2021-05-15
2021-06-14
6
2021-06-15
2021-07-14
まとめ
当初想定していたより、考慮しないといけないケースが多く、勉強になりました。
今回実装したソースコードはこちらにあります。
さいごに
当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
KINTO Technologies コーポレートサイト