システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

Interfaceのdefaultで多重継承(日時編)

コードの全量のリンクをこちらの記事に書いているので、先読みで全量を見たい方はこちらを参照してください。

vermeer.hatenablog.jp

今回はInterfaceのdefaultを使った「数値編」を拡張した「単位編」です。
Interfaceのdefaultではなくて、Interfaceを使った実装編になります。

何が嬉しいの?

期間とか比較とか時間に関する操作をインスタンスメソッドで扱えるとUtilsの内容を知らなくても利用側は使えるのでちょっと嬉しいかな?というくらいの嬉しさです。

とくに期間についてはChronoUnitを使った方が良いケースとPeriodを使った方が良いケースと、やりたいことのイメージは似ているのに使用するクラスが違うというようなものを統一して扱いたいということを満たしてくれます。

基底となるInterface

日付(‘LocalDate‘)、日時(LocalDateTime)はあえて分けました。

数値でBigDecimalに寄せたように最も制度の細かい LocalDateTimeに寄せることも考えたのですが、後述の DateUnaryOperatorの操作を行うことを鑑みて分けることにしました。

プリミティブ的な型への変換を基底Interfaceの操作として定義します。

日本のアプリケーションを前提としているのでdefaultで指定しています。
具象クラスで

UnixTimeへの変換は、以前 フロントへ日時関連の情報を返却するときに「フロントで任意で表記変換をしたいのでUnixTimeの方が嬉しい」という話を受けたことがあったので*1

LocalDateを拡張

Dateへの変換はミドルウェアによってはLocalDateではなくDateというケースがあったので。

public interface NullableDateType<T> extends SinglePropertyObjectType<LocalDate> {

  /**
   * プロパティがNullであるか判定をします.
   *
   * @return nullの場合はtrue
   */
  default boolean isEmpty() {
    return this.getNullableValue().isEmpty();
  }

  /**
   * 保持する日時に適用するタイムゾーンIDを返却します.
   *
   * @return タイムゾーンID
   */
  default ZoneId getZoneId() {
    return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
  }

  /**
   * プロパティ値をDateへ変換します.
   *
   * @return Data型インスタンス
   */
  default Optional<Date> toDate() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    return Optional.of(Date.from(
            this.getNullableValue().get().atStartOfDay(this.getZoneId()).toInstant()));
  }

  /**
   * プロパティ値をLocalDateTimeへ変換します.
   *
   * @return LocalDateTime型インスタンス
   */
  default Optional<LocalDateTime> toLocalDateTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    return Optional.of(this.getNullableValue().get().atStartOfDay(this.getZoneId()).toLocalDateTime());
  }

  /**
   * プロパティ値をUnixTimeへ変換します.
   *
   * @return UnixTime
   */
  default Optional<Long> toUnixTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    var zonedDateTime = ZonedDateTime.of(this.getNullableValue().get().atStartOfDay(), this.getZoneId());
    return Optional.of(zonedDateTime.toEpochSecond());
  }
}

LocalDateTimeを拡張

public interface NullableDateTimeType<T> extends SinglePropertyObjectType<LocalDateTime> {

  /**
   * プロパティがNullであるか判定をします.
   *
   * @return nullの場合はtrue
   */
  default boolean isEmpty() {
    return this.getNullableValue().isEmpty();
  }

  /**
   * 保持する日時に適用するタイムゾーンIDを返却します.
   *
   * @return タイムゾーンID
   */
  default ZoneId getZoneId() {
    return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
  }

  /**
   * プロパティ値をLocalDateへ変換して返却します.
   *
   * @return LocalDate
   */
  default Optional<LocalDate> toLocalDate() {
    if (this.getNullableValue().isEmpty()) {
      return Optional.empty();
    }

    var dateTime = this.getNullableValue().get();
    return Optional.of(LocalDate.of(dateTime.getYear(), dateTime.getMonth(), dateTime.getDayOfMonth()));
  }

