英文:
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 = "30 Jan 2016";
String endDate = "29 Jan 2017"; // current not using this ?!
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());
}
}
- 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 "Item{" +
"start=" + start +
", end=" + end +
'}';
}
}
答案1
得分: 2
java.time
由于您可以使用现代的 Java 日期和时间 API - java.time
,我建议您坚持使用该API,避免使用旧的类SimpleDateFormat
和Date
。您也不再需要使用Apache的DateUtils
。ChronoUnit
枚举来自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月30日添加一个月时,您会得到期望的2月29日。在非闰年中,您将得到2月28日。当您再次在2月29日添加一个月时,您将得到3月29日。如果您考虑一下,这是否令人惊讶?在非闰年中,您将得到3月28日,所以2016年作为闰年实际上帮助您更接近所需的结果。在我的代码中,我通过对原始的开始日期添加正确数量的月份来解决了这个问题,而不是在上一个开始日期的基础上添加一个月。
-
正如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("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);
}
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.
- 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.
- 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<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);
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 LocalDate
s here, but you could make it timezone-sensitive by using ZonedDateTime
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论