0

I need to be able manage dates in flexible way for calculating monthly events.

If I use

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

in result you can see

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

It seems following month semantic if we add one moth to date we should get next month (it's correct), but following occurrence semantic it has an issue, because of acceptance is to get same date day but in next month (as you can see it is not same sometimes. There are 31, 30, 28). If there is no same date, we should null for example.

Does java provides ability calculate months following occurrence semantic strategy to handle my issue?

More explanation examples.

I DO NOT NEED LAST MONTH DAY I need occurrences only from first explanation for 31th month day. I simplified question as it was possible.

I can do workarounds with calendar but I expected more elegant solution.

If you use ical4j to calculate occurrences, you'll get this result. And occurrences calculation should be fixed (it's a bug because of 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]
    */
}

but if I use same library with small change:

      rrule('FREQ=MONTHLY;UNTIL=20110101')

-->

      rrule('FREQ=MONTHLY;UNTIL=20110101;BYMONTHDAY=31')

it is calculated than correctly. But I need same calculation for both cases because of rfc5545.

The root issue in the ical4j is in Recur.java:

private void increment(final Calendar cal) {
    final int calInterval = (getInterval() >= 1) ? getInterval() : 1;
    cal.add(calIncField, calInterval);
}
Community
  • 1
  • 1
Sergii
  • 7,044
  • 14
  • 58
  • 116
  • 6
    I think you're trying to solve the wrong problem. This seems to be a very complicated way of saying "I need to know how many days there are in a specific month". What is the context of this problem? – Bex Aug 15 '17 at 12:17
  • 2
    if you need to add days, just add 31 days each time. And set lenient property to false. That should help. Otherwise, you are adding one month to the end of month, which correctly returns end of next month. Month semantic works like month. If you need it to work like days, you should use days. – xycf7 Aug 15 '17 at 12:21
  • 1
    You've correctly identified what `Calendar.add()` does -- which is also clearly documented in its Javadoc. It's not clear from the question why you want it to do something different. A `Calendar` containing a null field does not seem like a viable thing to me. Also, consider using Java 8 date types. – slim Aug 15 '17 at 12:22
  • @xycf7 it needs to cover 3 cases at least (29,30,31) - and if month has less than 31 day, and I need occurrence in 15th, the result date can be shifted to 16th (17th, 18th). Any way it's good idea, I'll try to consider one better. – Sergii Aug 15 '17 at 12:56
  • You are using troublesome old date-time classes now supplanted by the java.time classes. – Basil Bourque Aug 15 '17 at 16:35
  • Please state your problem or goal. – Basil Bourque Aug 15 '17 at 16:35
  • @BasilBourque which `java.time` class should I use? I need monthly occurrences on `31th` as described before (in specified range). – Sergii Aug 15 '17 at 16:37
  • 1
    Please edit your Question to state simply your problem. If you need the last day of every month, say "I need to determine the last date of every month". As written, I guess that is what you want but I'm not certain. I wrote an Answer based on my guess as to your unclear intentions. – Basil Bourque Aug 15 '17 at 16:48
  • @BasilBourque Not last day, but `31th` if it present in month. This is was described issue. – Sergii Aug 15 '17 at 16:53
  • ??? You mean you want the date of last day of months that have 31 days? – Basil Bourque Aug 15 '17 at 16:57
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151980/discussion-between-sergii-and-basil-bourque). – Sergii Aug 15 '17 at 16:59
  • The months that have 31 days are always the same (assuming you're always working with current gregorian calendar), so you just need to check if the next month is one of them. @Sergii If you could please **[edit]** the question and clarify it (possibly with examples of desired output), that would make things clearer for everybody and increase the chances of getting an answer. –  Aug 15 '17 at 18:42
  • @Hugo, i've just added as you asked. Also you've deleted your answer before I've got a chance to check it... – Sergii Aug 15 '17 at 19:19
  • @Sergii Well, I've undeleted my answer, so let me know if that's what you need. –  Aug 15 '17 at 19:27

2 Answers2

3

java.time

Use the modern java.time classes rather than the troublesome old legacy classes.

Your Question is overwrought and unclear. Here are some general pointers and examples that should get you going in the right direction.

The LocalDate class represents a date-only value without time-of-day and without time zone.

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) ) ;

