获取经过指定时期后的下一个有效日期,并相应调整月份的天数。

huangapple go评论70阅读模式
英文:

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

问题

我有一个枚举看起来像这样的:

enum Period{DAY, WEEK, MONTH, YEAR}

我需要的是一个函数,它将给定的 Period 按照指定的次数添加到“今天”日期上,同时设置日期的天数,使其等于起始日期(如果结果是有效的)。

或者也许这样理解更容易:
想象一下你每个月的31号(适用的话)会收到薪水。该函数返回下一个有效日期(从今天开始),您将在该日期收到下一笔薪水。函数可以区分是每天、每周、每月还是每年,并在指定的间隔内考虑如何频繁发放。它还会处理无效日期。

让我们看一个例子:

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 (这是今天之后的下一个有效日期)

我猜年份比较简单(好吧,至少有一个边缘情况):
getNextDate(28.02.2020, YEAR, 1) -> 28.02.2021
getNextDate(29.02.2020, YEAR, 1) -> 28.02.2021 <- 边缘情况,因为2020年是闰年
getNextDate(29.02.2020, YEAR, 4) -> 29.02.2024 <- 闰年到闰年

对于每周和每天,没有边缘情况,对吗?
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 (与 DAY,14 相同)

重要:如果今天已经是支付日,那么今天就是解决方案
getNextDate(03.09.2020, MONTH, 1) -> 03.09.2020 (在这里没有变化,日期与今天匹配)
*/
}

我实际上更喜欢使用现代的 LocalDate API(目前只有旧的日期对象作为输入,但以后会改变)

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

用我的更新

//这是提到的枚举的一个方法
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() 也有效,但对于边缘情况不会返回有效日期。例如 31.06.2020, MONTH,1 会立即卡在每月的30号,即使该月有31天也不会跳回。对于2020年9月30日是正确的。但对于2020年10月31日则不是。

英文:

I have an enum that looks like this

enum Period{DAY, WEEK, MONTH, YEAR}

What i need is a function that adds a specified amout of times the given Period to today while setting the day of month so that it is equal to the start date (if the outcome is valid).

Or maybe it is easier to understand like this:
Imagine you get your salary on the 31st every month (where applicable). The function returns the next valid date (from today) when you will receive your next salary. Where the function can distinguish if you get it Daily, Weekly, Monthly, Yearly and how often in the specified interval.
It also takes care of invalid dates

Lets have a look at an example:

public static Date getNextDate(Date startDate, Period period, int times){
/* 
Examples:
getNextDate(31.08.2020, MONTH, 1) -&gt; 30.09.2020
getNextDate(31.08.2020, MONTH, 2) -&gt; 31.10.2020
getNextDate(30.05.2020, MONTH, 2) -&gt; 30.09.2020
getNextDate(30.06.2020, MONTH, 2) -&gt; 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) -&gt; 28.02.2021
getNextDate(29.02.2020, YEAR, 1) -&gt; 28.02.2021 &lt;- Edge case, as 2020 is a gap year
getNextDate(29.02.2020, YEAR, 4) -&gt; 29.02.2024 &lt;- gap year to gap year

For weeks and days there are no edge cases, are there?
getNextDate(29.02.2020, DAY, 1) -&gt; 03.09.2020
getNextDate(29.02.2020, DAY, 3) -&gt; 05.09.2020
getNextDate(29.02.2020, WEEK, 2) -&gt; 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) -&gt; 03.09.2020 (No change here, the date matches today)
*/
}

I actually would prefer to use the modern LocalDate API (Just the input is an old date object at the moment, but will be changed later)

I hope i did not forget any edge cases.

Update with what i did

//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 ) &amp;&amp; !baseDate.after(today)){
            baseDate= getNextDate(baseDate,1);
        }
        return startHere;
    }

My getNextDate() Method works. The getNextDateAfterToday() also works, but does not return valid dates for edge cases. Example 31.06.2020, MONTH,1 would immediatly be stuc at 30st of every month and never skip back even if the month has 31 days. For 30.09.2020 it would be correct. But for 31.10.2020 it wouldn't

答案1

得分: 2

我终于找到了一个方法(尽管它似乎过于复杂,超出了我真正想要实现的目标)。我将我的 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,但将来会这样做。
这个方法适用于我上面发布的所有测试案例。虽然我希望我没有漏掉关键的案例。

