PAYATHON 2026

使用 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));

具体过程如下:

  1. atEndOfMonth() 返回当月最后一天。
  2. previousOrSame(FRIDAY) 从这一天开始向前查找星期五。
  3. 如果当月最后一天正好是星期五,则直接返回当天。

也可以写成:

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);

备注:内容仅供参考。