If you want to add a month, call plusMonths.

LocalDate monthLater = ld.plusMonths( 1 ) ;

If you want to add 30 days, call plusDays.

LocalDate thirtyDaysLater = ld.plusDays( 30 ) ;

If you want the last day of next month, use a TemporalAdjuster to get the first of next month, then another to get end of that day’s month. You will find such implementations in TemporalAdjusters class (note the plural s).

LocalDate firstOfNextMonth = ld.with( TemporalAdjusters.firstOfNextMonth() ) ;
LocalDate lastOfNextMonth = firstOfNextMonth.with( TemporalAdjusters.lastOfMonth() ) ;

If you only care about months that happen to have 31 days, use Month enum to determine the length. Consider writing your own implementation of TemporalAdjuster, like "lastOfTheNextMonthContaining31Days".

if( ld.getMonth().maxLength() == 31 ) { … }

If you want a certain day of every month, use the YearMonth class. Trap for DateTimeException if that month lacks that day-of-month such as 31 in February.

YearMonth ym = YearMonth.from( ld ) ;
LocalDate thirteenth = ym.atDay( 13 ) ;

Get same day-of-month in following month.

int dayOfMonth = ld.getDayOfMonth() ;
YearMonth my = YearMonth.from( ld ) ;
LocalDate sameDayOfMonthInNextMonth = ym.plusMonths( 1 ).atDay( dayOfMonth ) ; 

If needed, you can add a time-of-day and time zone to get a ZonedDateTime. If the time-of-day happens to be invalid in that date, it is automatically adjusted. Be sure to study the adjustment behavior to see that it agrees with your needs.

LocalTime lt = LocalTime.of( 9 , 30 ) ;
ZoneId z = ZoneId.of( "Pacific/Auckland" ) ;
ZonedDateTime zdt = ZonedDateTime.of( ld , lt , z ) ;
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Good answer! Just one detail: in `TemporalAdjusters` class, the methods names shouldn't be `firstDayOfNextMonth()` and `lastDayOfMonth()`? –  Aug 15 '17 at 17:42
  • answer is good but not for my question. To make workaround possible with calendar also. – Sergii Aug 15 '17 at 18:36
  • @BasilBourque your answer is good, I voted one, but my research is not finished, I expect better solution for my case and I hope it is possible, so I've not marked my question like answered. – Sergii Aug 15 '17 at 19:36
2

Based on the comments, where it was said that you want the date of last day of months that have 31 days, you could do:

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?)
    }
}

The output will be:

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 new Date/Time API

The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.

If you're using Java 8, consider using the new java.time API. It's easier, less bugged and less error-prone than the old APIs.

If you're using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, there's the ThreeTenABP (more on how to use it here).

The code below works for both. The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport (or Android's ThreeTenABP) is org.threeten.bp), but the classes and methods names are the same.

To work only with just a date and time, you can use a LocalDateTime:

// August 31th 2017, at 09:30:15
LocalDateTime d = LocalDateTime.of(2017, 8, 31, 9, 30, 15);

Then you create a TemporalAdjuster that encapsulates the logic of getting the last day of the next month that has 31 days:

// 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());
};

Or using Java <= 7 syntax:

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

Finally, you use a DateTimeFormatter to format the output and loop through the dates adjusting them with the 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));
}

The output will be the same:

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

In this code, I'm ignoring months that doesn't have 31 days. If you want to do some action in these cases (like return null or something else), you can create a method like that:

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

Then you'd do:

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

The output is also:

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

Community
  • 1
  • 1
  • 1
    your answer is good, I voted one, but my research is not finished, I expect better solution for my case I hope it is possible, so I've not marked my question like answered. – Sergii Aug 15 '17 at 19:35