/*
* $Id$
*
* Copyright 2006, The jCoderZ.org Project. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials
* provided with the distribution.
* * Neither the name of the jCoderZ.org Project nor the names of
* its contributors may be used to endorse or promote products
* derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jcoderz.commons.types;
import java.io.Serializable;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import org.jcoderz.commons.ArgumentMalformedException;
import org.jcoderz.commons.util.Assert;
/**
* Encapsulates the Year Month type.
* Instances of this class are immutable.
* Years before 1 are not fully supported and might result in wrong string
* representations. Not parseable time zones might be ignored.
* @author Andreas Mandel
*/
public final class YearMonth
implements Serializable
{
/** The name of this type. */
public static final String TYPE_NAME = "YearMonth";
/** Minimum length for the year. */
public static final int MINIMUM_NUMBER_OF_YEAR_DIGITS = 4;
/** Fixed length for the month. */
public static final int MONTH_LENGTH = 2;
private static final int TWO_DIGIT_MONTH = 10;
/** The <code>serialVersionUID</code>. */
private static final long serialVersionUID = 1L;
private final int mYear;
/** Month counting from 1 (January) to 12 (December). */
private final int mMonth;
// Lazy init members
private transient Date mEndDate;
private transient Date mStartDate;
private transient int mHashCode;
private transient String mString;
private transient Period mPeriod;
/**
*
*/
private YearMonth (int year, int month)
{
if (1 > month || month > Date.MONTH_PER_YEAR)
{
throw new ArgumentMalformedException(TYPE_NAME, String.valueOf(month),
"Month must be between 1 and 12.");
}
if (year == 0)
{
throw new ArgumentMalformedException(TYPE_NAME, String.valueOf(year),
"Value of Year must not be 0.");
}
mYear = year;
mMonth = month;
}
/**
* Parses a valid XML representation of the gYearMonth type.
* The format is <tt>CCYY-MM</tt>.
* @param str the string representing the year month.
* @return a YearMonth object representing the given year month.
*/
public static YearMonth fromString (String str)
{
Assert.notNull(str, TYPE_NAME);
// find separating '-' char.
final int minusPos = str.indexOf('-', 1);
if (minusPos == -1)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"MonthYear type must contain a '-' character. (CCYY-MM)");
}
if ((minusPos + 1) < MINIMUM_NUMBER_OF_YEAR_DIGITS)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"MonthYear type have at least "
+ MINIMUM_NUMBER_OF_YEAR_DIGITS + " digits in front of the '-' "
+ "character. (CCYY-MM)");
}
final int year;
try
{
year = Integer.parseInt(str.substring(0, minusPos));
}
catch (NumberFormatException ex)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"Failed to parse year. (CCYY-MM)", ex);
}
if (str.length() - minusPos <= MONTH_LENGTH)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"Month must be 2 digits long. (CCYY-MM)");
}
final int month;
try
{
month = Integer.parseInt(str.substring(minusPos + 1,
minusPos + 1 + MONTH_LENGTH));
}
catch (NumberFormatException ex)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"Failed to parse month. (CCYY-MM)", ex);
}
if (str.length() > minusPos + 1 + MONTH_LENGTH)
{
// Check timezone
final String tz = str.substring(minusPos + 1 + MONTH_LENGTH);
final TimeZone timeZone = TimeZone.getTimeZone(tz);
if (timeZone.getRawOffset() != 0)
{
throw new ArgumentMalformedException(TYPE_NAME, str,
"Only UTC is supported as time zone, not '" + tz + "'.");
}
}
return new YearMonth(year, month);
}
/**
* Returns the valid date that represents the beginning of the
* month year type.
* The date in time is the first second in the month year.
* @return a date denoting the first second in the month year.
*/
public Date toStartDate ()
{
if (mStartDate == null)
{
final Calendar cal = Calendar.getInstance(Date.TIME_ZONE);
cal.setLenient(false);
cal.clear();
if (getYear() > 0)
{
cal.set(getYear(), getMonth() - 1, 1);
}
else
{
cal.set(-getYear(), getMonth() - 1, 1);
cal.set(Calendar.ERA, GregorianCalendar.BC);
}
cal.set(Calendar.DAY_OF_MONTH,
cal.getActualMinimum(Calendar.DAY_OF_MONTH));
cal.set(Calendar.HOUR_OF_DAY,
cal.getActualMinimum(Calendar.HOUR_OF_DAY));
cal.set(Calendar.MINUTE, cal.getActualMinimum(Calendar.MINUTE));
cal.set(Calendar.SECOND, cal.getActualMinimum(Calendar.SECOND));
cal.set(Calendar.MILLISECOND,
cal.getActualMinimum(Calendar.MILLISECOND));
mStartDate = new Date(cal.getTimeInMillis());
}
return mStartDate;
}
/**
* Returns the valid date that represents the end of the
* month year type.
* The date in time is the first second in the month year.
* @return a date denoting the first second in the month year.
*/
public Date toEndDate ()
{
if (mEndDate == null)
{
final Calendar cal = Calendar.getInstance(Date.TIME_ZONE);
cal.setLenient(false);
cal.clear();
if (getYear() > 0)
{
cal.set(getYear(), getMonth() - 1, 1);
}
else
{
cal.set(-getYear(), getMonth() - 1, 1);
cal.set(Calendar.ERA, GregorianCalendar.BC);
}
cal.set(Calendar.DAY_OF_MONTH,
cal.getActualMaximum(Calendar.DAY_OF_MONTH));
cal.set(Calendar.HOUR_OF_DAY,
cal.getActualMaximum(Calendar.HOUR_OF_DAY));
cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
cal.set(Calendar.MILLISECOND,
cal.getActualMaximum(Calendar.MILLISECOND));
mEndDate = new Date(cal.getTimeInMillis());
}
return mEndDate;
}
/**
* Returns this as period from start date to end date of this
* YearMonth.
* @return a period representing the time period of this year month.
*/
public Period toPeriod ()
{
if (mPeriod == null)
{
mPeriod = Period.createPeriod(toStartDate(), toEndDate());
}
return mPeriod;
}
/**
* Returns the month counting from 1 (January) to 12 (December).
* @return The month counting from 1 (January) to 12 (December).
*/
public int getMonth ()
{
return mMonth;
}
/**
* @return Returns the year.
*/
public int getYear ()
{
return mYear;
}
/**
* Returns true if the given date is within this year/month.
* @param date the point in time to check.
* @return true if the given date is within this year/month.
*/
public boolean isWithin (Date date)
{
final long current = date.getTime();
return toStartDate().getTime() <= current
&& current <= toEndDate().getTime();
}
/** {@inheritDoc} */
public String toString ()
{
if (mString == null)
{
String year;
if (mYear >= 0)
{
year = Integer.toString(mYear);
if (year.length() < MINIMUM_NUMBER_OF_YEAR_DIGITS)
{
year = "0000".substring(year.length()) + year;
}
}
else
{
year = Integer.toString(-mYear);
if (year.length() < MINIMUM_NUMBER_OF_YEAR_DIGITS)
{
year = "0000".substring(year.length()) + year;
}
year = "-" + year;
}
if (mMonth < TWO_DIGIT_MONTH)
{
mString = year + "-0" + Integer.toString(mMonth) + 'Z';
}
else
{
mString = year + '-' + Integer.toString(mMonth) + 'Z';
}
}
return mString;
}
/** {@inheritDoc} */
public int hashCode ()
{
if (mHashCode == 0)
{
mHashCode = mYear * Date.MONTH_PER_YEAR + mMonth;
}
return mHashCode;
}
/** {@inheritDoc} */
public boolean equals (Object o)
{
final boolean result;
if (o instanceof YearMonth)
{
final YearMonth other = (YearMonth) o;
result = other.mMonth == mMonth && other.mYear == mYear;
}
else
{
result = false;
}
return result;
}
}