轨道
/
Java
Java
/
练习
/
美容院常客
美容院常客

美容院常客

学习练习

简介

时间调整器

处理 LocalDate 这类 java.time 对象时,经常需要做复杂的日期调整,比如找出“某个月的第二个星期二”。这时不必手动计算,可以用 TemporalAdjusters。

java.time.temporal.TemporalAdjusters(注意结尾的复数 s)是一个工具类,为许多常见的日期调整提供了静态方法。你可以在一个时间对象(比如 LocalDate)上通过 .with() 方法应用这些调整。

Note

使用 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 类提供的这些调整器,日期处理任务可以大大简化。

说明

你在“为美而预约”练习里帮过忙的那家纽约美容院,如今经营得很不错,很多客户都成了常客。 常客们不再每次到店都单独预约,而是有固定的安排:“每隔一个星期二”“每月最后一个星期五”,等等。 店主想请你写一个辅助工具,算出这些安排对应的日期,以及美容院自己每月例行事务的日期。

1. 求出常客的固定预约日

实现 SalonCalendar.standingAppointment() 方法,它接收年份、月份、第几次出现(1 表示第一次,2 表示第二次,以此类推)和一个星期几。 该方法返回该月内这个星期几第几次出现的那一天:

SalonCalendar calendar = new SalonCalendar();
calendar.standingAppointment(2024, Month.AUGUST, 2, DayOfWeek.TUESDAY);
// => LocalDate.of(2024, 8, 13)

2. 求出常客的月末预约日

有些常客更喜欢当月自己那个星期几的最后一次。 实现 SalonCalendar.lastAppointment() 方法,它接收年份、月份和一个星期几,返回该月内这个星期几最后一次出现的那一天:

SalonCalendar calendar = new SalonCalendar();
calendar.lastAppointment(2024, Month.AUGUST, DayOfWeek.FRIDAY);
// => LocalDate.of(2024, 8, 30)

3. 求出散客下一个可预约的日子

前台来了位客户,想要“下一个星期六”。 如果客户询问的当天就是星期六,那指的就是当天。 实现 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)

4. 求出结账日

美容院在每月最后一天结账。 不过美容院周末不营业,所以当月末最后一天是星期六或星期日时,就提前到之前的那个星期五结账。 实现 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)

5. 把周末规则复用到发薪日上

员工的薪水在每月 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() 里用上这个调整器。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Java Exercism

准备好开始 美容院常客 了吗?

注册 Exercism,借助 27 个概念159 个练习 和真人导师指导,学习并掌握 Java,全部免费。