きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

Javaで法律に従った暦による期間計算

以前、法律に従った暦上の計算方法を記事にしました。

当記事ではJavaで法律に従った暦による期間の計算を求める方法を記します。

環境

  • Java
    • 15

ユースケース

月次で契約するサブスクリプションサービスを実施しています。中途半端な日に解約されてしまうと、日割計算が発生しかねません。しかし、複数のサービスを取り扱っており、サービスごとの日割計算を取り扱うのは障害が発生しかねないので取り扱いたくありません。

ですので、キッチリ1ヵ月分の料金を請求できるように、日割が発生しない・1ヵ月分の料金を請求できる日付だけで解約できるようにしたい。


1日外出録ハンチョウ(1)の第5話 柿放みたいなイメージがわかりやすいですかね…。

法律(民法)上の暦による期間計算

実務の友様の良いページがありましたので引用させていただきます。

場合 算出されるべき満了日 根拠 具体例
月の初日から起算する場合 最終月の末日 民法143条2項本文 1月1日から起算して2か月は,平年なら2月28日,閏年なら2月29日が満了日。1月1日から起算して3か月は,3月31日が満了日
月の途中から起算し,最終月に応当日のある場合 最終月の応当日の前日 民法143条2項本文 1月20日から起算して2か月は,3月19日が満了日。1月31日から起算して2か月は,3月30日が満了日
月の途中から起算し,最終月に応当日のない場合 最終月の末日 民法143条2項但書 1月31日から起算して1か月は,平年なら2月28日,閏年なら2月29日が満了日。3月31日から起算して1か月は,4月30日が満了日

実装

ユースケースに従って、起算日契約日満了日解約日と表現します。

閏年や暦月の処理を考えるとLocalDateで処理をすることが一番簡単だったので、LocalDateを使用します。

public class Contract {
  /**
   * 契約日
   */
  private final LocalDate contractDate;
  /**
   * 解約日
   */
  private final LocalDate expireDate;
}

月の初日から起算する場合は最終月の末日

「初日 = 1日」なので、契約日の日付が1日であることを確認します。

「末日 = +1日すると翌月の1日になる日」、と読み替えることができるのでplusDays(1)で1日を追加します。

if (contractDate.getDayOfMonth() == 1) {
    if (expireDate.plusDays(1).getDayOfMonth() == 1) {
    return true;
    }
}

月の途中から起算し,最終月に応当日のある場合は、最終月の応当日の前日

応当日の前日なので、言葉通りに受け取ると「契約日の日付 -1 = 解約日の日付」になります。

しかし、契約日が初日の場合、-1日をしてしまうと先月によって28-31のどの日付になるかわかりません。「契約日の日付 = 解約日の日付+1日」で処理しましょう。

minusDays(1)を使わずに-1する方法もありますが、個人的にはコードに意図を持たせられるようにminusDays(1)plusDays(1)を使う方が好みです。

    if (contractDate.getDayOfMonth() == expireDate.plusDays(1).getDayOfMonth()) {
      return true;
    }

    // minusDays(1) ではなく、単純に-1する場合は正しく動きます。
    // if (contractDate.getDayOfMonth() -1 == expireDate.getDayOfMonth()) {
    //   return true;
    // }

月の途中から起算し,最終月に応当日のない場合は、最終月の末日

応当日が存在しない、ということは契約日の日付解約日の日付よりも大きいと読み替えることができます。

その後、解約日が月の末日であることをチェックしましょう。

    if (contractDate.getDayOfMonth() > expireDate.getDayOfMonth()) {
      if (expireDate.plusDays(1).getDayOfMonth() == 1) {
        return true;
      }
    }
  }

全体図

すべての条件を合算するとこのようになります。

リファクタリングの余地はありますが、総合的な条件が複雑なので、大掛かりなリファクタリングは止めておいた方が良いでしょう。強いて言えば、各条件(コメント)ごとにメソッドを切り出すのはいいと思います。

  public boolean canExpire() {
    // 月の初日から起算する場合は、最終月の末日
    if (contractDate.getDayOfMonth() == 1) {
      if (expireDate.plusDays(1).getDayOfMonth() == 1) {
        return true;
      }
    }
    // 月の途中から起算し,最終月に応当日のある場合は、最終月の応当日の前日
    // -1日すると、月を跨ぐ可能性があるのでNG
    if (contractDate.getDayOfMonth() == expireDate.plusDays(1).getDayOfMonth()) {
      return true;
    }

    // 月の途中から起算し,最終月に応当日のない場合は、最終月の末日
    if (contractDate.getDayOfMonth() > expireDate.getDayOfMonth()) {
      if (expireDate.plusDays(1).getDayOfMonth() == 1) {
        return true;
      }
    }

    return false;
  }

ソースコード

ソースコード: github.com

テストコード: github.com

終わりに

正直、簡単に作成できると考えていましたが、実際は閏年や応当日が無いパターンをどう表現していいかわからず、8時間くらいかかって作成しました。

分解してしまえば簡単ですが、それなりに難易度が高いので、TDDの題材とするのも面白そうです。

別の言語だと閏年が関連する日付の扱いをよく知らないので、ちょっとブログのネタに困ったときにGoとかTSとかRustやPython等でコーディングした結果を載せようと思いました。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

関連

暦上の期間計算の定義

nainaistar.hatenablog.com

f:id:nainaistar:20210502153858p:plain