  /**
   * プロパティ値をUnixTimeへ変換します.
   *
   * @return UnixTime
   */
  default Optional<Long> toUnixTime() {
    if (this.isEmpty()) {
      return Optional.empty();
    }
    var zonedDateTime = ZonedDateTime.of(this.getNullableValue().get(), this.getZoneId());
    return Optional.of(zonedDateTime.toEpochSecond());
  }

}

編集汎用

LocalDateLocalDateTimeの日付操作は使い勝手が良いので、そのまま使うのが良いと思います。

LocalDateを拡張

public interface DateUnaryOperator<T> extends NullableDateType<T>, SingleArgumentNewInstance<LocalDate, T> {

  /**
   * callbackを用いて保持している値を編集して新しいインスタンスを生成します.
   *
   * @param callback コールバック関数
   * @return 編集後の新しいインスタンス.
   */
  default T apply(UnaryOperator<LocalDate> callback) {
    LocalDate updated = callback.apply(this.getNullableValue().orElse(null));
    return this.newInstance(updated);
  }
}

実装例としてテスト
見てもらえれば分かるのですが、plusDaysは既に分かりやすいし使いやすいので単純な委譲相当で良さそうです。
強調の意味もあって ブロックで実装していますが 通常はブロックは使わなくて良いくらいシンプルです。

public class DateUnaryOperatorTest {

  @Test
  public void testApply() {
    var impl = new DateUnaryOperatorImpl(LocalDate.of(2024, 1, 1));
    var actual1 = impl.apply(value -> {
      return value;
    });

    Assertions.assertEquals(impl.getNullableValue().get(), actual1.getNullableValue().get());
    Assertions.assertTrue(impl != actual1);

    var actual2 = impl.apply(value -> {
      return value.plusDays(1);
    });

    Assertions.assertNotEquals(impl.getNullableValue().get(), actual2.getNullableValue().get());
    Assertions.assertEquals(2, actual2.getNullableValue().get().getDayOfMonth());

  }

  public static class DateUnaryOperatorImpl implements DateUnaryOperator<DateUnaryOperatorImpl> {

    private LocalDate value;

    public DateUnaryOperatorImpl(LocalDate value) {
      this.value = value;
    }

    @Override
    public Optional<LocalDate> getNullableValue() {
      return Optional.ofNullable(value);
    }
  }

}

LocalDateTimeを拡張

public interface DateTimeUnaryOperator<T> extends NullableDateTimeType<T>,
        SingleArgumentNewInstance<LocalDateTime, T> {

  /**
   * callbackを用いて保持している値を編集して新しいインスタンスを生成します.
   *
   * @param callback コールバック関数
   * @return 編集後の新しいインスタンス.
   */
  default T apply(UnaryOperator<LocalDateTime> callback) {
    LocalDateTime updated = callback.apply(this.getNullableValue().orElse(null));
    return this.newInstance(updated);
  }
}

実装例としてテスト

public class DateTimeUnaryOperatorTest {

  @Test
  public void testApply() {
    var impl = new DateTimeUnaryOperatorImpl(LocalDateTime.of(2024, 1, 1, 2, 3));
    var actual1 = impl.apply(value -> {
      return value;
    });

    Assertions.assertEquals(impl.getNullableValue().get(), actual1.getNullableValue().get());
    Assertions.assertTrue(impl != actual1);

    var actual2 = impl.apply(value -> {
      return value.plusDays(1);
    });

    Assertions.assertNotEquals(impl.getNullableValue().get(), actual2.getNullableValue().get());
    Assertions.assertEquals(2, actual2.getNullableValue().get().getDayOfMonth());
  }

  public static class DateTimeUnaryOperatorImpl implements DateTimeUnaryOperator<DateTimeUnaryOperatorImpl> {

    private LocalDateTime value;

    public DateTimeUnaryOperatorImpl(LocalDateTime value) {
      this.value = value;
    }

    @Override
    public Optional<LocalDateTime> getNullableValue() {
      return Optional.ofNullable(value);
    }
  }

}

