gt:       Date originalDate = cal.getTime();
context: /*
   * file:       ProjectCalendar.java
   * author:     Jon Iles
   * copyright:  (c) Packwood Software 2002-2003
   * date:       28/11/2003
   */
  
  /*
   * This library is free software; you can redistribute it and/or modify it
   * under the terms of the GNU Lesser General Public License as published by the
   * Free Software Foundation; either version 2.1 of the License, or (at your
   * option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with this library; if not, write to the Free Software Foundation, Inc.,
   * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
   */
  
  package net.sf.mpxj;
  
  import java.io.ByteArrayOutputStream;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Calendar;
  import java.util.Collections;
  import java.util.Date;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.WeakHashMap;
  
  import net.sf.mpxj.utility.DateUtility;
  import net.sf.mpxj.utility.NumberUtility;
  
  /**
   * This class represents the a Calendar Definition record. Both base calendars
   * and calendars derived from base calendars are represented by instances
   * of this class. The class is used to define the working and non-working days
   * of the week. The default calendar defines Monday to Friday as working days.
   */
  public final class ProjectCalendar extends ProjectCalendarWeek
  {
     /**
      * Default constructor.
      *
      * @param file the parent file to which this record belongs.
      */
     public ProjectCalendar(ProjectFile file)
     {
        m_projectFile = file;
  
        if (file.getAutoCalendarUniqueID() == true)
        {
           setUniqueID(Integer.valueOf(file.getCalendarUniqueID()));
        }
     }
  
     /**
      * Add an empty work week.
      * 
      * @return new work week
      */
     public ProjectCalendarWeek addWorkWeek()
     {
        ProjectCalendarWeek week = new ProjectCalendarWeek();
        week.setParent(this);
        m_workWeeks.add(week);
        return week;
     }
  
     /**
      * Retrieve the work weeks associated with this calendar.
      * 
      * @return list of work weeks
      */
     public List<ProjectCalendarWeek> getWorkWeeks()
     {
        return m_workWeeks;
     }
  
     /**
      * Used to add exceptions to the calendar. The MPX standard defines
      * a limit of 250 exceptions per calendar.
      *
      * @param fromDate exception start date
      * @param toDate exception end date
      * @return ProjectCalendarException instance
      */
     public ProjectCalendarException addCalendarException(Date fromDate, Date toDate)
     {
        ProjectCalendarException bce = new ProjectCalendarException(fromDate, toDate);
        m_exceptions.add(bce);
        m_exceptionsSorted = false;
        clearWorkingDateCache();
        return (bce);
     }
  
     /**
      * This method retrieves a list of exceptions to the current calendar.
      *
      * @return List of calendar exceptions
      */
     public List<ProjectCalendarException> getCalendarExceptions()
     {
        if (!m_exceptionsSorted)
        {
           Collections.sort(m_exceptions);
        }
        return (m_exceptions);
     }
  
     /**
      * Used to add working hours to the calendar. Note that the MPX file
      * definition allows a maximum of 7 calendar hours records to be added to
      * a single calendar.
      *
      * @param day day number
      * @return new ProjectCalendarHours instance
      */
     @Override public ProjectCalendarHours addCalendarHours(Day day)
     {
        clearWorkingDateCache();
        return super.addCalendarHours(day);
     }
  
     /**
      * Attaches a pre-existing set of hours to the correct
      * day within the calendar.
      *
      * @param hours calendar hours instance
      */
     @Override public void attachHoursToDay(ProjectCalendarHours hours)
     {
        clearWorkingDateCache();
        super.attachHoursToDay(hours);
     }
  
     /**
      * Removes a set of calendar hours from the day to which they
      * are currently attached.
      *
      * @param hours calendar hours instance
      */
     @Override public void removeHoursFromDay(ProjectCalendarHours hours)
     {
        clearWorkingDateCache();
        super.removeHoursFromDay(hours);
     }
  
     /**
      * Sets the ProjectCalendar instance from which this calendar is derived.
      *
      * @param calendar base calendar instance
      */
     public void setParent(ProjectCalendar calendar)
     {
        if (getParent() != null)
        {
           getParent().removeDerivedCalendar(this);
        }
  
        super.setParent(calendar);
  
        if (calendar != null)
        {
           calendar.addDerivedCalendar(this);
        }
        clearWorkingDateCache();
     }
  
     @Override public ProjectCalendar getParent()
     {
        return (ProjectCalendar) super.getParent();
     }
  
     /**
      * Method indicating whether a day is a working or non-working day.
      *
      * @param day required day
      * @return true if this is a working day
      */
     public boolean isWorkingDay(Day day)
     {
        DayType value = getWorkingDay(day);
        boolean result;
  
        if (value == DayType.DEFAULT)
        {
           ProjectCalendar cal = getParent();
           if (cal != null)
           {
              result = cal.isWorkingDay(day);
           }
           else
           {
              result = (day != Day.SATURDAY && day != Day.SUNDAY);
           }
        }
        else
        {
           result = (value == DayType.WORKING);
        }
  
        return (result);
     }
  
     /**
      * This method is provided to allow an absolute period of time
      * represented by start and end dates into a duration in working
      * days based on this calendar instance. This method takes account
      * of any exceptions defined for this calendar.
      *
      * @param startDate start of the period
      * @param endDate end of the period
      * @return new Duration object
      */
     public Duration getDuration(Date startDate, Date endDate)
     {
        Calendar cal = Calendar.getInstance();
        cal.setTime(startDate);
        int days = getDaysInRange(startDate, endDate);
        int duration = 0;
        Day day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
  
        while (days > 0)
        {
           if (isWorkingDate(cal.getTime(), day) == true)
           {
              ++duration;
           }
  
           --days;
           day = day.getNextDay();
           cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1);
        }
  
        return (Duration.getInstance(duration, TimeUnit.DAYS));
     }
  
     /**
      * Retrieves the time at which work starts on the given date, or returns
      * null if this is a non-working day.
      * 
      * @param date Date instance
      * @return start time, or null for non-working day
      */
     public Date getStartTime(Date date)
     {
        Date result = m_startTimeCache.get(date);
        if (result == null)
        {
           ProjectCalendarDateRanges ranges = getRanges(date, null, null);
           if (ranges == null)
           {
              result = getParentFile().getProjectHeader().getDefaultStartTime();
           }
           else
           {
              result = ranges.getRange(0).getStart();
           }
           result = DateUtility.getCanonicalTime(result);
           m_startTimeCache.put(new Date(date.getTime()), result);
        }
        return result;
     }
  
     /**
      * Retrieves the time at which work finishes on the given date, or returns
      * null if this is a non-working day.
      * 
      * @param date Date instance
      * @return finish time, or null for non-working day
      */
     public Date getFinishTime(Date date)
     {
        Date result = null;
  
        if (date != null)
        {
           ProjectCalendarDateRanges ranges = getRanges(date, null, null);
           if (ranges == null)
           {
              result = getParentFile().getProjectHeader().getDefaultEndTime();
              result = DateUtility.getCanonicalTime(result);
           }
           else
           {
              Date rangeStart = result = ranges.getRange(0).getStart();
              Date rangeFinish = ranges.getRange(ranges.getRangeCount() - 1).getEnd();
              Date startDay = DateUtility.getDayStartDate(rangeStart);
              Date finishDay = DateUtility.getDayStartDate(rangeFinish);
  
              result = DateUtility.getCanonicalTime(rangeFinish);
  
              //
              // Handle the case where the end of the range is at midnight -
              // this will show up as the start and end days not matching
              //
              if (startDay != null && finishDay != null && startDay.getTime() != finishDay.getTime())
              {
                 Calendar calendar = Calendar.getInstance();
                 calendar.setTime(result);
                 calendar.add(Calendar.DAY_OF_YEAR, 1);
                 result = calendar.getTime();
              }
           }
        }
        return result;
     }
  
     /**
      * Given a start date and a duration, this method calculates the
      * end date. It takes account of working hours in each day, non working
      * days and calendar exceptions. If the returnNextWorkStart parameter is
      * set to true, the method will return the start date and time of the next
      * working period if the end date is at the end of a working period.
      * 
      * @param startDate start date
      * @param duration duration
      * @param returnNextWorkStart if set to true will return start of next working period 
      * @return end date
      */
     public Date getDate(Date startDate, Duration duration, boolean returnNextWorkStart)
     {
        ProjectHeader header = getParentFile().getProjectHeader();
        // Note: Using a double allows us to handle date values that are accurate up to seconds.
        //       However, it also means we need to truncate the value to 2 decimals to make the
        //       comparisons work as sometimes the double ends up with some extra e.g. .0000000000003
        //       that wreak havoc on the comparisons.
        double remainingMinutes = NumberUtility.truncate(duration.convertUnits(TimeUnit.MINUTES, header).getDuration(), 2);
        Calendar cal = Calendar.getInstance();
        cal.setTime(startDate);
        Calendar endCal = Calendar.getInstance();
  
        while (remainingMinutes > 0)
        {
           //
           // Get the current date and time and determine how many 
           // working hours remain
           //
           Date currentDate = cal.getTime();
           endCal.setTime(currentDate);
           endCal.add(Calendar.DAY_OF_YEAR, 1);
           Date currentDateEnd = DateUtility.getDayStartDate(endCal.getTime());
           double currentDateWorkingMinutes = getWork(currentDate, currentDateEnd, TimeUnit.MINUTES).getDuration();
  
           //
           // We have more than enough hours left
           //
           if (remainingMinutes > currentDateWorkingMinutes)
           {
              //
              // Deduct this day's hours from our total
              //
              remainingMinutes = NumberUtility.truncate(remainingMinutes - currentDateWorkingMinutes, 2);
  
              //
              // Move the calendar forward to the next working day
              //            
              Day day;
              int nonWorkingDayCount = 0;
              do
              {
                 cal.add(Calendar.DAY_OF_YEAR, 1);
                 day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
                 ++nonWorkingDayCount;
                 if (nonWorkingDayCount > MAX_NONWORKING_DAYS)
                 {
                    cal.setTime(startDate);
                    cal.add(Calendar.DAY_OF_YEAR, 1);
                    remainingMinutes = 0;
                    break;
                 }
              }
              while (!isWorkingDate(cal.getTime(), day));
  
              //
              // Retrieve the start time for this day
              //
              Date startTime = getStartTime(cal.getTime());
              DateUtility.setTime(cal, startTime);
           }
           else
           {
              //
              // We have less hours to allocate than there are working hours
              // in this day. We need to calculate the time of day at which 
              // our work ends.
              //
              ProjectCalendarDateRanges ranges = getRanges(cal.getTime(), cal, null);
  
              //
              // Now we have the range of working hours for this day,
              // step through it to work out the end point
              //
              Date endTime = null;
              Date currentDateStartTime = DateUtility.getCanonicalTime(currentDate);
              boolean firstRange = true;
              for (DateRange range : ranges)
              {
                 //
                 // Skip this range if its end is before our start time
                 //
                 Date rangeStart = range.getStart();
                 Date rangeEnd = range.getEnd();
  
                 if (rangeStart == null || rangeEnd == null)
                 {
                    continue;
                 }
  
                 Date canonicalRangeEnd = DateUtility.getCanonicalTime(rangeEnd);
                 Date canonicalRangeStart = DateUtility.getCanonicalTime(rangeStart);
  
                 Date rangeStartDay = DateUtility.getDayStartDate(rangeStart);
                 Date rangeEndDay = DateUtility.getDayStartDate(rangeEnd);
  
                 if (rangeStartDay.getTime() != rangeEndDay.getTime())
                 {
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(canonicalRangeEnd);
                    calendar.add(Calendar.DAY_OF_YEAR, 1);
                    canonicalRangeEnd = calendar.getTime();
                 }
  
                 if (firstRange && canonicalRangeEnd.getTime() < currentDateStartTime.getTime())
                 {
                    continue;
                 }
  
                 //
                 // Move the start of the range if our current start is 
                 // past the range start
                 //               
                 if (firstRange && canonicalRangeStart.getTime() < currentDateStartTime.getTime())
                 {
                    canonicalRangeStart = currentDateStartTime;
                 }
                 firstRange = false;
  
                 double rangeMinutes;
  
                 rangeMinutes = canonicalRangeEnd.getTime() - canonicalRangeStart.getTime();
                 rangeMinutes /= (1000 * 60);
  
                 if (remainingMinutes > rangeMinutes)
                 {
                    remainingMinutes = NumberUtility.truncate(remainingMinutes - rangeMinutes, 2);
                 }
                 else
                 {
                    if (remainingMinutes == rangeMinutes)
                    {
                       endTime = canonicalRangeEnd;
                       if (rangeStartDay.getTime() != rangeEndDay.getTime())
                       {
                          // The range ends the next day, so let's adjust our date accordingly.
                          cal.add(Calendar.DAY_OF_YEAR, 1);
                       }
                    }
                    else
                    {
                       endTime = new Date((long) (canonicalRangeStart.getTime() + (remainingMinutes * (60 * 1000))));
                       returnNextWorkStart = false;
                    }
                    remainingMinutes = 0;
                    break;
                 }
              }
  
              DateUtility.setTime(cal, endTime);
           }
        }
  
        if (returnNextWorkStart)
        {
           updateToNextWorkStart(cal);
        }
  
        return cal.getTime();
     }
  
     /**
      * Given a finish date and a duration, this method calculates backwards to the 
      * start date. It takes account of working hours in each day, non working
      * days and calendar exceptions. 
      * 
      * @param finishDate finish date
      * @param duration duration
      * @return start date
      */
     public Date getStartDate(Date finishDate, Duration duration)
     {
        ProjectHeader header = getParentFile().getProjectHeader();
        // Note: Using a double allows us to handle date values that are accurate up to seconds.
        //       However, it also means we need to truncate the value to 2 decimals to make the
        //       comparisons work as sometimes the double ends up with some extra e.g. .0000000000003
        //       that wreak havoc on the comparisons.
        double remainingMinutes = NumberUtility.truncate(duration.convertUnits(TimeUnit.MINUTES, header).getDuration(), 2);
        Calendar cal = Calendar.getInstance();
        cal.setTime(finishDate);
        Calendar startCal = Calendar.getInstance();
  
        while (remainingMinutes > 0)
        {
           //
           // Get the current date and time and determine how many 
           // working hours remain
           //
           Date currentDate = cal.getTime();
           startCal.setTime(currentDate);
           startCal.add(Calendar.DAY_OF_YEAR, -1);
           Date currentDateEnd = DateUtility.getDayEndDate(startCal.getTime());
           double currentDateWorkingMinutes = getWork(currentDateEnd, currentDate, TimeUnit.MINUTES).getDuration();
  
           //
           // We have more than enough hours left
           //
           if (remainingMinutes > currentDateWorkingMinutes)
           {
              //
              // Deduct this day's hours from our total
              //
              remainingMinutes = NumberUtility.truncate(remainingMinutes - currentDateWorkingMinutes, 2);
  
              //
              // Move the calendar backward to the previous working day
              //            
              int count = 0;
              Day day;
              do
              {
                 if (count > 7)
                 {
                    break; // Protect against a calendar with all days non-working
                 }
                 count++;
                 cal.add(Calendar.DAY_OF_YEAR, -1);
                 day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
              }
              while (!isWorkingDate(cal.getTime(), day));
  
              if (count > 7)
              {
                 // We have a calendar with no working days.
                 return null;
              }
  
              //
              // Retrieve the finish time for this day
              //
              Date finishTime = getFinishTime(cal.getTime());
              DateUtility.setTime(cal, finishTime);
           }
           else
           {
              //
              // We have less hours to allocate than there are working hours
              // in this day. We need to calculate the time of day at which 
              // our work starts.
              //
              ProjectCalendarDateRanges ranges = getRanges(cal.getTime(), cal, null);
  
              //
              // Now we have the range of working hours for this day,
              // step through it to work out the start point
              //
              Date startTime = null;
              Date currentDateFinishTime = DateUtility.getCanonicalTime(currentDate);
              boolean firstRange = true;
              // Traverse from end to start
              for (int i = ranges.getRangeCount() - 1; i >= 0; i--)
              {
                 DateRange range = ranges.getRange(i);
                 //
                 // Skip this range if its start is after our end time
                 //
                 Date rangeStart = range.getStart();
                 Date rangeEnd = range.getEnd();
  
                 if (rangeStart == null || rangeEnd == null)
                 {
                    continue;
                 }
  
                 Date canonicalRangeEnd = DateUtility.getCanonicalTime(rangeEnd);
                 Date canonicalRangeStart = DateUtility.getCanonicalTime(rangeStart);
  
                 Date rangeStartDay = DateUtility.getDayStartDate(rangeStart);
                 Date rangeEndDay = DateUtility.getDayStartDate(rangeEnd);
  
                 if (rangeStartDay.getTime() != rangeEndDay.getTime())
                 {
                    Calendar calendar = Calendar.getInstance();
                    calendar.setTime(canonicalRangeEnd);
                    calendar.add(Calendar.DAY_OF_YEAR, 1);
                    canonicalRangeEnd = calendar.getTime();
                 }
  
                 if (firstRange && canonicalRangeStart.getTime() > currentDateFinishTime.getTime())
                 {
                    continue;
                 }
  
                 //
                 // Move the end of the range if our current end is 
                 // before the range end
                 //               
                 if (firstRange && canonicalRangeEnd.getTime() > currentDateFinishTime.getTime())
                 {
                    canonicalRangeEnd = currentDateFinishTime;
                 }
                 firstRange = false;
  
                 double rangeMinutes;
  
                 rangeMinutes = canonicalRangeEnd.getTime() - canonicalRangeStart.getTime();
                 rangeMinutes /= (1000 * 60);
  
                 if (remainingMinutes > rangeMinutes)
                 {
                    remainingMinutes = NumberUtility.truncate(remainingMinutes - rangeMinutes, 2);
                 }
                 else
                 {
                    if (remainingMinutes == rangeMinutes)
                    {
                       startTime = canonicalRangeStart;
                    }
                    else
                    {
                       startTime = new Date((long) (canonicalRangeEnd.getTime() - (remainingMinutes * (60 * 1000))));
                    }
                    remainingMinutes = 0;
                    break;
                 }
              }
  
              DateUtility.setTime(cal, startTime);
           }
        }
  
        return cal.getTime();
     }
  
     /**
      * This method finds the start of the next working period.
      * 
      * @param cal current Calendar instance
      */
     private void updateToNextWorkStart(Calendar cal)
     {
  