Java日期 – 闰年 – 奇怪行为

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

Java Date - Leap year - strange behavior

问题

以下是您提供的代码的翻译:

import org.apache.commons.lang.time.DateUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class Main {

    public static void main(String[] args) throws ParseException {
        String startDate = "30 Jan 2016";
        String endDate = "29 Jan 2017"; // 当前未使用此值
        SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy");

        Date start = formatter.parse(startDate);

        List<Item> items = new ArrayList<>();

        for (int i = 0; i < 12; i++) {
            Date end = DateUtils.addMonths(start, 1);
            end = DateUtils.addSeconds(end, -1);
            items.add(new Item(start, end));
            start = DateUtils.addMonths(start, 1);
        }

        items.forEach(item -> {
            System.out.println(item.getStart() + " ::::: " + item.getEnd() + " ::::: " + getDifferenceDays(item.getStart(), item.getEnd()));
        });

    }

    public static long getDifferenceDays(Date d1, Date d2) {
        return ChronoUnit.DAYS.between(d1.toInstant(), d2.toInstant());
    }
}
import java.util.Date;

public class Item {
    Date start;
    Date end;

    public Item(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return "Item{" +
                "start=" + start +
                ", end=" + end +
                '}';
    }
}

请注意,这是您提供的代码的翻译版本,其中包括了主类 Main 和辅助类 Item

英文:

So I've got this kind of strange problem with Java's Date.

The entry-point for this problem are 2 String dates startDate("30 Jan 2016") and endate ""29 Jan 2017".

The problem is a little bit more complex,but I've summed it up just to this case.

What I have to do is get an 1 year schedule exactly like the one from bellow starting with this data.

The table contains for each month: first day, last day, and days between those 2 days.

Expected results:

/**

  • Sat Jan 30 00:00:00 GMT 2016 - Sun Feb 28 23:59:59 GMT 2016 - 30

  • Mon Feb 29 00:00:00 GMT 2016 - Tue Mar 29 23:59:59 BST 2016 - 30

  • Wed Mar 30 00:00:00 BST 2016 - Fri Apr 29 23:59:59 BST 2016 - 31

  • Sat Apr 30 00:00:00 BST 2016 - Sun May 29 23:59:59 BST 2016 - 30

  • Mon May 30 00:00:00 BST 2016 - Wed Jun 29 23:59:59 BST 2016 - 31

  • Thu Jun 30 00:00:00 BST 2016 - Fri Jul 29 23:59:59 BST 2016 - 30

  • Sat Jul 30 00:00:00 BST 2016 - Mon Aug 29 23:59:59 BST 2016 - 31

  • Tue Aug 30 00:00:00 BST 2016 - Thu Sep 29 23:59:59 BST 2016 - 31

  • Fri Sep 30 00:00:00 BST 2016 - Sat Oct 29 23:59:59 BST 2016 - 30

  • Sun Oct 30 00:00:00 BST 2016 - Tue Nov 29 23:59:59 GMT 2016 - 31

  • Wed Nov 30 00:00:00 GMT 2016 - Thu Dec 29 23:59:59 GMT 2016 - 30

  • Fri Dec 30 00:00:00 GMT 2016 - Sun Jan 29 23:59:59 GMT 2017 - 31

*/

My results are

Sat Jan 30 00:00:00 EET 2016 ::::: Sun Feb 28 23:59:59 EET 2016 ::::: 29

Mon Feb 29 00:00:00 EET 2016 ::::: Mon Mar 28 23:59:59 EEST 2016 ::::: 28

Tue Mar 29 00:00:00 EEST 2016 ::::: Thu Apr 28 23:59:59 EEST 2016 ::::: 30

Fri Apr 29 00:00:00 EEST 2016 ::::: Sat May 28 23:59:59 EEST 2016 ::::: 29

Sun May 29 00:00:00 EEST 2016 ::::: Tue Jun 28 23:59:59 EEST 2016 ::::: 30

