1

Given an example event recurrence schedule which is defined as follows:

StartTime: Monday, 2016-June-06 09:00H
EndTime:   Monday, 2016-August-29 10:00H 
Period:    Bi-weekly

This means, every 2 weeks on Monday at 09:00H, until 29th August 2016. However the endTime is on a Monday, 29th August, which is just at the beginning of the new week, but should be included in the schedule (because it ends at 10:00H). So I expect 7 occurrences of the event.

Conversely, if the endTime were 08:00H on 29th August, then I would expect only 6 occurrences of the event.

Right now, I calculate number of occurrences using Joda Time as such: Weeks.weeksBetween(startTime, endTime).dividedBy(multiplier).getWeeks()

Of course, this gives me only 6 occurrences instead of 7, because there are really only 12 weeks in the time duration and the last occurrences is on the Monday after the last week (ending on Sunday). So that does not count as full week. The same goes for any other kinds of schedules - months, days, years etc.

How can I reliably calculate number of occurrences of an event between two times?

Thanks

codinguser
  • 5,562
  • 2
  • 33
  • 40

3 Answers3

1

Using pseudo-code, as I with Yoda not too familiar I am:

  1. counter = 1, someDate = start time, increment = two weeks
  2. loop: someDate = someDate + increment
  3. if someDate < end time: then counter++ else break loop

Alternatively, for the people that don't like looping:

  1. Compute the exact number of hours between start date and end date
  2. Figure how often 24 * 7 * 2 would fit into the aforementioned number of hours

Sample code using JodaTime library:

int multiple = 2; //or whatever periodicity is needed
ReadablePeriod jodaPeriod;
switch (mPeriodType){
    case DAY:
        jodaPeriod = Days.days(multiple);
        break;
    case WEEK:
        jodaPeriod = Weeks.weeks(multiple);
        break;
    case MONTH:
        jodaPeriod = Months.months(multiple);
        break;
    case YEAR:
        jodaPeriod = Years.years(multiple);
        break;
    default:
        jodaPeriod = Months.months(multiple);
}
int count = 0;
LocalDateTime startTime = new LocalDateTime(mPeriodStart.getTime());
while (startTime.toDateTime().getMillis() < mPeriodEnd.getTime()){
    ++count;
    startTime = startTime.plus(jodaPeriod);
}
return count;
RealSkeptic
  • 33,993
  • 7
  • 53
  • 79
GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • I think this would work. Thanks GhostCat. I guess I was unconsciously trying to avoid looping because I would have to handle each type of period (days, weeks, months, years etc) separately in the loop, while avoiding timezone pitfalls. – codinguser Aug 31 '16 at 07:09
  • The trouble with looping is that you get a calculation time that depends on the length of the interval. Since this is supposed to be a simple calculation we would all do with a pen and paper in the same time for periods of 100 years as quickly as for 2 weeks, I think looping is not a good idea. – RealSkeptic Aug 31 '16 at 07:12
  • Put in some other non-looping approach. – GhostCat Aug 31 '16 at 07:18
  • Ah, but that approach won't work with months, only with weeks, and also would fail on daylight saving boundaries. – RealSkeptic Aug 31 '16 at 07:21
  • @RealSkeptic Well, its a free site here. Feel free to add your better answer any time ;-) I wouldn't mind upvoting it. – GhostCat Aug 31 '16 at 07:26
  • I updated the answer to at least get the "different period" type in. Although I am not so sure if joda-time allows for such tricks as pseudo-code. – GhostCat Aug 31 '16 at 07:27
  • @RealSkeptic if I do the addition of the `increment` using a Joda Time constructs which handle timezones, daylight saving etc, it should work fine, no? – codinguser Aug 31 '16 at 07:31
  • I added some code for the solution, however, I would still be interested in a solution which does not use looping – codinguser Aug 31 '16 at 07:39
  • @codinguser the daylight saving issue is with the non-looping solution, which bypasses Joda as it relies on calculating weeks by hours. The looping solution would work properly using Joda constructions, but if you have a long duration and a short period of repetition, it is going to take a long time. There should be a better solution using Joda - I don't have Joda at work so I can't do this right now - but I'm thinking about your basic solution plus a correction at the end. – RealSkeptic Aug 31 '16 at 07:40
  • @RealSkeptic Define "a long time". It is still - just computations. So, even when you have a solution that takes 500 miliseconds ... the question would still be: how often are those 500 miliseconds spent?! Beyond that: i would assume, that **when using joda** to get the hours between two dates ... that you would not have to worry about DST (otherwise, the looping thing wouldn't work either!) – GhostCat Aug 31 '16 at 07:43
  • @GhostCat you are assuming wrong. When you ask Joda to calculate how many hours are between two dates, it will give you the exact hours. However, that time in weeks is not the same as dividing it by 7*24. That's the exact point - over the daylight saving transition, one week can be 7*24-1 or 7*24+1. But when you ask Joda to add multiples of "weeks", it takes that into account. – RealSkeptic Aug 31 '16 at 07:45
  • @RealSkeptic I would be still interested in your proposal using Joda library and no iteration – codinguser Sep 02 '16 at 10:25
1

Just following GhostCat here in Joda. Some shortcuts are taken by removing "H" from the date time. I am sure it can be solved. Thanks Ghostcat :)

