きり丸の技術日記

技術検証したり、資格等をここに残していきます。

JavaでLocalDateとLocalDateTimeを元に月末日を求める

年と月のパラメータから月末日を求めるYearMonth型の記事を書きました。しかし、個人的に時刻を扱うときはLocalDate, LocalDateTimeのどちらかで扱いたいです。

今回の記事では、LocalDate, LocalDateTimeを元に月末日を求めます。

環境

  • Java
    • 15

前提条件

なし。

ゴール

  • LocalDateから月末日を求める
  • LocalDateTimeから月末日を求める

月末日を求める

withメソッドとTemporalAdjusters.lastDayOfMonthメソッドを組み合わせます。

withメソッドはパラメータの新しいインスタンスを作成します。例えば、withMonthに1を渡すと、1月のインスタンスが返却されます。withメソッドにTemporalAdjusters.lastDayOfMonthメソッドを指定すると、閏年を考慮した月末日のインスタンスになります。

LocalDateLocalDateTimewithメソッドを持ったTemporalインターフェースを継承していますので、どちらも同様に使うことができます。

// LocalDate
@Test
void test_01() {
  assertThat(
    LocalDate.of(2020, 1, 15).with(TemporalAdjusters.lastDayOfMonth())
  ).isEqualTo(LocalDate.of(2020, 1, 31));
}

// LocalDateTime
@Test
void test_05() {
  assertThat(
    LocalDateTime.of(2021, 2, 15, 1, 1).with(TemporalAdjusters.lastDayOfMonth())
  ).isEqualTo(LocalDateTime.of(2021, 2, 28, 1, 1));
}

インスタンスが月末であることを確認する

+1日した結果、月跨ぎが発生して1日になれば月末とみてよいでしょう。

LocalDate.of(2021, 2, 28).plusDays(1).getDayOfMonth() == 1;

こちらを紹介した理由としては、withメソッドを使うよりは誤差ですが早く済むからです。10億回実行して、最も早く処理をすることができました。認知できない程度の速度差なので、お好きなやり方を使用してください。

  1. 20ミリ秒
    • +1日したら月跨ぎが発生
  2. 40ミリ秒
    • LocalDateTimeでwithメソッドを使う
  3. 60ミリ秒
    • LocalDateでwithメソッドを使う
  4. 65ミリ秒
    • LocalDateをYearMonthにキャストする

ソースコード

テストコード(性能確認も含む): github.com

終わりに

個人的に普段から使う型での月末を求めることができて良かったです。


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

f:id:nainaistar:20210512181109p:plain

Javaで年と月を元に月末日を求める(YearMonth)

Javaにて年と月から、その年の月末日を求める方法をメモとして残します。

環境

  • Java
    • 15

前提条件

  • パラメータは年と月のみ
    • 「2021-05」等の1つにまとまったパラメータも含む

ゴール

  • 閏年も含めて、月末日を求めることができる

YearMonth型

Java8からjava.time.YearMonthクラスが提供されております。クラス名の通り、YearとMonthを扱います。

インスタンスの生成

複数ありますが、使い勝手のいい2つのメソッドを紹介します。

ofメソッド

// intの年とintのmonthから生成する方法
YearMonth.of(int year, int month);

YearMonth.of(2020, 2);

parseメソッド

デフォルトのDateTimeFormatteryyyy-MMとなっています。(※正確には、年は4桁から10桁まで許容しています)

// Stringの年月から生成する方法
YearMonth.parse(CharSequence text);
YearMonth.parse(CharSequence text, DateTimeFormatter dateTimeFormatter);


YearMonth.parse("2020-02");
YearMonth.parse("202_0-02"); // Java7からの数値表現
YearMonth.parse("2020/02", DateTimeFormatter.of("yyyy/MM"));

月末日を求める方法

atEndOfMonthメソッドを使用します。このメソッドの戻り値が指定した年の月末日のLocalDateクラスとなります。

@Test
void test_01() {
    assertThat(
        YearMonth.of(2020, 1).atEndOfMonth()
    ).isEqualTo(LocalDate.of(2020, 1, 31));
}

もし、日付だけ取得したい場合は、LocalDateクラスのgetDayOfMonthメソッドを呼ぶことで取得することができます。

@Test
void test_01_02() {
    assertThat(
        YearMonth.of(2020, 1).atEndOfMonth().getDayOfMonth()
    ).isEqualTo(31);
}

ソースコード

テストコード: github.com

終わりに

YearMonthクラスは特化しているだけあって意図が分かりやすいですね。

月末日を求める方法は複数あるので、YearMonthクラスを意図的に使うことは少ないかもしれませんが、メソッド名等を直感的に使えます。ぜひこの記事を元に使ってみてください。


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

f:id:nainaistar:20210511082344p:plain

Javaで法律に従った暦による期間計算(日割りにならない1ヵ月を求める)

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

当記事では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でのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

類似記事