我需要能够灵活地管理日期以计算月度事件。
如果我使用
SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.AUGUST, 31, 9, 30, 15);
Calendar next = calendar;
for (int i = 1; i++<10;) {
next = (Calendar) calendar.clone();
next.add(Calendar.MONTH, i);
System.out.println(sdf.format(next.getTime()));
}
结果你可以看到
31-10-2017 09:30:15
30-11-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
28-2-2018 09:30:15
31-3-2018 09:30:15
30-4-2018 09:30:15
31-5-2018 09:30:15
30-6-2018 09:30:15
似乎遵循月语义,如果我们在下个月添加一个飞蛾,我们应该在下个月获得(它是正确的),但是在发生语义后它有一个问题,因为接受是为了获得相同的日期,但是在下个月(因为您可以看到它有时不相同。有31
,30
,28
)。如果没有相同的日期,我们应该null
例如。
java是否提供能力计算出现语义策略后数月来处理我的问题?
我不需要上一个月我只需要在第31个月的第一个解释中出现。我尽可能地简化了问题。
我可以使用calendar
做变通办法,但我期待更优雅的解决方案。
如果您使用ical4j来计算出现次数,那么您将获得此结果。并且应该修复事件计算(由于rfc5545,它是一个错误。)
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.component.VEvent
void testCalculateRecurrenceSetOn31th() {
VEvent event = new ContentBuilder().vevent {
dtstart('20100831T061500Z', parameters: parameters() {
value('DATETIME')})
dtend('20100831T064500Z', parameters: parameters() {
value('DATETIME')})
rrule('FREQ=MONTHLY;UNTIL=20110101')
}
def dates = event.calculateRecurrenceSet(new Period('20100831T000000/20110131T000000'))
def expected = new PeriodList(true)
expected.add new Period('20100831T061500Z/PT30M')
expected.add new Period('20101031T061500Z/PT30M')
expected.add new Period('20101231T061500Z/PT30M')
println dates
assert dates == expected
/*
Assertion failed:
assert dates == expected
| | |
| | [20100831T061500Z/PT30M, 20101031T061500Z/PT30M, 20101231T061500Z/PT30M]
| false
[20100831T061500Z/PT30M, 20100930T061500Z/PT30M, 20101030T061500Z/PT30M, 20101130T061500Z/PT30M, 20101230T061500Z/PT30M]
*/
}
但如果我使用相同的库进行小改动:
rrule('FREQ=MONTHLY;UNTIL=20110101')
- &GT;
rrule('FREQ=MONTHLY;UNTIL=20110101;BYMONTHDAY=31')
计算结果不正确。但由于 rfc5545 ,我需要对两种情况进行相同的计算。
ical4j中的根问题位于Recur.java
:
private void increment(final Calendar cal) {
final int calInterval = (getInterval() >= 1) ? getInterval() : 1;
cal.add(calIncField, calInterval);
}
答案 0 :(得分:3)
使用现代java.time类而不是麻烦的旧遗留类。
你的问题过于紧张且不清楚。以下是一些通用指针和示例,可以帮助您朝着正确的方向前进。
LocalDate
类表示没有时间且没有时区的仅限日期的值。
LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) ) ;
如果您想添加月份,请致电plusMonths
。
LocalDate monthLater = ld.plusMonths( 1 ) ;
如果您想添加30天,请致电plusDays
。
LocalDate thirtyDaysLater = ld.plusDays( 30 ) ;
如果您想要下个月的最后一天,请使用TemporalAdjuster
获取下个月的第一天,然后使用另一个来获得当月的结束。您将在TemporalAdjusters
类中找到此类实现(请注意复数s
)。
LocalDate firstOfNextMonth = ld.with( TemporalAdjusters.firstOfNextMonth() ) ;
LocalDate lastOfNextMonth = firstOfNextMonth.with( TemporalAdjusters.lastOfMonth() ) ;
如果您只关心碰巧有31天的月份,请使用Month
枚举来确定长度。考虑编写自己的TemporalAdjuster
实现,例如“lastOfTheNextMonthContaining31Days”。
if( ld.getMonth().maxLength() == 31 ) { … }
如果您想要每月的某一天,请使用YearMonth
课程。陷阱DateTimeException
如果那个月缺少那个月,例如2月31日。
YearMonth ym = YearMonth.from( ld ) ;
LocalDate thirteenth = ym.atDay( 13 ) ;
在下个月获得相同的日期。
int dayOfMonth = ld.getDayOfMonth() ;
YearMonth my = YearMonth.from( ld ) ;
LocalDate sameDayOfMonthInNextMonth = ym.plusMonths( 1 ).atDay( dayOfMonth ) ;
如果需要,您可以添加时间和时区来获取ZonedDateTime
。如果该日期的时间恰好无效,则会自动进行调整。一定要研究调整行为,看它是否符合您的需求。
LocalTime lt = LocalTime.of( 9 , 30 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;
答案 1 :(得分:2)
基于comments,据说您希望 31天的最后一天的日期,您可以这样做:
SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");
Calendar calendar = Calendar.getInstance();
calendar.set(2017, Calendar.AUGUST, 31, 9, 30, 15);
Calendar next = calendar;
for (int i = 1; i < 10; i++) {
next = (Calendar) calendar.clone();
next.add(Calendar.MONTH, i);
// check if month has 31 days
switch (next.get(Calendar.MONTH)) {
// months that have 31 days
case Calendar.JANUARY:
case Calendar.MARCH:
case Calendar.MAY:
case Calendar.JULY:
case Calendar.AUGUST:
case Calendar.OCTOBER:
case Calendar.DECEMBER:
// set to last day of month
next.set(Calendar.DAY_OF_MONTH, 31);
System.out.println(sdf.format(next.getTime()));
break;
default:
// month doesn't have 31 days, do what? (return null? exception? do nothing?)
}
}
输出将是:
31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15
旧类(Date
,Calendar
和SimpleDateFormat
)有lots of problems和design issues,它们将被新API取代。< / p>
如果您使用的是 Java 8 ,请考虑使用new java.time API。它更容易,less bugged and less error-prone than the old APIs。
如果您使用的是 Java&lt; = 7 ,则可以使用ThreeTen Backport,这是Java 8新日期/时间类的绝佳后端。对于 Android ,有ThreeTenABP(更多关于如何使用它here)。
以下代码适用于两者。
唯一的区别是包名称(在Java 8中为java.time
,在ThreeTen Backport(或Android的ThreeTenABP)中为org.threeten.bp
),但类和方法名称是相同的
要仅使用日期和时间,您可以使用LocalDateTime
:
// August 31th 2017, at 09:30:15
LocalDateTime d = LocalDateTime.of(2017, 8, 31, 9, 30, 15);
然后你创建一个TemporalAdjuster
,它封装了下一个月的最后一天有31天的逻辑:
// adjust the date to the next month that has 31 days (Java 8 syntax)
TemporalAdjuster lastDayOfNext31daysMonth = temporal -> {
Temporal t = temporal;
do {
// next month
t = t.plus(1, ChronoUnit.MONTHS);
// check if month has 31 days
} while (Month.from(t).maxLength() != 31);
// adjust to the end of the month
return t.with(TemporalAdjusters.lastDayOfMonth());
};
或使用Java&lt; = 7语法:
TemporalAdjuster lastDayOfNext31daysMonth = new TemporalAdjuster() {
@Override
public Temporal adjustInto(Temporal temporal) {
Temporal t = temporal;
do {
// next month
t = t.plus(1, ChronoUnit.MONTHS);
// check if month has 31 days
} while (Month.from(t).maxLength() != 31);
// adjust to the end of the month
return t.with(TemporalAdjusters.lastDayOfMonth());
}
};
最后,您使用DateTimeFormatter
格式化输出并循环使用TemporalAdjuster
调整它们的日期:
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-M-yyyy hh:mm:ss");
for (int i = 1; i <= 5; i++) {
// adjust to last day of next month that has 31 days
d = d.with(lastDayOfNext31daysMonth);
// format the date
System.out.println(d.format(fmt));
}
输出结果相同:
31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15
在这段代码中,我忽略了没有31天的月份。如果你想在这些情况下做一些动作(比如返回null
或其他),你可以创建一个这样的方法:
// check if next n-th month has 31 days
public LocalDateTime nextMonth(int n, LocalDateTime d) {
// get next n-th month
LocalDateTime date = d.plusMonths(n);
// if month has 31 days, return it
if (date.getMonth().maxLength() == 31) {
return date;
}
// otherwise, return null
return null;
}
然后你会这样做:
LocalDateTime d = LocalDateTime.of(2017, 8, 31, 9, 30, 15);
for (int i = 1; i < 10; i++) {
LocalDateTime next = nextMonth(i, d);
if (next != null) {
System.out.println(next.format(fmt));
}
}
输出也是:
31-10-2017 09:30:15
31-12-2017 09:30:15
31-1-2018 09:30:15
31-3-2018 09:30:15
31-5-2018 09:30:15