民法第143条が定める暦による期間の計算を実装してみる

[KINTO Technologies Advent Calendar 2021 - Qiita](https://qiita.com/advent-calendar/2021/kinto-technologies) の20日目の記事です。

## 概要
当社が企画/開発を行う、トヨタ車のサブスク「KINTO」は[月々定額のサブスクリプションサービス](https://kinto-jp.com/kinto_one/)となり、1ヶ月単位のご利用料の期間は民法第143条が定める[「暦による期間」に従って](https://kinto-jp.com/kinto_one/payment/)計算されています。

この「暦による期間」の計算、ちょっと複雑であり、なんかコード書いてみたいなという気分になったため書いてみる次第です。

## 民法第143条が定める「暦による期間」の計算とは？
[WIKIBOOKSによると](https://ja.wikibooks.org/wiki/%E6%B0%91%E6%B3%95%E7%AC%AC143%E6%9D%A1)、民法第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日`


コードにすると以下の通りです。期間終了日はケースに応じた計算をしたいため、計算結果を外から関数で渡すことにしました。

```java
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. 起算日が月の初日
    - 期間終了日は必ず期間開始日がある月の末日
1. 起算日に応当する日が最後の月にない
    - 期間終了日は最後の月の末日
1. 起算日に応当する日が最後の月にある
    - 期間終了日は応当する日 - 1日


```java
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  |  |
|...  |...  |...  |  |

### 実行結果
条件満たせてそうです

```起算日が月の初日.sh
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
```

```起算日に応当する日が最後の月にない.sh
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
```

```起算日に応当する日が最後の月にある.sh
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
```

## まとめ
当初想定していたより、考慮しないといけないケースが多く、勉強になりました。

今回実装したソースコードはこちらにあります。

- https://github.com/ryous0411/cop4j


## さいごに
当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
[KINTO Technologies コーポレートサイト](https://www.kinto-technologies.com/)