コードの全量のリンクをこちらの記事に書いているので、先読みで全量を見たい方はこちらを参照してください。
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
default boolean isEmpty() {
return this.getNullableValue().isEmpty();
}
保持する日時に適用するタイムゾーンIDを返却します.
@return
default ZoneId getZoneId() {
return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
}
プロパティ値をDateへ変換します.
@return
default Optional<Date> toDate() {
if (this.isEmpty()) {
return Optional.empty();
}
return Optional.of(Date.from(
this.getNullableValue().get().atStartOfDay(this.getZoneId()).toInstant()));
}
プロパティ値をLocalDateTimeへ変換します.
@return
default Optional<LocalDateTime> toLocalDateTime() {
if (this.isEmpty()) {
return Optional.empty();
}
return Optional.of(this.getNullableValue().get().atStartOfDay(this.getZoneId()).toLocalDateTime());
}
プロパティ値をUnixTimeへ変換します.
@return
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
default boolean isEmpty() {
return this.getNullableValue().isEmpty();
}
保持する日時に適用するタイムゾーンIDを返却します.
@return
default ZoneId getZoneId() {
return TimeZone.getTimeZone("Asia/Tokyo").toZoneId();
}
プロパティ値をLocalDateへ変換して返却します.
@return
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
default Optional<Long> toUnixTime() {
if (this.isEmpty()) {
return Optional.empty();
}
var zonedDateTime = ZonedDateTime.of(this.getNullableValue().get(), this.getZoneId());
return Optional.of(zonedDateTime.toEpochSecond());
}
}
編集汎用
LocalDate
やLocalDateTime
の日付操作は使い勝手が良いので、そのまま使うのが良いと思います。
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
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
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
@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
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
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
@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
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
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
@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
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
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
@param function
@return
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
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
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
@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
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
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
@param function
@return
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
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
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
@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
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
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
@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>
</p>
@param other
@return{@code this = other}
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>
</p>
@param other
@return{@code this != other}
default boolean ne(T other) {
return !this.eq(other);
}
less than.
<p>
</p>
@param other
@return{@code this < other}
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>
</p>
@param other
@return{@code this <= other}
default boolean le(T other) {
return this.eq(other) || this.lt(other);
}
greater than.
<p>
</p>
@param other
@return{@code this > other}
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>
</p>
@param other
@return{@code this >= other}
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>
</p>
@param other
@return{@code this = other}
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>
</p>
@param other
@return{@code this != other}
default boolean ne(T other) {
return !this.eq(other);
}
less than.
<p>
</p>
@param other
@return{@code this < other}
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>
</p>
@param other
@return{@code this <= other}
default boolean le(T other) {
return this.eq(other) || this.lt(other);
}
greater than.
<p>
</p>
@param other
@return{@code this > other}
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>
</p>
@param other
@return{@code this >= other}
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の再発明的なことをしたわけですが、こういうもの素振りとして色々と学びがあって良いものですね。
もう少し、こういうのもやってみようかな?というのがあったりしますが それはまた別の機会にしようと思います。