Thursday, December 27, 2012

Rolling the Calendar

For NetXStudio , I needed a function which would split a period into sub-periods. The function needs to be generic, so I can pass in a Calendar field which would then split the Period object according to field value.

First some definitions:
  • The DateTimeRange object is a simple Java Object with set and get methods for two Date fields named 'begin' and 'end'. 
  • Day start is defined at 00:00h and the day ends at 23.59h (Yes there is one lost minute but OK for me). 
The Java Calendar  or better the GregorianCalendar is capable to roll forward and backward as a real Calendar would do. The method roll(...) is used for that. The Calendar can be forced into a specific position using the set(...) method.

My method to deal with at least Calendar.MONTH and Calendar.DAY_OF_MONTH ended using the method getActualMinimum(...) and getActualMaximum(...) to set the Calendar to the desired position. The desired positions are:
  • Calendar.MONTH => The first day of the current month. 
  • Calendar.DAY_OF_MONTH => The first hour of the day. 
Now to roll into these positions, I need to know the 'child' Calendar field, to be used in the getActualMin/Max() methods. By 'child' field I mean the following mapping:
  • Calendar.MONTH => Calendar.DAY_OF_MONTH
  • Calendar.DAY_OF_MONTH => Calendar.HOUR_OF_DAY
Calling getActualMinimum(Calendar.DAY_OF_MONTH) and setting this as the new Calendar position, would set the Calendar to the first day of the current month of the Calendar. Calling getActualMinimum(Calendar.HOUR_OF_DAY) and setting this on the Calendar would set it at the first hour of the day.

All fine... The Unit test on a 6 month period which would be asked to split it in first MONTH_OF_YEAR, and subsequently DAY_OF_MONTH generated the intended result nicely.

Full: From: 27-06-2012 @ 00:00 To: 27-12-2012 @ 00:00
 Month: From: 01-12-2012 @ 00:00 To: 27-12-2012 @ 23:59
   Day: From: 27-12-2012 @ 00:00 To: 27-12-2012 @ 23:59
   Day: From: 26-12-2012 @ 00:00 To: 26-12-2012 @ 23:59
   Day: From: 25-12-2012 @ 00:00 To: 25-12-2012 @ 23:59
   Day: From: 24-12-2012 @ 00:00 To: 24-12-2012 @ 23:59
   Day: From: 23-12-2012 @ 00:00 To: 23-12-2012 @ 23:59
   Day: From: 22-12-2012 @ 00:00 To: 22-12-2012 @ 23:59
   Day: From: 21-12-2012 @ 00:00 To: 21-12-2012 @ 23:59

Being confident about my code, I expanded it by adding an option to split the Period in weeks. This is were trouble started. First I had to pick the 'child' field for the day of the week for the 'parent' field  Calendar.WEEK_OF_YEAR.

I quickly choose to use Calendar.DAY_OF_WEEK for this, makes sense right? Well that's what I initially thought. The result for setting the Calendar to the getActualMinimum(Calendar.DAY_OF_WEEK) actually didn't set the Calendar position to what I expected. I took me a while to figure out why. The reason is best explained with the following diagram:




In the diagram, the Calendar is set to today (Which happens to be the publication date of this blog-post, but this purely a coincidence, trust me on this). Now calling getActualMinimum with my freshly developed method for Calendar.DAY_OF_MONTH and Calendar.HOUR_OF_DAY works as expected. the Calendar rolls to the intended position. For Calendar.DAY_OF_WEEK) however, it doesn't. It actually roles forward! What I really expected was the new Calendar position to be the previous Sunday at least. Which was not even correct, as it should be the Monday for my Local.

mmmh... It triggered my curiosity (Ok, first there was a few moments of programmers frustration..). Why does it do that. I read about Calendar and noticed a bit of documentation on Calendar field conflicts. Suddenly I realized I should not use the actualMinimum for rolling the week day. What I should use is getFirstDayOfWeek() and set this on the calendar.

Ok, but this means that depending on the field, my method would need to act differently, which was not my initial goal. Sofar I haven't found another solution with the current GregorianCalendar capabilities. I even had to implement a function getLastDayOfWeek(Calendar cal) to set the end boundary of the week.  I encourage any reader to comment and let me know a better solution. (Note: I know about date/time libraries, but without them, can you make this better?).

Here is the code for the final solution:

public List periods(DateTimeRange dtr, int calField) {

  boolean weekTreatment = false;

  int childField = -1;
  switch (calField) {
  case Calendar.MONTH: {
   childField = Calendar.DAY_OF_MONTH;
  }
   break;
  case Calendar.DAY_OF_MONTH: {
   childField = Calendar.HOUR_OF_DAY;
  }
   break;
  case Calendar.WEEK_OF_YEAR: {
   childField = Calendar.DAY_OF_WEEK;
   weekTreatment = true;
  }
   break;
  }

  List result = Lists.newArrayList();

  if (childField == -1) {
   result.add(dtr);
   return result;
  }

  final Calendar cal = GregorianCalendar.getInstance();
  cal.setTime(dtr.getEnd().toGregorianCalendar().getTime());

  // An end calendar to compare the calendar field, and not take the field
  // maximum but the value from the end calendar.
  final Calendar endCal = GregorianCalendar.getInstance();
  endCal.setTime(dtr.getEnd().toGregorianCalendar().getTime());

  // Go back in time and create a new DateTime Range.
  do {

   // Set the begin to the actual minimum and end to the actual
   // maximum, except at the start, where we keep the actual.
   // At the end, roll one beyond the minimum to set the new actual.
   if (cal.get(calField) != endCal.get(calField)) {
    if (weekTreatment) {
     // :-( there is no method to get the last day of week.
     cal.set(childField, getLastDayOfWeek(cal));

    } else {
     cal.set(childField, cal.getActualMaximum(childField));
    }

   }

   final Date end = cal.getTime();

   // Special Treatment for Week
   if (weekTreatment) {
    final int firstDayOfWeek = cal.getFirstDayOfWeek();
    cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);
   } else {
    int minimum = cal.getActualMinimum(childField);
    cal.set(childField, minimum);
   }

   Date begin;
   if (cal.getTime().getTime() < dtr.getBegin().toGregorianCalendar()
     .getTimeInMillis()) {
    begin = this.fromXMLDate(dtr.getBegin());
   } else {
    begin = cal.getTime();
   }

   final DateTimeRange period = period(this.adjustToDayStart(begin),
     this.adjustToDayEnd(end));
   result.add(period);

   // Role back one more, so the new actual can be applied.
   // This will cause the
   cal.add(calField, -1);
  } while (cal.getTime().getTime() > dtr.getBegin().toGregorianCalendar()
    .getTimeInMillis());

  return result;

 }


Notes:

  1. Usage of Google Collect to produce collections.
  2. The DateTimeRange type holds a 'begin' and 'end' member fields. 
  3. The period(Date begin, Date end) is a factory for an instance of of type DateTimeRange. 
  4. The method ignores the beginning of the period, which precedes the intended boundary.
  5. The method getLastDayOfWeek(Calendar cal) looks like this:

public int getLastDayOfWeek(Calendar cal) {
  final int firstDayOfWeek = cal.getFirstDayOfWeek();

  final int lastDayOfWeek;
  if (firstDayOfWeek != 1) {
   lastDayOfWeek = firstDayOfWeek - 1; // One before the first day...
  } else {
   lastDayOfWeek = cal.getActualMaximum(Calendar.DAY_OF_WEEK); // Expect
  }
  return lastDayOfWeek;
 }