Wed Jun 29 00:00:00 EEST 2016 ::::: Thu Jul 28 23:59:59 EEST 2016 ::::: 29

Fri Jul 29 00:00:00 EEST 2016 ::::: Sun Aug 28 23:59:59 EEST 2016 ::::: 30

Mon Aug 29 00:00:00 EEST 2016 ::::: Wed Sep 28 23:59:59 EEST 2016 ::::: 30

Thu Sep 29 00:00:00 EEST 2016 ::::: Fri Oct 28 23:59:59 EEST 2016 ::::: 29

Sat Oct 29 00:00:00 EEST 2016 ::::: Mon Nov 28 23:59:59 EET 2016 ::::: 31

Tue Nov 29 00:00:00 EET 2016 ::::: Wed Dec 28 23:59:59 EET 2016 ::::: 29

Thu Dec 29 00:00:00 EET 2016 ::::: Sat Jan 28 23:59:59 EET 2017 ::::: 30

So I’m not sure if this happens because of the February month or because 2016 was a leap year that included 29th of February.

What am I missing? I have the same problem run multiple test cases and all others are OK, but this.

I've also tried to do this with Javas 8 LocalDate and LocalDateTime and I get the exactly same results.

Here is my code

import org.apache.commons.lang.time.DateUtils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


public class Main {

    public static void main(String[] args) throws ParseException {
        String startDate = &quot;30 Jan 2016&quot;;
        String endDate = &quot;29 Jan 2017&quot;; // current not using this ?!
        SimpleDateFormat formatter = new SimpleDateFormat(&quot;dd MMM yyyy&quot;);

        Date start = formatter.parse(startDate);

        List&lt;Item&gt; items = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; 12; i++) {
            Date end = DateUtils.addMonths(start, 1);
            end = DateUtils.addSeconds(end, -1);
            items.add(new Item(start, end));
            start = DateUtils.addMonths(start, 1);
        }

        items.forEach(item -&gt; {
            System.out.println(item.getStart() + &quot; ::::: &quot; + item.getEnd() + &quot; ::::: &quot; + getDifferenceDays(item.getStart(), item.getEnd()));
        });

    }

    public static long getDifferenceDays(Date d1, Date d2) {
        return ChronoUnit.DAYS.between(d1.toInstant(), d2.toInstant());
    }

}
  • Item Class
import java.util.Date;

public class Item {
    Date start;
    Date end;

    public Item(Date start, Date end) {
        this.start = start;
        this.end = end;
    }

    public Date getStart() {
        return start;
    }

    public void setStart(Date start) {
        this.start = start;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return &quot;Item{&quot; +
                &quot;start=&quot; + start +
                &quot;, end=&quot; + end +
                &#39;}&#39;;
    }
}

答案1

得分: 2

java.time

由于您可以使用现代的 Java 日期和时间 API - java.time,我建议您坚持使用该API,避免使用旧的类SimpleDateFormatDate。您也不再需要使用Apache的DateUtilsChronoUnit枚举来自java.time

DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("d MMM u", Locale.ENGLISH);

String startDateString = "30 Jan 2016";
LocalDate originalStartDate = LocalDate.parse(startDateString, dateFormatter);

for (int i = 0; i < 12; i++) {
    LocalDate startDate = originalStartDate.plusMonths(i);
    LocalDate nextStartDate = originalStartDate.plusMonths(i + 1);
    LocalDate endDate = nextStartDate.minusDays(1);
    long differenceDays = ChronoUnit.DAYS.between(startDate, nextStartDate);

    System.out.format("%s - %s : %d%n", startDate, endDate, differenceDays);
}

输出结果为:

>     2016-01-30 - 2016-02-28 : 30
>     2016-02-29 - 2016-03-29 : 30
>     2016-03-30 - 2016-04-29 : 31
>     2016-04-30 - 2016-05-29 : 30
>     2016-05-30 - 2016-06-29 : 31
>     2016-06-30 - 2016-07-29 : 30
>     2016-07-30 - 2016-08-29 : 31
>     2016-08-30 - 2016-09-29 : 31
>     2016-09-30 - 2016-10-29 : 30
>     2016-10-30 - 2016-11-29 : 31
>     2016-11-30 - 2016-12-29 : 30
>     2016-12-30 - 2017-01-29 : 31

代码中的问题

在您的代码中出现了一些意外结果的原因。

