处理 LocalDate 这类 java.time 对象时,经常需要做复杂的日期调整,比如找出“某个月的第二个星期二”。这时不必手动计算,可以用 TemporalAdjusters。
java.time.temporal.TemporalAdjusters(注意结尾的复数 s)是一个工具类,为许多常见的日期调整提供了静态方法。你可以在一个时间对象(比如 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) 可以找出某个月中第 n 次出现的某个星期几。
// 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 是一个函数式接口,所以你可以用 lambda 写一个自己的调整器:它接收要调整的时间对象,返回调整后的对象。用法和预定义调整器一样,也是通过 .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 类提供的这些调整器,日期处理任务可以大大简化。
你在“为美而预约”练习里帮过忙的那家纽约美容院,如今经营得很不错,很多客户都成了常客。 常客们不再每次到店都单独预约,而是有固定的安排:“每隔一个星期二”“每月最后一个星期五”,等等。 店主想请你写一个辅助工具,算出这些安排对应的日期,以及美容院自己每月例行事务的日期。
实现 SalonCalendar.standingAppointment() 方法,它接收年份、月份、第几次出现(1 表示第一次,2 表示第二次,以此类推)和一个星期几。
该方法返回该月内这个星期几第几次出现的那一天:
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 号是星期六或星期日时,发薪日提前到之前的那个星期五。 与其把这条规则再写一遍,不如把它做成一个可复用的调整器。
实现 SalonCalendar.precedingWorkday() 方法,它返回一个 TemporalAdjuster:工作日保持不变,星期六或星期日则往前移到前一个星期五。
然后实现 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() 里用上这个调整器。