/*
* 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.common.DateHelper;
import net.sf.mpxj.common.NumberHelper;
/**
* 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 implements ProjectEntityWithUniqueID
{
/**
* Default constructor.
*
* @param file the parent file to which this record belongs.
*/
public ProjectCalendar(ProjectFile file)
{
m_projectFile = file;
if (file.getProjectConfig().getAutoCalendarUniqueID() == true)
{
setUniqueID(Integer.valueOf(file.getProjectConfig().getNextCalendarUniqueID()));
}
}
/**
* 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().getProjectProperties().getDefaultStartTime();
}
else
{
result = ranges.getRange(0).getStart();
}
result = DateHelper.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().getProjectProperties().getDefaultEndTime();
result = DateHelper.getCanonicalTime(result);
}
else
{
Date rangeStart = result = ranges.getRange(0).getStart();
Date rangeFinish = ranges.getRange(ranges.getRangeCount() - 1).getEnd();
Date startDay = DateHelper.getDayStartDate(rangeStart);
Date finishDay = DateHelper.getDayStartDate(rangeFinish);
result = DateHelper.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)
{
ProjectProperties properties = getParentFile().getProjectProperties();
// 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 = NumberHelper.truncate(duration.convertUnits(TimeUnit.MINUTES, properties).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 = DateHelper.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 = NumberHelper.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());
DateHelper.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 = DateHelper.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 = DateHelper.getCanonicalTime(rangeEnd);
Date canonicalRangeStart = DateHelper.getCanonicalTime(rangeStart);
Date rangeStartDay = DateHelper.getDayStartDate(rangeStart);
Date rangeEndDay = DateHelper.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 = NumberHelper.truncate(remainingMinutes - rangeMinutes, 2);
}
else
{
if (Duration.durationValueEquals(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;
}
}
DateHelper.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)
{
ProjectProperties properties = getParentFile().getProjectProperties();
// 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 = NumberHelper.truncate(duration.convertUnits(TimeUnit.MINUTES, properties).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 = DateHelper.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 = NumberHelper.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());
DateHelper.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 = DateHelper.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 = DateHelper.getCanonicalTime(rangeEnd);
Date canonicalRangeStart = DateHelper.getCanonicalTime(rangeStart);
Date rangeStartDay = DateHelper.getDayStartDate(rangeStart);
Date rangeEndDay = DateHelper.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 = NumberHelper.truncate(remainingMinutes - rangeMinutes, 2);
}
else
{
if (Duration.durationValueEquals(remainingMinutes, rangeMinutes))
{
startTime = canonicalRangeStart;
}
else
{
startTime = new Date((long) (canonicalRangeEnd.getTime() - (remainingMinutes * (60 * 1000))));
}
remainingMinutes = 0;
break;
}
}
DateHelper.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)
{
Date originalDate = cal.getTime();
//
// Find the date ranges for the current day
//
ProjectCalendarDateRanges ranges = getRanges(originalDate, cal, null);
if (ranges != null)
{
//
// Do we have a start time today?
//
Date calTime = DateHelper.getCanonicalTime(cal.getTime());
Date startTime = null;
for (DateRange range : ranges)
{
Date rangeStart = DateHelper.getCanonicalTime(range.getStart());
Date rangeEnd = DateHelper.getCanonicalTime(range.getEnd());
Date rangeStartDay = DateHelper.getDayStartDate(range.getStart());
Date rangeEndDay = DateHelper.getDayStartDate(range.getEnd());
if (rangeStartDay.getTime() != rangeEndDay.getTime())
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(rangeEnd);
calendar.add(Calendar.DAY_OF_YEAR, 1);
rangeEnd = calendar.getTime();
}
if (calTime.getTime() < rangeEnd.getTime())
{
if (calTime.getTime() > rangeStart.getTime())
{
startTime = calTime;
}
else
{
startTime = rangeStart;
}
break;
}
}
//
// If we don't have a start time today - find the next working day
// then retrieve the start time.
//
if (startTime == null)
{
Day day;
int nonWorkingDayCount = 0;
do
{
cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) + 1);
day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
++nonWorkingDayCount;
if (nonWorkingDayCount > MAX_NONWORKING_DAYS)
{
cal.setTime(originalDate);
break;
}
}
while (!isWorkingDate(cal.getTime(), day));
startTime = getStartTime(cal.getTime());
}
DateHelper.setTime(cal, startTime);
}
}
/**
* This method finds the finish of the previous working period.
*
* @param cal current Calendar instance
*/
private void updateToPreviousWorkFinish(Calendar cal)
{
Date originalDate = cal.getTime();
//
// Find the date ranges for the current day
//
ProjectCalendarDateRanges ranges = getRanges(originalDate, cal, null);
if (ranges != null)
{
//
// Do we have a start time today?
//
Date calTime = DateHelper.getCanonicalTime(cal.getTime());
Date finishTime = null;
for (DateRange range : ranges)
{
Date rangeEnd = DateHelper.getCanonicalTime(range.getEnd());
Date rangeStartDay = DateHelper.getDayStartDate(range.getStart());
Date rangeEndDay = DateHelper.getDayStartDate(range.getEnd());
if (rangeStartDay.getTime() != rangeEndDay.getTime())
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(rangeEnd);
calendar.add(Calendar.DAY_OF_YEAR, 1);
rangeEnd = calendar.getTime();
}
if (calTime.getTime() >= rangeEnd.getTime())
{
finishTime = rangeEnd;
break;
}
}
//
// If we don't have a finish time today - find the previous working day
// then retrieve the finish time.
//
if (finishTime == null)
{
Day day;
int nonWorkingDayCount = 0;
do
{
cal.set(Calendar.DAY_OF_YEAR, cal.get(Calendar.DAY_OF_YEAR) - 1);
day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
++nonWorkingDayCount;
if (nonWorkingDayCount > MAX_NONWORKING_DAYS)
{
cal.setTime(originalDate);
break;
}
}
while (!isWorkingDate(cal.getTime(), day));
finishTime = getFinishTime(cal.getTime());
}
DateHelper.setTime(cal, finishTime);
}
}
/**
* Utility method to retrieve the next working date start time, given
* a date and time as a starting point.
*
* @param date date and time start point
* @return date and time of next work start
*/
public Date getNextWorkStart(Date date)
{
Calendar cal = Calendar.getInstance();
cal.setTime(date);
updateToNextWorkStart(cal);
return cal.getTime();
}
/**
* Utility method to retrieve the previous working date finish time, given
* a date and time as a starting point.
*
* @param date date and time start point
* @return date and time of previous work finish
*/
public Date getPreviousWorkFinish(Date date)
{
Calendar cal = Calendar.getInstance();
cal.setTime(date);
updateToPreviousWorkFinish(cal);
return cal.getTime();
}
/**
* This method allows the caller to determine if a given date is a
* working day. This method takes account of calendar exceptions.
*
* @param date Date to be tested
* @return boolean value
*/
public boolean isWorkingDate(Date date)
{
Calendar cal = Calendar.getInstance();
cal.setTime(date);
Day day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
return (isWorkingDate(date, day));
}
/**
* This private method allows the caller to determine if a given date is a
* working day. This method takes account of calendar exceptions. It assumes
* that the caller has already calculated the day of the week on which
* the given day falls.
*
* @param date Date to be tested
* @param day Day of the week for the date under test
* @return boolean flag
*/
private boolean isWorkingDate(Date date, Day day)
{
ProjectCalendarDateRanges ranges = getRanges(date, null, day);
return ranges.getRangeCount() != 0;
}
/**
* This method calculates the absolute number of days between two dates.
* Note that where two date objects are provided that fall on the same
* day, this method will return one not zero. Note also that this method
* assumes that the dates are passed in the correct order, i.e.
* startDate < endDate.
*
* @param startDate Start date
* @param endDate End date
* @return number of days in the date range
*/
private int getDaysInRange(Date startDate, Date endDate)
{
int result;
Calendar cal = Calendar.getInstance();
cal.setTime(endDate);
int endDateYear = cal.get(Calendar.YEAR);
int endDateDayOfYear = cal.get(Calendar.DAY_OF_YEAR);
cal.setTime(startDate);
if (endDateYear == cal.get(Calendar.YEAR))
{
result = (endDateDayOfYear - cal.get(Calendar.DAY_OF_YEAR)) + 1;
}
else
{
result = 0;
do
{
result += (cal.getActualMaximum(Calendar.DAY_OF_YEAR) - cal.get(Calendar.DAY_OF_YEAR)) + 1;
cal.roll(Calendar.YEAR, 1);
cal.set(Calendar.DAY_OF_YEAR, 1);
}
while (cal.get(Calendar.YEAR) < endDateYear);
result += endDateDayOfYear;
}
return (result);
}
/**
* Modifier method to set the unique ID of this calendar.
*
* @param uniqueID unique identifier
*/
@Override public void setUniqueID(Integer uniqueID)
{
ProjectFile parent = getParentFile();
if (m_uniqueID != null)
{
parent.getCalendars().unmapUniqueID(m_uniqueID);
}
parent.getCalendars().mapUniqueID(uniqueID, this);
m_uniqueID = uniqueID;
}
/**
* Accessor method to retrieve the unique ID of this calendar.
*
* @return calendar unique identifier
*/
@Override public Integer getUniqueID()
{
return (m_uniqueID);
}
/**
* Retrieve the resource to which this calendar is linked.
*
* @return resource instance
*/
public Resource getResource()
{
return (m_resource);
}
/**
* Sets the resource to which this calendar is linked. Note that this
* method updates the calendar's name to be the same as the resource name.
* If the resource does not yet have a name, then the calendar is given
* a default name.
*
* @param resource resource instance
*/
public void setResource(Resource resource)
{
m_resource = resource;
String name = m_resource.getName();
if (name == null || name.length() == 0)
{
name = "Unnamed Resource";
}
setName(name);
}
/**
* Removes this calendar from the project.
*/
public void remove()
{
getParentFile().removeCalendar(this);
}
/**
* Retrieve a calendar calendar exception which applies to this date.
*
* @param date target date
* @return calendar exception, or null if none match this date
*/
public ProjectCalendarException getException(Date date)
{
ProjectCalendarException exception = null;
if (!m_exceptions.isEmpty())
{
if (!m_exceptionsSorted)
{
Collections.sort(m_exceptions);
m_exceptionsSorted = true;
}
int low = 0;
int high = m_exceptions.size() - 1;
long targetDate = date.getTime();
while (low <= high)
{
int mid = (low + high) >>> 1;
ProjectCalendarException midVal = m_exceptions.get(mid);
int cmp = 0 - DateHelper.compare(midVal.getFromDate(), midVal.getToDate(), targetDate);
if (cmp < 0)
{
low = mid + 1;
}
else
{
if (cmp > 0)
{
high = mid - 1;
}
else
{
exception = midVal;
break;
}
}
}
}
if (exception == null && getParent() != null)
{
// Check base calendar as well for an exception.
exception = getParent().getException(date);
}
return (exception);
}
/**
* Retrieves the amount of work on a given day, and
* returns it in the specified format.
*
* @param date target date
* @param format required format
* @return work duration
*/
public Duration getWork(Date date, TimeUnit format)
{
ProjectCalendarDateRanges ranges = getRanges(date, null, null);
long time = getTotalTime(ranges);
return convertFormat(time, format);
}
/**
* This method retrieves a Duration instance representing the amount of
* work between two dates based on this calendar.
*
* @param startDate start date
* @param endDate end date
* @param format required duration format
* @return amount of work
*/
public Duration getWork(Date startDate, Date endDate, TimeUnit format)
{
DateRange range = new DateRange(startDate, endDate);
Long cachedResult = m_workingDateCache.get(range);
long totalTime = 0;
if (cachedResult == null)
{
//
// We want the start date to be the earliest date, and the end date
// to be the latest date. Set a flag here to indicate if we have swapped
// the order of the supplied date.
//
boolean invert = false;
if (startDate.getTime() > endDate.getTime())
{
invert = true;
Date temp = startDate;
startDate = endDate;
endDate = temp;
}
Date canonicalStartDate = DateHelper.getDayStartDate(startDate);
Date canonicalEndDate = DateHelper.getDayStartDate(endDate);
if (canonicalStartDate.getTime() == canonicalEndDate.getTime())
{
ProjectCalendarDateRanges ranges = getRanges(startDate, null, null);
if (ranges.getRangeCount() != 0)
{
totalTime = getTotalTime(ranges, startDate, endDate);
}
}
else
{
//
// Find the first working day in the range
//
Date currentDate = startDate;
Calendar cal = Calendar.getInstance();
cal.setTime(startDate);
Day day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
while (isWorkingDate(currentDate, day) == false && currentDate.getTime() < canonicalEndDate.getTime())
{
cal.add(Calendar.DAY_OF_YEAR, 1);
currentDate = cal.getTime();
day = day.getNextDay();
}
if (currentDate.getTime() < canonicalEndDate.getTime())
{
//
// Calculate the amount of working time for this day
//
totalTime += getTotalTime(getRanges(currentDate, null, day), currentDate, true);
//
// Process each working day until we reach the last day
//
while (true)
{
cal.add(Calendar.DAY_OF_YEAR, 1);
currentDate = cal.getTime();
day = day.getNextDay();
//
// We have reached the last day
//
if (currentDate.getTime() >= canonicalEndDate.getTime())
{
break;
}
//
// Skip this day if it has no working time
//
ProjectCalendarDateRanges ranges = getRanges(currentDate, null, day);
if (ranges.getRangeCount() == 0)
{
continue;
}
//
// Add the working time for the whole day
//
totalTime += getTotalTime(ranges);
}
}
//
// We are now at the last day
//
ProjectCalendarDateRanges ranges = getRanges(endDate, null, day);
if (ranges.getRangeCount() != 0)
{
totalTime += getTotalTime(ranges, DateHelper.getDayStartDate(endDate), endDate);
}
}
if (invert)
{
totalTime = -totalTime;
}
m_workingDateCache.put(range, Long.valueOf(totalTime));
}
else
{
totalTime = cachedResult.longValue();
}
return convertFormat(totalTime, format);
}
/**
* Utility method used to convert an integer time representation into a
* Duration instance.
*
* @param totalTime integer time representation
* @param format required time format
* @return new Duration instance
*/
private Duration convertFormat(long totalTime, TimeUnit format)
{
double duration = totalTime;
double minutesPerDay = getParentFile().getProjectProperties().getMinutesPerDay().doubleValue();
double minutesPerWeek = getParentFile().getProjectProperties().getMinutesPerWeek().doubleValue();
double daysPerMonth = getParentFile().getProjectProperties().getDaysPerMonth().doubleValue();
switch (format)
{
case MINUTES:
case ELAPSED_MINUTES:
{
duration /= (60 * 1000);
break;
}
case HOURS:
case ELAPSED_HOURS:
{
duration /= (60 * 60 * 1000);
break;
}
case DAYS:
{
if (minutesPerDay != 0)
{
duration /= (minutesPerDay * 60 * 1000);
}
else
{
duration = 0;
}
break;
}
case WEEKS:
{
if (minutesPerWeek != 0)
{
duration /= (minutesPerWeek * 60 * 1000);
}
else
{
duration = 0;
}
break;
}
case MONTHS:
{
if (daysPerMonth != 0 && minutesPerDay != 0)
{
duration /= (daysPerMonth * minutesPerDay * 60 * 1000);
}
else
{
duration = 0;
}
break;
}
case ELAPSED_DAYS:
{
duration /= (24 * 60 * 60 * 1000);
break;
}
case ELAPSED_WEEKS:
{
duration /= (7 * 24 * 60 * 60 * 1000);
break;
}
case ELAPSED_MONTHS:
{
duration /= (30 * 24 * 60 * 60 * 1000);
break;
}
default:
{
throw new IllegalArgumentException("TimeUnit " + format + " not supported");
}
}
return (Duration.getInstance(duration, format));
}
/**
* Retrieves the amount of time represented by a calendar exception
* before or after an intersection point.
*
* @param exception calendar exception
* @param date intersection time
* @param after true to report time after intersection, false to report time before
* @return length of time in milliseconds
*/
private long getTotalTime(ProjectCalendarDateRanges exception, Date date, boolean after)
{
long currentTime = DateHelper.getCanonicalTime(date).getTime();
long total = 0;
for (DateRange range : exception)
{
total += getTime(range.getStart(), range.getEnd(), currentTime, after);
}
return (total);
}
/**
* Retrieves the amount of working time represented by
* a calendar exception.
*
* @param exception calendar exception
* @return length of time in milliseconds
*/
private long getTotalTime(ProjectCalendarDateRanges exception)
{
long total = 0;
for (DateRange range : exception)
{
total += getTime(range.getStart(), range.getEnd());
}
return (total);
}
/**
* This method calculates the total amount of working time in a single
* day, which intersects with the supplied time range.
*
* @param hours collection of working hours in a day
* @param startDate time range start
* @param endDate time range end
* @return length of time in milliseconds
*/
private long getTotalTime(ProjectCalendarDateRanges hours, Date startDate, Date endDate)
{
long total = 0;
if (startDate.getTime() != endDate.getTime())
{
Date start = DateHelper.getCanonicalTime(startDate);
Date end = DateHelper.getCanonicalTime(endDate);
for (DateRange range : hours)
{
Date rangeStart = range.getStart();
Date rangeEnd = range.getEnd();
if (rangeStart != null && rangeEnd != null)
{
Date canoncialRangeStart = DateHelper.getCanonicalTime(rangeStart);
Date canonicalRangeEnd = DateHelper.getCanonicalTime(rangeEnd);
Date startDay = DateHelper.getDayStartDate(rangeStart);
Date finishDay = DateHelper.getDayStartDate(rangeEnd);
//
// 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.getTime() != finishDay.getTime())
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(canonicalRangeEnd);
calendar.add(Calendar.DAY_OF_YEAR, 1);
canonicalRangeEnd = calendar.getTime();
}
if (canoncialRangeStart.getTime() == canonicalRangeEnd.getTime() && rangeEnd.getTime() > rangeStart.getTime())
{
total += (24 * 60 * 60 * 1000);
}
else
{
total += getTime(start, end, canoncialRangeStart, canonicalRangeEnd);
}
}
}
}
return (total);
}
/**
* Calculates how much of a time range is before or after a
* target intersection point.
*
* @param start time range start
* @param end time range end
* @param target target intersection point
* @param after true if time after target required, false for time before
* @return length of time in milliseconds
*/
private long getTime(Date start, Date end, long target, boolean after)
{
long total = 0;
if (start != null && end != null)
{
Date startTime = DateHelper.getCanonicalTime(start);
Date endTime = DateHelper.getCanonicalTime(end);
Date startDay = DateHelper.getDayStartDate(start);
Date finishDay = DateHelper.getDayStartDate(end);
//
// 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.getTime() != finishDay.getTime())
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(endTime);
calendar.add(Calendar.DAY_OF_YEAR, 1);
endTime = calendar.getTime();
}
int diff = DateHelper.compare(startTime, endTime, target);
if (diff == 0)
{
if (after == true)
{
total = (endTime.getTime() - target);
}
else
{
total = (target - startTime.getTime());
}
}
else
{
if ((after == true && diff < 0) || (after == false && diff > 0))
{
total = (endTime.getTime() - startTime.getTime());
}
}
}
return (total);
}
/**
* Retrieves the amount of time between two date time values. Note that
* these values are converted into canonical values to remove the
* date component.
*
* @param start start time
* @param end end time
* @return length of time
*/
private long getTime(Date start, Date end)
{
long total = 0;
if (start != null && end != null)
{
Date startTime = DateHelper.getCanonicalTime(start);
Date endTime = DateHelper.getCanonicalTime(end);
Date startDay = DateHelper.getDayStartDate(start);
Date finishDay = DateHelper.getDayStartDate(end);
//
// 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.getTime() != finishDay.getTime())
{
Calendar calendar = Calendar.getInstance();
calendar.setTime(endTime);
calendar.add(Calendar.DAY_OF_YEAR, 1);
endTime = calendar.getTime();
}
total = (endTime.getTime() - startTime.getTime());
}
return (total);
}
/**
* This method returns the length of overlapping time between two time
* ranges.
*
* @param start1 start of first range
* @param end1 end of first range
* @param start2 start start of second range
* @param end2 end of second range
* @return overlapping time in milliseconds
*/
private long getTime(Date start1, Date end1, Date start2, Date end2)
{
long total = 0;
if (start1 != null && end1 != null && start2 != null && end2 != null)
{
long start;
long end;
if (start1.getTime() < start2.getTime())
{
start = start2.getTime();
}
else
{
start = start1.getTime();
}
if (end1.getTime() < end2.getTime())
{
end = end1.getTime();
}
else
{
end = end2.getTime();
}
if (start < end)
{
total = end - start;
}
}
return (total);
}
/**
* Add a reference to a calendar derived from this one.
*
* @param calendar derived calendar instance
*/
protected void addDerivedCalendar(ProjectCalendar calendar)
{
m_derivedCalendars.add(calendar);
}
/**
* Remove a reference to a derived calendar.
*
* @param calendar derived calendar instance
*/
protected void removeDerivedCalendar(ProjectCalendar calendar)
{
m_derivedCalendars.remove(calendar);
}
/**
* Retrieve a list of derived calendars.
*
* @return list of derived calendars
*/
public List<ProjectCalendar> getDerivedCalendars()
{
return (m_derivedCalendars);
}
/**
* {@inheritDoc}
*/
@Override public String toString()
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.println("[ProjectCalendar");
pw.println(" ID=" + m_uniqueID);
pw.println(" name=" + getName());
pw.println(" baseCalendarName=" + (getParent() == null ? "" : getParent().getName()));
pw.println(" resource=" + (m_resource == null ? "" : m_resource.getName()));
String[] dayName =
{
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
};
for (int loop = 0; loop < 7; loop++)
{
pw.println(" [Day " + dayName[loop]);
pw.println(" type=" + getDays()[loop]);
pw.println(" hours=" + getHours()[loop]);
pw.println(" ]");
}
if (!m_exceptions.isEmpty())
{
pw.println(" [Exceptions=");
for (ProjectCalendarException ex : m_exceptions)
{
pw.println(" " + ex.toString());
}
pw.println(" ]");
}
pw.println("]");
pw.flush();
return (os.toString());
}
/**
* Create a calendar based on the intersection of a task calendar and a resource calendar.
*
* @param file the parent file to which this record belongs.
* @param taskCalendar task calendar to merge
* @param resourceCalendar resource calendar to merge
*/
public ProjectCalendar(ProjectFile file, ProjectCalendar taskCalendar, ProjectCalendar resourceCalendar)
{
m_projectFile = file;
// Set the resource
setResource(resourceCalendar.getResource());
// Merge the exceptions
// Merge the hours
for (int i = 1; i <= 7; i++)
{
Day day = Day.getInstance(i);
// Set working/non-working days
setWorkingDay(day, taskCalendar.isWorkingDay(day) && resourceCalendar.isWorkingDay(day));
ProjectCalendarHours hours = addCalendarHours(day);
int taskIndex = 0;
int resourceIndex = 0;
ProjectCalendarHours taskHours = taskCalendar.getHours(day);
ProjectCalendarHours resourceHours = resourceCalendar.getHours(day);
DateRange range1 = null;
DateRange range2 = null;
Date start = null;
Date end = null;
Date start1 = null;
Date start2 = null;
Date end1 = null;
Date end2 = null;
while (true)
{
// Find next range start
if (taskHours.getRangeCount() > taskIndex)
{
range1 = taskHours.getRange(taskIndex);
}
else
{
break;
}
if (resourceHours.getRangeCount() > resourceIndex)
{
range2 = resourceHours.getRange(resourceIndex);
}
else
{
break;
}
start1 = range1.getStart();
start2 = range2.getStart();
end1 = range1.getEnd();
end2 = range2.getEnd();
// Get the later start
if (start1.compareTo(start2) > 0)
{
start = start1;
}
else
{
start = start2;
}
// Get the earlier end
if (end1.compareTo(end2) < 0)
{
end = end1;
taskIndex++;
}
else
{
end = end2;
resourceIndex++;
}
if (end != null && end.compareTo(start) > 0)
{
// Found a block
hours.addRange(new DateRange(start, end));
}
}
}
// For now just combine the exceptions. Probably overkill (although would be more accurate) to also merge the exceptions.
m_exceptions.addAll(taskCalendar.getCalendarExceptions());
m_exceptions.addAll(resourceCalendar.getCalendarExceptions());
m_exceptionsSorted = false;
}
/**
* Copy the settings from another calendar to this calendar.
*
* @param cal calendar data source
*/
public void copy(ProjectCalendar cal)
{
setName(cal.getName());
setParent(cal.getParent());
System.arraycopy(cal.getDays(), 0, getDays(), 0, getDays().length);
for (ProjectCalendarException ex : cal.m_exceptions)
{
addCalendarException(ex.getFromDate(), ex.getToDate());
for (DateRange range : ex)
{
ex.addRange(new DateRange(range.getStart(), range.getEnd()));
}
}
for (ProjectCalendarHours hours : getHours())
{
if (hours != null)
{
ProjectCalendarHours copyHours = cal.addCalendarHours(hours.getDay());
for (DateRange range : hours)
{
copyHours.addRange(new DateRange(range.getStart(), range.getEnd()));
}
}
}
}
/**
* Utility method to clear cached calendar data.
*/
private void clearWorkingDateCache()
{
m_workingDateCache.clear();
m_startTimeCache.clear();
for (ProjectCalendar calendar : m_derivedCalendars)
{
calendar.clearWorkingDateCache();
}
}
/**
* Retrieves the working hours on the given date.
*
* @param date required date
* @param cal optional calendar instance
* @param day optional day instance
* @return working hours
*/
private ProjectCalendarDateRanges getRanges(Date date, Calendar cal, Day day)
{
ProjectCalendarDateRanges ranges = getException(date);
if (ranges == null)
{
if (day == null)
{
if (cal == null)
{
cal = Calendar.getInstance();
cal.setTime(date);
}
day = Day.getInstance(cal.get(Calendar.DAY_OF_WEEK));
}
ranges = getHours(day);
}
return ranges;
}
/**
* Accessor method allowing retrieval of ProjectFile reference.
*
* @return reference to this the parent ProjectFile instance
*/
public final ProjectFile getParentFile()
{
return (m_projectFile);
}
/**
* Reference to parent ProjectFile.
*/
private ProjectFile m_projectFile;
/**
* Unique identifier of this calendar.
*/
private Integer m_uniqueID = Integer.valueOf(0);
/**
* List of exceptions to the base calendar.
*/
private List<ProjectCalendarException> m_exceptions = new LinkedList<ProjectCalendarException>();
/**
* Flag indicating if the list of exceptions is sorted.
*/
private boolean m_exceptionsSorted;
/**
* This resource to which this calendar is attached.
*/
private Resource m_resource;
/**
* List of calendars derived from this calendar instance.
*/
private ArrayList<ProjectCalendar> m_derivedCalendars = new ArrayList<ProjectCalendar>();
/**
* Caches used to speed up date calculations.
*/
private Map<DateRange, Long> m_workingDateCache = new WeakHashMap<DateRange, Long>();
private Map<Date, Date> m_startTimeCache = new WeakHashMap<Date, Date>();
/**
* Work week definitions.
*/
private ArrayList<ProjectCalendarWeek> m_workWeeks = new ArrayList<ProjectCalendarWeek>();
/**
* Default base calendar name to use when none is supplied.
*/
public static final String DEFAULT_BASE_CALENDAR_NAME = "Standard";
/**
* It is possible for a project calendar to be configured with no working
* days. This will result in an infinite loop when looking for the next
* working day from a date, so we use this constant to set a limit on the
* maximum number of non-working days we'll skip before we bail out
* and take an alternative approach.
*/
private static final int MAX_NONWORKING_DAYS = 1000;
}