如何计算java中的月度事件

时间:2017-08-15 12:11:35

标签: java date datetime calendar rfc5545

我需要能够灵活地管理日期以计算月度事件。

如果我使用

    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

似乎遵循月语义,如果我们在下个月添加一个飞蛾,我们应该在下个月获得(它是正确的),但是在发生语义后它有一个问题,因为接受是为了获得相同的日期,但是在下个月(因为您可以看到它有时不相同。有313028)。如果没有相同的日期,我们应该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);
}

2 个答案:

答案 0 :(得分:3)

java.time

使用现代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

Java新日期/时间API

旧类(DateCalendarSimpleDateFormat)有lots of problemsdesign 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