期間日数

LocalDateを拡張

public interface DateDaysRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間日数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeDays(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return count;
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 期間日数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeDays(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの日数のドメインオブジェクトを生成する関数
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 日数を扱うドメインオブジェクト
   */
  default <R> R rangeDays(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = ChronoUnit.DAYS.between(this.getNullableValue().get(), after.getNullableValue().get());
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeDaysRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間日数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeDays(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return count;
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 期間日数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeDays(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との日数の差異を返却します.
   *
   * @param <R> 日数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの日数のドメインオブジェクトを生成する関数
   * @param function 日数を扱うドメインオブジェクトを生成する関数
   * @return 日数を扱うドメインオブジェクト
   */
  default <R> R rangeDays(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = ChronoUnit.DAYS.between(
            this.getNullableValue().get().toLocalDate(),
            after.getNullableValue().get().toLocalDate());
    return function.apply(count);
  }
}

期間月数

ちょっとしたものとして、以前の開発で月を3分割にして刻んだ表現が欲しいというものがあったので それを作りました。
かなり特定のドメインに寄ったものなように思うのですが、まぁこういうのも

LocalDateを拡張

public interface DateMonthsRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間月数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeMonths(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    Long count = period.toTotalMonths();
    return count;
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonths(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    Long count = this.rangeMonths(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト
   */
  default <R> R rangeMonths(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    Long count = this.rangeMonths(after);
    return function.apply(count);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 月数(0.5単位での月数)
   */
  default BigDecimal rangeMonthsHalfUp(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return BigDecimal.ZERO;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    var monthCount = period.toTotalMonths();
    var dayCount = period.minusMonths(monthCount).getDays();

    var halfUpDay = new BigDecimal(dayCount)
            .divide(new BigDecimal(30), 2, RoundingMode.HALF_UP)
            .multiply(new BigDecimal(2)).setScale(0, RoundingMode.HALF_UP)
            .divide(new BigDecimal(2));

    return new BigDecimal(monthCount).add(halfUpDay);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト(0.5単位での月数). thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonthsHalfUp(T after, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト(0.5単位での月数)
   */
  default <R> R rangeMonthsHalfUp(T after, Supplier<R> defaultSupplier, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeMonthsRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間月数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default Long rangeMonths(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0L;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return count;
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonths(T after, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト
   */
  default <R> R rangeMonths(T after, Supplier<R> defaultSupplier, Function<Long, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    Long count = period.toTotalMonths();
    return function.apply(count);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param after 指定日
   * @return 月数(0.5単位での月数)
   */
  default BigDecimal rangeMonthsHalfUp(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return BigDecimal.ZERO;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    var monthCount = period.toTotalMonths();
    var dayCount = period.minusMonths(monthCount).getDays();

    var halfUpDay = new BigDecimal(dayCount)
            .divide(new BigDecimal(30), 2, RoundingMode.HALF_UP)
            .multiply(new BigDecimal(2)).setScale(0, RoundingMode.HALF_UP)
            .divide(new BigDecimal(2));

    return new BigDecimal(monthCount).add(halfUpDay);
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 期間月数を扱うドメインオブジェクト(0.5単位での月数). thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeMonthsHalfUp(T after, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との月数の差異を返却します.
   *
   * @param <R> 月数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの月数のドメインオブジェクトを生成する関数
   * @param function 月数を扱うドメインオブジェクトを生成する関数
   * @return 月数を扱うドメインオブジェクト(0.5単位での月数)
   */
  default <R> R rangeMonthsHalfUp(T after, Supplier<R> defaultSupplier, Function<BigDecimal, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    BigDecimal count = this.rangeMonthsHalfUp(after);
    return function.apply(count);
  }

}

期間年数

LocalDateを拡張

public interface DateYearsRange<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間年数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default int rangeYears(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0;
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return count;
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 期間年数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeYears(T after, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの年数のドメインオブジェクトを生成する関数
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 年数を扱うドメインオブジェクト
   */
  default <R> R rangeYears(T after, Supplier<R> defaultSupplier, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get(), after.getNullableValue().get());
    int count = period.getYears();
    return function.apply(count);
  }
}

LocalDateTimeを拡張

public interface DateTimeYearsRange<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param after 指定日
   * @return 期間年数. thisまたはafterのプロパティ値がnullの場合は0
   */
  default int rangeYears(T after) {
    if (this.isEmpty() || after.isEmpty()) {
      return 0;
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return count;
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 期間年数を扱うドメインオブジェクト. thisまたはafterのプロパティ値がnullの場合はOptional.empty()
   */
  default <R> Optional<R> rangeYears(T after, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return Optional.empty();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return Optional.ofNullable(function.apply(count));
  }

  /**
   * 指定日との年数の差異を返却します.
   *
   * @param <R> 年数を扱うドメインオブジェクトの型
   * @param after 指定日
   * @param defaultSupplier thisまたはafterのプロパティ値がnullの場合のデフォルトの年数のドメインオブジェクトを生成する関数
   * @param function 年数を扱うドメインオブジェクトを生成する関数
   * @return 年数を扱うドメインオブジェクト
   */
  default <R> R rangeYears(T after, Supplier<R> defaultSupplier, Function<Integer, R> function) {
    if (this.isEmpty() || after.isEmpty()) {
      return defaultSupplier.get();
    }
    var period = Period.between(this.getNullableValue().get().toLocalDate(), after.getNullableValue().get().toLocalDate());
    int count = period.getYears();
    return function.apply(count);
  }
}

日付比較

isAfterとかisBeforeよりも、数直線的な比較記述(less than的な記述)の方が境界が分かりやすいので数値比較と同じ操作の属性表現として揃えました。
やったこととしてはそれくらいです。

LocalDateを拡張

public interface DateComparator<T extends NullableDateType<T>> extends NullableDateType<T> {

  /**
   * equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this = other} を満たす場合 true
   */
  default boolean eq(T other) {
    if (this.getNullableValue().isEmpty() && other.getNullableValue().isEmpty()) {
      return true;
    }

    if (this.getNullableValue().isEmpty() || other.getNullableValue().isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isEqual(other.getNullableValue().get());
  }

  /**
   * not equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this != other} を満たす場合 true
   */
  default boolean ne(T other) {
    return !this.eq(other);
  }

  /**
   * less than.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this < other} を満たす場合 true
   */
  default boolean lt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return false;
    }

    if (this.isEmpty()) {
      return true;
    }

    return this.getNullableValue().get().isBefore(other.getNullableValue().get());
  }

  /**
   * less than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this <= other} を満たす場合 true
   */
  default boolean le(T other) {
    return this.eq(other) || this.lt(other);
  }

  /**
   * greater than.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this > other} を満たす場合 true
   */
  default boolean gt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return true;
    }

    if (this.isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isAfter(other.getNullableValue().get());
  }

  /**
   * greater than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日付相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this >= other} を満たす場合 true
   */
  default boolean ge(T other) {
    return this.eq(other) || this.gt(other);
  }

}

LocalDateTimeを拡張

public interface DateTimeComparator<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T> {

  /**
   * equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this = other} を満たす場合 true
   */
  default boolean eq(T other) {
    if (this.getNullableValue().isEmpty() && other.getNullableValue().isEmpty()) {
      return true;
    }

    if (this.getNullableValue().isEmpty() || other.getNullableValue().isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isEqual(other.getNullableValue().get());
  }

  /**
   * not equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this != other} を満たす場合 true
   */
  default boolean ne(T other) {
    return !this.eq(other);
  }

  /**
   * less than.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this < other} を満たす場合 true
   */
  default boolean lt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return false;
    }

    if (this.isEmpty()) {
      return true;
    }

    return this.getNullableValue().get().isBefore(other.getNullableValue().get());
  }

  /**
   * less than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this <= other} を満たす場合 true
   */
  default boolean le(T other) {
    return this.eq(other) || this.lt(other);
  }

  /**
   * greater than.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this > other} を満たす場合 true
   */
  default boolean gt(T other) {
    if (this.eq(other)) {
      return false;
    }

    if (other.isEmpty()) {
      return true;
    }

    if (this.isEmpty()) {
      return false;
    }

    return this.getNullableValue().get().isAfter(other.getNullableValue().get());
  }

  /**
   * greater than or equal to.
   * <p>
   * 保持している値がnullの場合は最も小さい日時相当で比較
   * </p>
   *
   * @param other 比較対象
   * @return {@code this >= other} を満たす場合 true
   */
  default boolean ge(T other) {
    return this.eq(other) || this.gt(other);
  }

}

日付補正

LocalDateを拡張

月初日、月末日にシフトしたインスタンスを生成します。

public interface DateMonthsShift<T extends NullableDateType<T>> extends NullableDateType<T>,
        NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDate, T> {

  /**
   * 月初日補正をしたインスタンスを返却します.
   *
   * @return 月初日補正をしたインスタンス
   */
  default T beginMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDate updated = this.getNullableValue().get().withDayOfMonth(1);
    return this.newInstance(updated);
  }

  /**
   * 月末日補正をしたインスタンスを返却します.
   *
   * @return 月末日補正をしたインスタンス
   */
  default T endMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var date = this.getNullableValue().get();
    var updated = this.getNullableValue().get().withDayOfMonth(date.lengthOfMonth());
    return this.newInstance(updated);
  }

}

LocalDateTimeを拡張

月初日、月末日にシフトしたインスタンスを生成します。

public interface DateTimeMonthsShift<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T>,
        NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDateTime, T> {

  /**
   * 月初日補正をしたインスタンスを返却します.
   *
   * @return 月初日補正をしたインスタンス
   */
  default T beginMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDateTime updated = this.getNullableValue().get().toLocalDate().atStartOfDay().withDayOfMonth(1);
    return this.newInstance(updated);
  }

  /**
   * 月末日補正をしたインスタンスを返却します.
   *
   * @return 月末日補正をしたインスタンス
   */
  default T endMonth() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var date = this.getNullableValue().get().toLocalDate().plusMonths(1L)
            .atStartOfDay().withDayOfMonth(1).minusNanos(1L);
    return this.newInstance(date);
  }

}

1日の開始時刻および終了時刻にシフトしたインスタンスを生成します。

public interface DateTimeDaysShift<T extends NullableDateTimeType<T>> extends NullableDateTimeType<T>,
         NoArgumentNewInstance<T>, SingleArgumentNewInstance<LocalDateTime, T> {

  /**
   * 1日の開始時刻補正をしたインスタンスを返却します.
   *
   * @return 開始時刻補正をしたインスタンス
   */
  default T beginDay() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    LocalDateTime updated = this.getNullableValue().get().toLocalDate().atStartOfDay();
    return this.newInstance(updated);
  }

  /**
   * 1日の終了時刻補正をしたインスタンスを返却します.
   *
   * @return 終了時刻補正をしたインスタンス
   */
  default T endDay() {
    if (this.isEmpty()) {
      return this.newInstance();
    }
    var updated = this.getNullableValue().get().toLocalDate().plusDays(1).atStartOfDay().minusNanos(1);
    return this.newInstance(updated);
  }

}

さいごに

期間取得に微妙な違いがあったりと、やってみて分かったことがありました。
Interfaceのdefaultを使ったとはいえ、実質Utilsの再発明的なことをしたわけですが、こういうもの素振りとして色々と学びがあって良いものですね。

もう少し、こういうのもやってみようかな?というのがあったりしますが それはまた別の機会にしようと思います。

*1:タイムゾーンのところは、正式に(?)使う時は、ちゃんと確認した方が良いかもしれない