英文:

I finally figured a way (although it seems way, way, way to complicated for what i really wanted to achieve). I changed my getNextDateAfterTodayto this

public Date getNextValidFutureDate(Date entryDate, Date startDate, int specific) {
        Date result = new Date(startDate.getTime());
        while (!result.equals(entryDate) &amp;&amp; !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() &lt; ldStart.getDayOfMonth() &amp;&amp; this != DAY &amp;&amp; this != WEEK &amp;&amp; this != YEAR) {
            if (ldResult.lengthOfMonth() &gt;= 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());
    }

I did not change the other method to use LocalDate, but will do this in the future.
This works with all test cases i posted above. Though i hope i did not miss essential ones

答案2

得分: 1

[<strike>不使用那个写得糟糕、通常不安全且使用起来很痛苦的十年历史的日期 API,可能是个不错的主意。</strike>](https://stackoverflow.com/questions/28730136/should-i-use-java-util-date-or-switch-to-java-time-localdate) 在这里,使用 `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(2020, 9, 3);
    LocalDate nextDate = getNextDate(startDate, Period.ofDays(20)); // 2020-09-23

或者干脆不使用您的辅助函数,直接这样使用:

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

<strike>Not using the decade old date api which is badly written and generally unsafe and painful to use might be the best idea.</strike> Using java.time might be in your favor here. Changing your method signature to this, is all you'd have to do:

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

...

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

And can then be called like:

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

Or simply dropping your helper function in the first place and using it directly:

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

答案3

得分: 1

> …尽管它看起来相当、相当、相当复杂,超出了我想要实现的目标…

你自己的解决方案还不错。但我无法放弃这个挑战,所以我来试试。我认为我的方法会稍微简单一些。

我完全采用了java.time,现代的Java日期和时间API。我也跳过了你的Period枚举,因为预定义的ChronoUnit枚举已经满足了需求。只是它还包括了小时、分钟和其他在这里没有意义的单位,所以我们需要排除这些。

Date类设计得很糟糕,而且早已过时。如果可能的话,应该避免使用它(如果你不能避免使用它,我会在最后给出解决方案)。

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)) {
        // 计算我们需要添加 times 个单位以获取未来日期(或今天)的次数。
        // 我们需要向上取整;这样做的技巧是计算到昨天,然后加 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

我应该说,它与你在问题中给出的示例是一致的。

如果你无法避免使用老式的Date对象和自己的Period枚举实例,或者你必须要回到老式的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);
}
英文:

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

Your own solution is not bad. I just couldn’t let the challenge rest, so here’s my go. I believe it’s a little bit simpler.

I am going all-in on java.time, the modern Java date and time API. I also skipped your Period enum since the predefined ChronoUnit enum fulfils the purpose. Only it also includes hours, minutes and other units that don’t make sense here, so we need to reject those.

The Date class is poorly designed as well as long outdated. Avoid it if you can (if you cannot avoid it, I am giving you the solution in the end).

public static LocalDate getNextDate(LocalDate startDate, TemporalUnit period, int times) {
	if (! period.isDateBased()) {
		throw new IllegalArgumentException(&quot;Cannot add &quot; + period + &quot; to a date&quot;);
	}
	
	LocalDate today = LocalDate.now(ZoneId.of(&quot;America/Eirunepe&quot;));
	
	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;
	}
}

For demonstrating the method I am using this little utility method:

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

Now let’s see:

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

When running just now, the output was:

> 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

I should say that it agrees with your examples from the question.

If you cannot avoid having an old-fashioned Date object and an instance of your own Period enum and/or you indispensably need an old-fashioned Date back, you may wrap my method into one that performs the necessary conversions. First I would extend your enum to know its corresponding ChronoUnit constants:

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

Now a wrapper method may look like this;

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

答案4

得分: 0

你可以使用 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();
}

其中,period 是在 Calendar 类中定义的整数,你可以像下面这样调用你的函数:

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,也是可以的!

英文:

You can use the class Calendar to resolve your problem like that :

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

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

The period is an int defined in the Calendar class, you can call your function like that :

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

If you realy need to use your enum, you can do it !

huangapple
  • 本文由 发表于 2020年9月3日 20:58:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/63724192.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定