import java.util.Locale;

import org.joda.time.DateTime;
import org.joda.time.Hours;
import org.joda.time.Period;
import org.joda.time.Weeks;
import org.joda.time.format.DateTimeFormat;

public class TestDateJoda {
    public static void main(String[] args) {

        String startTime = "Monday, 2016-June-06 09:00";
        String endTime = "Monday, 2016-August-29 10:00";
        DateTime startDateTime = DateTimeFormat.forPattern("E, yyyy-MMM-dd HH:mm").withLocale(Locale.ENGLISH)
                .parseDateTime(startTime);
        DateTime endDateTime = DateTimeFormat.forPattern("E, yyyy-MMM-dd HH:mm").withLocale(Locale.ENGLISH)
                .parseDateTime(endTime);

        System.out.println(startDateTime);
        System.out.println(endDateTime);

        Period period = new Period(startDateTime,endDateTime);

        Hours hours = Hours.hoursBetween(startDateTime, endDateTime);

        Weeks weeks = Weeks.weeksBetween(startDateTime, endDateTime);

        //starting first week add 1 follow ghost cat
        System.out.println(hours.getHours()/(24*7*2) + 1);

        System.out.println(weeks.getWeeks()/(2) + 1);

    }

}
Ramachandran.A.G
  • 4,788
  • 1
  • 12
  • 24
  • Now try that for the other OP examples, like end time of 08:00, and over a period where there is daylight saving change. – RealSkeptic Aug 31 '16 at 07:50
1

It seems to me that you simply need to add 1 to the result of your calculation in all cases, except when the end date is exactly at the same hour after the same interval (unless you calculate that as an occurrence).

╔══════════════════╤══════════════════╤══════════════╤══════════════╤════════════╗
║ Start Date       │ End Date         │ result of    │ result of    │ Expected   ║
║                  │                  │ weeksBetween │ dividedBy(2) │ result     ║
╠══════════════════╪══════════════════╪══════════════╪══════════════╪════════════╣
║ 2016-06-07 09:00 │ 2016-08-30 10:00 │ 12           │ 6            │ 7          ║
╟──────────────────┼──────────────────┼──────────────┼──────────────┼────────────╢
║ 2016-06-07 09:00 │ 2016-08-30 08:00 │ 11           │ 5            │ 6          ║
╟──────────────────┼──────────────────┼──────────────┼──────────────┼────────────╢
║ 2016-06-07 09:00 │ 2016-08-30 09:00 │ 12           │ 6            │ 6 or 7     ║
║                  │                  │              │              │ you decide ║
╟──────────────────┼──────────────────┼──────────────┼──────────────┼────────────╢
║ 2016-06-07 09:00 │ 2016-08-23 10:00 │ 11           │ 5            │ 6          ║
╟──────────────────┼──────────────────┼──────────────┼──────────────┼────────────╢
║ 2016-06-07 09:00 │ 2016-08-23 08:00 │ 10           │ 5            │ 6          ║
╟──────────────────┼──────────────────┼──────────────┼──────────────┼────────────╢
║ 2016-06-07 09:00 │ 2016-08-23 09:00 │ 11           │ 5            │ 6          ║
╚══════════════════╧══════════════════╧══════════════╧══════════════╧════════════╝

Take the first combination - the end date is slightly after the supposed last occurrence:

Start
──╂──────┼──────┼──────┼──────┼──────┼──────┼─┰╴
                                             end
      1      2      3      4      5     6

What Joda counts is the gaps. What you want to count is the ticks. And there is always one more tick than there are gaps.

In the second case, the end is a little before the next occurrence:

Start
──╂──────┼──────┼──────┼──────┼──────┼─────┰┼─╴
                                          end
      1      2      3      4      5

This time Joda says there are 5 whole "periods" between the start and the end, but what you want to count is the ends of those periods, , so it's always one more.

The only case where you might want to decide against adding one is if the end falls exactly on the next occurrence, in which case you may want to exclude it as you consider it to be "already ended".

If so, you simply add a test for it:

Weeks weeks = Weeks.weeksBetween(startDateTime, endDateTime);
int occurrences = weeks.dividedBy(2).getWeeks();
if ( ! startDateTime.plus(weeks).equals(endDateTime) ) {
    occurrences++;
}
RealSkeptic
  • 33,993
  • 7
  • 53
  • 79