获取指定时间段过后的下一个有效日期,并相应地调整月份中的第几天

Get the next valid date after a specified period has passed and adjust the day of month accordingly

我有一个像这样的枚举

enum Period{DAY, WEEK, MONTH, YEAR}

我需要的是一个函数,它在设置月份日期时将给定期间的指定次数添加到 today,以便它等于开始日期(如果结果有效)。

或者这样更容易理解: 假设您在每个月的 31 号(如适用)领取薪水。函数 return 是您将收到下一份薪水的下一个有效日期(从今天开始)。如果你在指定的时间间隔内每天、每周、每月、每年获取它,该功能可以区分在哪里。 它还处理无效日期

让我们看一个例子:

public static Date getNextDate(Date startDate, Period period, int times){
/* 
Examples:
getNextDate(31.08.2020, MONTH, 1) -> 30.09.2020
getNextDate(31.08.2020, MONTH, 2) -> 31.10.2020
getNextDate(30.05.2020, MONTH, 2) -> 30.09.2020
getNextDate(30.06.2020, MONTH, 2) -> 30.10.2020 (This is the next valid date after today)

Years are pretty simple i guess (Okay, there is at least one edge case):
getNextDate(28.02.2020, YEAR, 1) -> 28.02.2021
getNextDate(29.02.2020, YEAR, 1) -> 28.02.2021 <- Edge case, as 2020 is a gap year
getNextDate(29.02.2020, YEAR, 4) -> 29.02.2024 <- gap year to gap year

For weeks and days there are no edge cases, are there?
getNextDate(29.02.2020, DAY, 1) -> 03.09.2020
getNextDate(29.02.2020, DAY, 3) -> 05.09.2020
getNextDate(29.02.2020, WEEK, 2) -> 12.09.2020 (Same as DAY,14)

Important: If today is already a payment day, this already is the solution
getNextDate(03.09.2020, MONTH, 1) -> 03.09.2020 (No change here, the date matches today)
*/
}

我实际上更愿意使用现代的 LocalDate API(只是输入目前是旧日期对象,但稍后会更改)

我希望我没有忘记任何边缘情况。

更新我所做的

//This is a method of the enum mentioned
public Date getNextDate(Date baseDate, int specific) {
        Date result = null;
        switch (this) {
            case DAY:
                result = DateTimeUtils.addDays(baseDate, specific);
                break;
         
            case WEEK:
                result = DateTimeUtils.addWeeks(baseDate, specific);
                break;
          
            case MONTH:
                result = DateTimeUtils.addMonths(baseDate, specific);
                break;
            case YEAR:
                result = DateTimeUtils.addYears(baseDate, specific);
                break;
        }
        return result;
    }

public Date getNextDateAfterToday(Date baseDate) {
        today = new Date();
        while(!baseDate.equals(today ) && !baseDate.after(today)){
            baseDate= getNextDate(baseDate,1);
        }
        return startHere;
    }

我的getNextDate()方法有效。 getNextDateAfterToday() 也有效,但对边缘情况没有 return 有效日期。示例 31.06.2020, MONTH,1 将在每个月的 30 日立即执行,并且即使该月有 31 天也不会跳回。对于 2020 年 9 月 30 日,它是正确的。但是对于 2020 年 10 月 31 日,它不会

您可以使用 class Calendar 来解决您的问题:

public static Date getNextDate(Date startDate, int period, int times) {

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(startDate);
    calendar.add(period, times);
    return calendar.getTime();
}

周期是在日历 class 中定义的一个整数,您可以这样调用您的函数:

System.out.println(getNextDate(new Date(), Calendar.MONTH, 1));
System.out.println(getNextDate(new Date(), Calendar.MONTH, 3));
System.out.println(getNextDate(new Date(), Calendar.YEAR, 1));

如果你真的需要使用你的enum,你可以做到!

Not using the decade old date api which is badly written and generally unsafe and painful to use might be the best idea. 使用 java.time 可能对您有利。将您的方法签名更改为此,您只需要做的就是:

import java.time.LocalDate;
import java.time.Period;

...

public static LocalDate getNextDate(LocalDate startDate, Period period) {
    return startDate.plus(period);
}

然后可以这样调用:

LocalDate startDate = LocalDate.of(3, 9, 2020);
LocalDate nextDate = getNextDate(startDate, Period.ofDays(20)); // 2020-09-23

或者直接删除辅助函数并直接使用它:

LocalDate nextDate = startDate.plus(Period.ofDays(20));

我终于想出了一个办法(尽管对于我真正想要实现的目标来说,这看起来很复杂)。我把我的getNextDateAfterToday改成这个