  1. 当您在1月30日添加一个月时,您会得到期望的2月29日。在非闰年中,您将得到2月28日。当您再次在2月29日添加一个月时,您将得到3月29日。如果您考虑一下,这是否令人惊讶?在非闰年中,您将得到3月28日,所以2016年作为闰年实际上帮助您更接近所需的结果。在我的代码中,我通过对原始的开始日期添加正确数量的月份来解决了这个问题,而不是在上一个开始日期的基础上添加一个月。

  2. 正如PeterMmm已经提到的,ChronoUnit.DAYS.between()计算完整的24小时天数,任何不足一天的时间都会被丢弃,甚至是23小时59分钟59秒。从2016年1月30日00:00:00 EET到2016年2月28日23:59:59 EET是30天23小时59分钟59秒,所以您得到的结果是30天。在我的代码中,我通过计算到下一个项目开始之前的天数来解决这个问题。

另外一点:从2016年2月29日00:00:00 EET到2016年3月28日23:59:59 EEST,由于转入夏时制(DST),只有28天22小时59分钟59秒。所以即使不减去一秒,也无法解决这种情况下的问题。

英文:

java.time

Since you can use java.time, the modern Java date and time API, I recommend that you stick to that and leave the old classes SimpleDateFormat and Date alone. Then you also don’t need the Apache DateUtils. The ChronoUnit enum is from java.time.

	DateTimeFormatter dateFormatter
			= DateTimeFormatter.ofPattern(&quot;d MMM u&quot;, Locale.ENGLISH);
	
	String startDateString = &quot;30 Jan 2016&quot;;
	LocalDate originalStartDate
			= LocalDate.parse(startDateString, dateFormatter);
	for (int i = 0; i &lt; 12; i++) {
		LocalDate startDate = originalStartDate.plusMonths(i);
		LocalDate nextStartDate = originalStartDate.plusMonths(i + 1);
		LocalDate endDate = nextStartDate.minusDays(1);
		long differenceDays = ChronoUnit.DAYS.between(startDate, nextStartDate);
		
		System.out.format(&quot;%s - %s : %d%n&quot;, startDate, endDate, differenceDays);
	}

Output is:

> 2016-01-30 - 2016-02-28 : 30
> 2016-02-29 - 2016-03-29 : 30
> 2016-03-30 - 2016-04-29 : 31
> 2016-04-30 - 2016-05-29 : 30
> 2016-05-30 - 2016-06-29 : 31
> 2016-06-30 - 2016-07-29 : 30
> 2016-07-30 - 2016-08-29 : 31
> 2016-08-30 - 2016-09-29 : 31
> 2016-09-30 - 2016-10-29 : 30
> 2016-10-30 - 2016-11-29 : 31
> 2016-11-30 - 2016-12-29 : 30
> 2016-12-30 - 2017-01-29 : 31

What went wrong in your code?

There are a couple of reasons behind your observed unexpected results.

