TemporalAdjustersLocalDateのようなjava.timeのオブジェクトを扱っていると、「その月の第2火曜日」を求めるような複雑な日付の調整が必要になることがよくあります。
これを手作業で計算する代わりに、**TemporalAdjusters**を使うことができます。
java.time.temporal.TemporalAdjusters(複数形の's'が付いていることに注意してください)というユーティリティクラスには、よく使われる日付調整のためのstaticメソッドが多数用意されています。
これらの調整は、LocalDateのような時間的オブジェクトに対して.with()メソッドを呼び出すことで適用します。
with(TemporalAdjuster)を使うと_新しい_インスタンスが返り、既存のインスタンスは更新されません。java.timeのオブジェクトは不変だからです。
import java.time.LocalDate;
import java.time.Month;
import java.time.DayOfWeek;
import java.time.temporal.TemporalAdjusters;
LocalDate date = LocalDate.of(2024, Month.AUGUST, 15); // 2024-08-15 (a Thursday)
// Use a predefined adjuster: find the last day of the month
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
// => 2024-08-31
スケジュールの計算に役立つアジャスターを、いくつか見てみましょう。
dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek)を使うと、ある曜日がその月の何回目にあたるかを求められます。
// Find the second Tuesday of August 2024
LocalDate secondTuesday = date.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY));
// => 2024-08-13
firstInMonth(DayOfWeek dayOfWeek)またはlastInMonth(DayOfWeek dayOfWeek)を使います。
// Find the last Monday of August 2024
LocalDate lastMonday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.MONDAY));
// => 2024-08-26
next()、previous()、nextOrSame()、previousOrSame()などのアジャスターは、現在の日付を基準にして、指定した曜日が次または前に現れる日を求めます。
next()/previous():現在の日付が該当する場合は含めません。nextOrSame()/previousOrSame():現在の日付が該当する場合は含めます。LocalDate thursday = LocalDate.of(2024, Month.AUGUST, 15); // A Thursday
// Find the next Thursday (exclusive)
LocalDate nextThursday = thursday.with(TemporalAdjusters.next(DayOfWeek.THURSDAY));
// => 2024-08-22
// Find the next Thursday (inclusive) - returns the same date
LocalDate nextOrSameThursday = thursday.with(TemporalAdjusters.nextOrSame(DayOfWeek.THURSDAY));
// => 2024-08-15
// Find the previous Monday (exclusive)
LocalDate previousMonday = thursday.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
// => 2024-08-12
TemporalAdjusterは関数型インターフェースなので、調整対象の時間的オブジェクトを受け取り、調整後のオブジェクトを返すラムダとして、独自のアジャスターを書くことができます。
定義済みのアジャスターと同じように.with()で適用でき、その内部で定義済みのアジャスターを使うこともできます。
import java.time.temporal.TemporalAdjuster;
// An adjuster that moves a weekend date to the following Monday
TemporalAdjuster skipWeekend = temporal -> {
DayOfWeek day = DayOfWeek.from(temporal);
if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
return temporal.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
}
return temporal;
};
LocalDate saturday = LocalDate.of(2024, Month.AUGUST, 17);
LocalDate monday = saturday.with(skipWeekend);
// => 2024-08-19
TemporalAdjustersクラスのこれらのアジャスターを使えば、日付を操作する作業を大幅にシンプルにできます。
Booking Up For Beautyの演習で手伝ったニューヨークの美容室は順調に営業していて、常連のお客さんがたくさんできました。 常連のお客さんは、毎回の来店を個別に予約するのではなく、「第2火曜日」「毎月最後の金曜日」といった決まった取り決めを持っています。 オーナーから、そうした取り決めが指す日付と、サロン自身の毎月の決まった予定の日付を計算してくれるヘルパーを作ってほしいと頼まれました。
年、月、第何回目か(1が1回目、2が2回目、以下同様)、曜日を引数に取るSalonCalendar.standingAppointment()メソッドを実装します。
このメソッドは、その月の中で指定した曜日がその回数目に来る日付を返します。
SalonCalendar calendar = new SalonCalendar();
calendar.standingAppointment(2024, Month.AUGUST, 2, DayOfWeek.TUESDAY);
// => LocalDate.of(2024, 8, 13)
常連のお客さんの中には、その月の最後に来る自分の曜日を好む人もいます。
年、月、曜日を引数に取り、その月の中で指定した曜日が最後に来る日付を返すSalonCalendar.lastAppointment()メソッドを実装します。
SalonCalendar calendar = new SalonCalendar();
calendar.lastAppointment(2024, Month.AUGUST, DayOfWeek.FRIDAY);
// => LocalDate.of(2024, 8, 30)
受付のお客さんが「次の土曜日」を希望しています。
その日がすでに土曜日なら、その当日のことです。
日付と曜日を引数に取り、指定した日付以降で最初にその曜日になる日付を返すSalonCalendar.nextAvailable()メソッドを実装します。
SalonCalendar calendar = new SalonCalendar();
calendar.nextAvailable(LocalDate.of(2024, 8, 15), DayOfWeek.SATURDAY);
// => LocalDate.of(2024, 8, 17)
calendar.nextAvailable(LocalDate.of(2024, 8, 17), DayOfWeek.SATURDAY);
// => LocalDate.of(2024, 8, 17)
サロンは毎月末日に帳簿を締めます。
ただし週末は休業なので、月末が土曜日か日曜日になる場合は、その前の金曜日に帳簿を締めます。
年と月を引数に取り、その月の帳簿締めの日を返すSalonCalendar.bookkeepingDay()メソッドを実装します。
SalonCalendar calendar = new SalonCalendar();
calendar.bookkeepingDay(2024, Month.SEPTEMBER);
// => LocalDate.of(2024, 9, 30)
calendar.bookkeepingDay(2024, Month.AUGUST);
// => LocalDate.of(2024, 8, 30)
スタッフへの給料は毎月15日に支払われ、同じ週末のルールが適用されます。15日が土曜日か日曜日なら、給料日はその前の金曜日です。 同じルールをもう一度書く代わりに、再利用できるアジャスターにしましょう。
平日はそのままにし、土曜日か日曜日を前の金曜日に移動させるTemporalAdjusterを返すSalonCalendar.precedingWorkday()メソッドを実装します。
次に、そのアジャスターを使って、年と月を引数に取るSalonCalendar.payday()メソッドを実装します。
SalonCalendar calendar = new SalonCalendar();
LocalDate.of(2024, 6, 15).with(calendar.precedingWorkday());
// => LocalDate.of(2024, 6, 14)
calendar.payday(2024, Month.JUNE);
// => LocalDate.of(2024, 6, 14)
calendar.payday(2024, Month.AUGUST);
// => LocalDate.of(2024, 8, 15)
bookkeepingDay()に戻って、このアジャスターを使ってみてもかまいません。