public Date getNextValidFutureDate(Date entryDate, Date startDate, int specific) {
        Date result = new Date(startDate.getTime());
        while (!result.equals(entryDate) && !result.after(entryDate)) {
            result = getNextDate(result, true, specific);
        }
        LocalDate ldStart = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate ldResult = result.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

        if (ldResult.getDayOfMonth() < ldStart.getDayOfMonth() && this != DAY && this != WEEK && this != YEAR) {
            if (ldResult.lengthOfMonth() >= ldStart.getDayOfMonth()) {
                ldResult = ldResult.with(ChronoField.DAY_OF_MONTH, ldStart.getDayOfMonth());
            } else {
                ldResult = ldResult.with(ChronoField.DAY_OF_MONTH, ldResult.lengthOfMonth());
            }
        }
        return Date.from(ldResult.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }

我没有更改其他方法来使用 LocalDate,但将来会这样做。 这适用于我上面发布的所有测试用例。虽然我希望我没有错过重要的

… (although it seems way, way, way to complicated for what i really wanted to achieve) …

你自己的方案还不错。我只是不能让挑战休息,所以我开始了。我相信它会更简单一些。

我要去 all-in java.time,现代 Java 日期和时间 API。我还跳过了您的 Period 枚举,因为预定义的 ChronoUnit 枚举可以达到目的。只是它还包括小时、分钟等在这里没有意义的单位,所以我们需要拒绝这些。

Date class 设计不佳且已过时。能避免就避免(如果不能避免,最后给你解决方法)。

public static LocalDate getNextDate(LocalDate startDate, TemporalUnit period, int times) {
    if (! period.isDateBased()) {
        throw new IllegalArgumentException("Cannot add " + period + " to a date");
    }
    
    LocalDate today = LocalDate.now(ZoneId.of("America/Eirunepe"));
    
    if (startDate.isBefore(today)) {
        // Calculate how many times we need to add times units to get a future date (or today).
        // We need to round up; the trick for doing so is count until yesterday and add 1.
        LocalDate yesterday = today.minusDays(1);
        long timesToAdd = period.between(startDate, yesterday) / times + 1;
        return startDate.plus(timesToAdd * times, period);
    } else {
        return startDate;
    }
}

为了演示方法,我使用了这个实用的小方法:

public static void demo(LocalDate startDate, TemporalUnit period, int times) {
    LocalDate nextDate = getNextDate(startDate, period, times);
    System.out.format("getNextDate(%s, %s, %d) -> %s%n", startDate, period, times, nextDate);
}

现在让我们看看:

    demo(LocalDate.of(2020, Month.AUGUST, 31), ChronoUnit.MONTHS, 1);
    demo(LocalDate.of(2020, Month.AUGUST, 31), ChronoUnit.MONTHS, 2);
    demo(LocalDate.of(2020, Month.MAY, 30), ChronoUnit.MONTHS, 2);
    demo(LocalDate.of(2020, Month.JUNE, 30), ChronoUnit.MONTHS, 2);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.FEBRUARY, 28), ChronoUnit.YEARS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.YEARS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.YEARS, 4);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.DAYS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.DAYS, 3);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.WEEKS, 2);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.SEPTEMBER, 4), ChronoUnit.MONTHS, 1);

刚才运行时,输出为:

getNextDate(2020-08-31, Months, 1) -> 2020-09-30
getNextDate(2020-08-31, Months, 2) -> 2020-10-31
getNextDate(2020-05-30, Months, 2) -> 2020-09-30
getNextDate(2020-06-30, Months, 2) -> 2020-10-30

getNextDate(2020-02-28, Years, 1) -> 2021-02-28
getNextDate(2020-02-29, Years, 1) -> 2021-02-28
getNextDate(2020-02-29, Years, 4) -> 2024-02-29

getNextDate(2020-02-29, Days, 1) -> 2020-09-04
getNextDate(2020-02-29, Days, 3) -> 2020-09-05
getNextDate(2020-02-29, Weeks, 2) -> 2020-09-12

getNextDate(2020-09-04, Months, 1) -> 2020-09-04

我应该说它同意你在问题中的例子。

如果您无法避免拥有一个 old-fashioned Date 对象和一个您自己的 Period 枚举 and/or 的实例,那么您不可避免地需要一个 old-fashioned Date 回来,你可以将我的方法包装到一个执行必要转换的方法中。首先,我将扩展您的枚举以了解其对应的 ChronoUnit 常量:

enum Period {
    DAY(ChronoUnit.DAYS),
    WEEK(ChronoUnit.WEEKS),
    MONTH(ChronoUnit.MONTHS),
    YEAR(ChronoUnit.YEARS);

    private final ChronoUnit unit;
    
    private Period(ChronoUnit unit) {
        this.unit = unit;
    }

    public ChronoUnit getUnit() {
        return unit;
    }
}

现在包装器方法可能如下所示;

public static Date getNextDate(Date startDate, Period period, int times) {
    ZoneId zone = ZoneId.of("America/Eirunepe");
    LocalDate startLocalDate = startDate.toInstant().atZone(zone).toLocalDate();
    LocalDate nextDate = getNextDate(startLocalDate, period.getUnit(), times);
    Instant startOfDay = nextDate.atStartOfDay(zone).toInstant();
    return Date.from(startOfDay);
}