  1. When you add a month to January 30, you get February 29 as you had expected. In a non-leap year you would have got February 28. When you add another month to February 29, you get March 29. Is it surprising when you think about it? In a non-leap year you would have got March 28, so 2016 being a leap year actually helped you get closer to your desired result. In my code I solve this problem by adding the correct number of months to the original start date rather than adding one month to the previous start date.
  2. As PeterMmm already said, ChronoUnit.DAYS.Between() counts full 24 hours days. Any partial day is discarded. Even 23 hours 59 minutes 59 seconds. From Sat Jan 30 00:00:00 EET 2016 to Sun Feb 28 23:59:59 EET 2016 is 30 days 23 hours 59 minutes 59 seconds, so the result you get is 30 days. In my code I solve the problem by counting the days until the start of the next item.

As an aside: From Mon Feb 29 00:00:00 EET 2016 to Mon Mar 28 23:59:59 EEST 2016, because of transistion to summer time (DST) is only 28 days 22 hours 59 minutes 59 seconds. So abstaining from subtracting a second would not solve your problem in this case.

答案2

得分: 1

这部分代码不是必要的。或者查看一下between()文档。第二个参数是排除的。这意味着您的第二个参数未达到当天的结束时间,因此会减少一天的计数(仅计算完整的24小时天数)。

英文:

This

end = DateUtils.addSeconds(end, -1);

isn't necessary. Or do

end = DateUtils.addSeconds(end, 0);

have a look at between() documentation.
Second parameter is exclusive. That means, your second parameter does not reach the end of day and so a day less is counted (only full 24h days are counted).

答案3

得分: 1

以下是稍微不同的方法:

LocalDate startDate = LocalDate.of(2016, 1, 30);
LocalDate endDate = LocalDate.of(2017, 1, 30);

Function<YearMonth, LocalDate> addMonthFunction = ym -> ym
    .atDay(Math.min(startDate.getDayOfMonth(), ym.lengthOfMonth()));

long months = ChronoUnit.MONTHS.between(startDate, endDate);
YearMonth yearMonth = YearMonth.from(startDate);
Stream.iterate(yearMonth, ym -> ym.plusMonths(1))
    .limit(months)
    .map(addMonthFunction)
    .map(LocalDate::atStartOfDay)
    .map(date -> {
        LocalDateTime end = addMonthFunction.apply(YearMonth.from(date.plusMonths(1)))
            .atStartOfDay()
            .minusSeconds(1);
        long between = ChronoUnit.DAYS.between(date, end) + 1;
        return String.format("%s to %s (%s days)", date, end, between);
    })
    .forEach(System.out::println);

思路是,每个后续日期总是落在月份的第30天(更准确地说,与开始日期的同一月份的日子),除非那将是一个无效的日期。

我在这里使用了 LocalDate,但你可以通过使用 ZonedDateTime 来使其对时区更敏感。

英文:

Here's a slightly different approach:

LocalDate startDate = LocalDate.of(2016, 1, 30);
LocalDate endDate = LocalDate.of(2017, 1, 30);

Function&lt;YearMonth, LocalDate&gt; addMonthFunction = ym -&gt; ym
    .atDay(Math.min(startDate.getDayOfMonth(), ym.lengthOfMonth()));

long months = ChronoUnit.MONTHS.between(startDate, endDate);
YearMonth yearMonth = YearMonth.from(startDate);
Stream.iterate(yearMonth, ym -&gt; ym.plusMonths(1))
    .limit(months)
    .map(addMonthFunction)
    .map(LocalDate::atStartOfDay)
    .map(date -&gt; {
        LocalDateTime end = addMonthFunction.apply(YearMonth.from(date.plusMonths(1)))
            .atStartOfDay()
            .minusSeconds(1);
        long between = ChronoUnit.DAYS.between(date, end) + 1;
        return String.format(&quot;%s to %s (%s days)&quot;, date, end, between);
    })
    .forEach(System.out::println);

The idea is that each next date always falls on the 30th day of the month (or, more precisely, the same day-of-month as the start date), except if that would be an invalid date.

I used LocalDates here, but you could make it timezone-sensitive by using ZonedDateTime.

huangapple
  • 本文由 发表于 2020年10月18日 00:49:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/64404936.html
匿名

发表评论

匿名网友

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

确定