使用 Java 8 获取每月最后一个 Friday 的付款日期列表
支付老李
结论
Java 8 可以用 YearMonth 按月遍历,再通过 TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) 计算每个月的最后一个星期五。只需将合同日期范围内的付款日加入 List<LocalDate>。
推荐实现
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
public class PaymentDates {
public static void main(String[] args) {
String contractBegin = "01-01-2018";
String contractEnd = "31-12-2018";
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd-MM-yyyy");
LocalDate contractStart =
LocalDate.parse(contractBegin, formatter);
LocalDate contractStop =
LocalDate.parse(contractEnd, formatter);
if (contractStart.isAfter(contractStop)) {
throw new IllegalArgumentException(
"合同开始日期不能晚于结束日期"
);
}
List<LocalDate> payChecks = new ArrayList<>();
YearMonth currentMonth = YearMonth.from(contractStart);
YearMonth endMonth = YearMonth.from(contractStop);
while (!currentMonth.isAfter(endMonth)) {
LocalDate paymentDate = currentMonth
.atEndOfMonth()
.with(TemporalAdjusters.previousOrSame(
DayOfWeek.FRIDAY
));
if (!paymentDate.isBefore(contractStart)
&& !paymentDate.isAfter(contractStop)) {
payChecks.add(paymentDate);
}
currentMonth = currentMonth.plusMonths(1);
}
payChecks.forEach(date ->
System.out.println(date.format(formatter))
);
}
}
运行结果如下:
26-01-2018
23-02-2018
30-03-2018
27-04-2018
25-05-2018
29-06-2018
27-07-2018
31-08-2018
28-09-2018
26-10-2018
30-11-2018
28-12-2018
计算方式
YearMonth 只包含年份和月份,用它按月遍历时,无需手动判断每个月有多少天。
下面的表达式先找到当月最后一天,再向前查找最近的星期五:
currentMonth.atEndOfMonth()
.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY));
具体过程如下:
atEndOfMonth()返回当月最后一天。previousOrSame(FRIDAY)从这一天开始向前查找星期五。- 如果当月最后一天正好是星期五,则直接返回当天。
也可以写成:
LocalDate paymentDate = currentMonth
.atDay(1)
.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
两种写法的结果相同。
Java 8 Stream 写法
如果想使用 Stream,可以先算出合同覆盖的月份数:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("dd-MM-yyyy");
LocalDate contractStart =
LocalDate.parse("01-01-2018", formatter);
LocalDate contractStop =
LocalDate.parse("31-12-2018", formatter);
if (contractStart.isAfter(contractStop)) {
throw new IllegalArgumentException("合同开始日期不能晚于结束日期");
}
YearMonth startMonth = YearMonth.from(contractStart);
YearMonth stopMonth = YearMonth.from(contractStop);
long monthCount =
ChronoUnit.MONTHS.between(startMonth, stopMonth) + 1;
List<LocalDate> payChecks = Stream
.iterate(startMonth, month -> month.plusMonths(1))
.limit(monthCount)
.map(month -> month.atDay(1).with(
TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)
))
.filter(date -> !date.isBefore(contractStart))
.filter(date -> !date.isAfter(contractStop))
.collect(Collectors.toList());
这种按月计算比较简单,普通的 while 循环通常更容易理解,Stream 写法也没有明显的性能优势。
注意事项
不要使用原代码中的 args.length 控制循环。args 存放的是程序的命令行参数,和合同包含的月份数无关。
列表应声明明确的泛型类型:
List<LocalDate> payChecks = new ArrayList<>();
当合同不是从月初开始,或不是在月末结束时,日期范围过滤就会影响结果。例如,合同从 29-01-2018 开始,而一月份最后一个星期五是 26-01-2018,那么一月份不会生成付款日期,因为该日期早于合同开始日期。
当前实现要求付款日期位于合同开始日期和结束日期之间,并将起止日期当天也算作有效日期。如果业务规则是只要合同覆盖某个月,就要生成该月最后一个星期五,可以删除以下范围过滤:
if (!paymentDate.isBefore(contractStart)
&& !paymentDate.isAfter(contractStop)) {
payChecks.add(paymentDate);
}
直接添加付款日期:
payChecks.add(paymentDate);
备注:内容仅供参考。
