/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program 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.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.platform.util;
import org.pentaho.platform.util.messages.LocaleHelper;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import java.util.StringTokenizer;
/**
* Provides a utility for calculating relative dates. The class calculates a date based upon an expression. The
* syntax of the expression is given below.
* <p>
* <b>Date Expression</b><br>
*
* <pre>
* <expression> := <expression>+ ( ';' DATESPEC )?
* <expression> := OPERATION? OPERAND ':' <unit> <position>?
* <unit> := 'Y' | 'M' | 'D' | 'W' | 'h' | 'm' | 's'
* <position> := 'S' | 'E'
* OPERATION := '+' | '-'
* OPERAND := [0..9]+
* DATESPEC := <i>any {@link java.text.SimpleDateFormat} format pattern</i>
* </pre>
*
* The <tt>OPERAND</tt> specifies the positive or negative offset to the date. The <tt>unit</tt> inidcates the
* <i>unit</i> of the date to manipulate. The optional position indicates the relative position for the specified
* unit: <i>S</i> for start and <i>E</i> for end. The following are the valid unit values.
*
* <pre>
* Y Year
* M Month
* W Week
* D Day
* h hour
* m minute
* s second
* </pre>
*
* <p>
* <b>Examples</b>:
*
* <pre>
* 0:ME -1:DS 00:00:00.000 of the day before the last day of the current month
* 0:MS 0:WE 23:59:59.999 the last day of the first week of the month
* 0:ME 23:59:59.999 of the last day of the current month
* 5:Y the current month, day and time 5 years in the future
* 5:YS 00:00:00.000 of the first day of the years 5 years in the future
* </pre>
*/
public class DateMath {
private static final char POSITION_END = 'E';
private static final char POSITION_START = 'S';
private static final char UNIT_YEAR = 'Y';
private static final char UNIT_MONTH = 'M';
private static final char UNIT_WEEK = 'W';
private static final char UNIT_DAY = 'D';
private static final char UNIT_HOUR = 'h';
private static final char UNIT_MINUTE = 'm';
private static final char UNIT_SECOND = 's';
/**
* Calculates a date, returning the formatted string version of the calculated date. The method is a short cut
* for {@link #calculateDate(Calendar,String,Locale) calculateDate(null,expressionWithFormat,null)}. If the date
* format is omitted, the short format for the {@link PentahoSystem#getLocale()} is used.
*
* @param expressionWithFormat
* the relative date expression with optional format specification.
* @return The calculated date as a string.
* @throws IllegalArgumentException
* if <tt>expressionWithFormat</tt> is invalid.
*/
public static String claculateDateString( final String expressionWithFormat ) {
return DateMath.calculateDateString( null, expressionWithFormat, null );
}
/**
* Calculates a date, returning the formatted string version of the calculated date. The method is a short cut
* for {@link #calculateDate(Calendar,String,Locale) calculateDate(date,expressionWithFormat,null)}.
*
* @param date
* the target date against the expression will be applied.
* @param expressionWithFormat
* the relative date expression with optional format specification.
* @return The calculated date as a string.
* @throws IllegalArgumentException
* if <tt>expressionWithFormat</tt> is invalid.
*/
public static String calculateDateString( final Calendar date, final String expressionWithFormat ) {
return DateMath.calculateDateString( date, expressionWithFormat, null );
}
/**
* Calculates a date, returning the formatted string version of the calculated date.
*
* @param date
* the target date against the expression will be applied. If <tt>null</tt>, the current date is used.
* @param expressionWithFormat
* the relative date expression with optional format specification.
* @param locale
* the desired locale for the formatted string.
* @return The calculated date as a string.
* @throws IllegalArgumentException
* if <tt>expressionWithFormat</tt> is invalid.
*/
public static String
calculateDateString( final Calendar date, final String expressionWithFormat, final Locale locale ) {
int index = expressionWithFormat.indexOf( ';' );
String expression;
String pattern = null;
Calendar target = ( date == null ) ? Calendar.getInstance() : date;
DateFormat format;
Locale myLocale;
if ( index >= 0 ) {
pattern = expressionWithFormat.substring( index + 1 );
expression = expressionWithFormat.substring( 0, index );
} else {
expression = expressionWithFormat;
}
target = DateMath.calculateDate( date, expression );
myLocale = ( locale == null ) ? LocaleHelper.getLocale() : locale;
if ( myLocale == null ) {
myLocale = LocaleHelper.getDefaultLocale();
}
if ( pattern != null ) {
format = new SimpleDateFormat( pattern, myLocale );
} else {
format = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT, myLocale );
}
return format.format( target.getTime() );
}
/**
* Calculates the date specified by the expression, relative to the current date/time. The method is a short cut
* for {@link #calculate(Calendar, String) calculate(null,expression)}.
*
* @param expression
* the date expression as described above.
* @return The calculated date.
* @throws IllegalArgumentException
* if <tt>expression</tt> is invalid.
*/
public static Calendar calculateDate( final String expression ) {
return DateMath.calculateDate( null, expression );
}
/**
* Calculates the date specified by the expression, relative to the indicated date/time.
*
* @param date
* the target date against the expression is evaluated. If <tt>null</tt>, the current date/time is used.
* If not <tt>null</tt>, the object is manipulated by the expression.
* @param expression
* the date expression as described above.
* @return The calculated date. This will be <tt>date</tt> if <tt>date</tt> is not <tt>null</tt>.
*/
public static Calendar calculateDate( final Calendar date, final String expression ) {
StringTokenizer tok;
String myExpression;
Calendar target = date;
int index = expression.indexOf( ';' );
if ( index >= 0 ) {
myExpression = expression.substring( index + 1 );
} else {
myExpression = expression;
}
tok = new StringTokenizer( myExpression, " \t;" ); //$NON-NLS-1$
while ( tok.hasMoreElements() ) {
target = DateMath.parseAndCalculateDate( target, (String) tok.nextElement() );
}
return target;
}
/**
* Parses and executes a single expression, one without subexpressions.
*
* @param date
* the target date against the expression is evaluated. If <tt>null</tt>, the current date/time is used.
* If not <tt>null</tt>, the object is manipulated by the expression.
* @param expression
* the date expression as described above.
* @return The calculated date. This will be <tt>date</tt> if <tt>date</tt> is not <tt>null</tt>.
*/
private static Calendar parseAndCalculateDate( final Calendar date, final String expression ) {
int index = expression.indexOf( ':' ); // $NON-NLS-1$
char operation = '+'; // $NON-NLS-1$
char unit = ' '; // $NON-NLS-1$
char position = ' '; // $NON-NLS-1$
int operand = 0;
Calendar result;
if ( index >= 0 ) {
try {
String number = expression.substring( 0, index );
operation = number.charAt( 0 );
if ( ( operation == '+' ) || ( operation == '-' ) ) { // $NON-NLS-1$
// Integer.praseInt doesn't handle '+' for positive numbers
//
number = number.substring( 1 );
} else {
operation = '+';
}
operand = Integer.parseInt( number );
index++;
unit = expression.charAt( index );
index++;
if ( index < expression.length() ) {
position = expression.charAt( index );
}
result = DateMath.calculateDate( date, operation, operand, unit, position );
} catch ( Exception ex ) {
IllegalArgumentException err = new IllegalArgumentException( expression );
err.initCause( ex );
throw err;
}
} else {
throw new IllegalArgumentException( expression );
}
return result;
}
/**
* Calculates the relative date based upon the values of the BNF non-terminals above.
*
* @param date
* the target date against the expression is evaluated. If <tt>null</tt>, the current date/time is used.
* If not <tt>null</tt>, the object is manipulated by the expression.
* @param operation
* the value of the operation. Currently, this is the sign on the operand. However, in the future, it
* could be some value to indicate a relative or specific value.
* @param operand
* the value of the NUM token.
* @param unit
* the value of the <unit> non-terminal
* @param position
* the value of teh <position> non-terminal
* @return The calculated date. This will be <tt>date</tt> if <tt>date</tt> is not <tt>null</tt>.
*/
private static Calendar calculateDate( final Calendar date, final char operation, int operand, final char unit,
final char position ) {
Calendar target = ( date == null ) ? Calendar.getInstance() : date;
int calendarField = -1;
switch ( unit ) {
case UNIT_YEAR:
calendarField = Calendar.YEAR;
break;
case UNIT_MONTH:
calendarField = Calendar.MONTH;
break;
case UNIT_WEEK:
calendarField = Calendar.DAY_OF_YEAR;
operand = operand * 7;
break;
case UNIT_DAY:
calendarField = Calendar.DAY_OF_YEAR;
break;
case UNIT_HOUR:
calendarField = Calendar.HOUR_OF_DAY;
break;
case UNIT_MINUTE:
calendarField = Calendar.MINUTE;
break;
case UNIT_SECOND:
calendarField = Calendar.SECOND;
break;
default:
throw new IllegalArgumentException();
}
if ( operation == ' ' ) {
target.set( calendarField, operand );
} else if ( operation == '+' ) {
target.add( calendarField, operand );
} else if ( operation == '-' ) {
target.add( calendarField, -Math.abs( operand ) );
}
if ( unit == DateMath.UNIT_YEAR ) {
if ( position == DateMath.POSITION_START ) {
target.set( Calendar.DAY_OF_YEAR, 1 );
DateMath.setTimeToStart( target );
} else if ( position == DateMath.POSITION_END ) {
target.set( Calendar.DAY_OF_YEAR, target.getActualMaximum( Calendar.DAY_OF_YEAR ) );
DateMath.setTimeToEnd( target );
}
} else if ( unit == DateMath.UNIT_MONTH ) {
if ( position == DateMath.POSITION_START ) {
target.set( Calendar.DAY_OF_MONTH, 1 );
DateMath.setTimeToStart( target );
} else if ( position == DateMath.POSITION_END ) {
target.set( Calendar.DAY_OF_MONTH, target.getActualMaximum( Calendar.DAY_OF_MONTH ) );
DateMath.setTimeToEnd( target );
}
} else if ( unit == DateMath.UNIT_WEEK ) {
int firstDOW = target.getFirstDayOfWeek();
int dayOfWeek = target.get( Calendar.DAY_OF_WEEK ); // force
// calculation
int dayOffset = 0;
if ( position == DateMath.POSITION_START ) {
if ( dayOfWeek > firstDOW ) {
// Past first day of week; go backwards to first day
//
dayOffset = firstDOW - dayOfWeek;
} else if ( dayOfWeek < firstDOW ) {
// Before the first day; go back a week and move forward to
// first day
// Should only happen if first day is not Sunday.
//
dayOffset = -7 + ( firstDOW - dayOfWeek );
}
DateMath.setTimeToStart( target );
} else if ( position == DateMath.POSITION_END ) {
int lastDOW;
if ( firstDOW == Calendar.SUNDAY ) {
lastDOW = Calendar.SATURDAY;
} else {
lastDOW = firstDOW - 1;
}
if ( dayOfWeek < lastDOW ) {
// Before the last day of week; move forward to last day
//
dayOffset = lastDOW - dayOfWeek;
} else if ( dayOfWeek > lastDOW ) {
// Should only happen if last day is anything but Saturday;
// Move to next week; roll back to last day.
dayOffset = 7 - ( dayOfWeek - lastDOW );
}
DateMath.setTimeToEnd( target );
}
if ( dayOffset != 0 ) {
target.add( Calendar.DAY_OF_YEAR, dayOffset );
}
} else if ( unit == DateMath.UNIT_DAY ) {
if ( position == DateMath.POSITION_START ) {
DateMath.setTimeToStart( target );
} else if ( position == DateMath.POSITION_END ) {
DateMath.setTimeToEnd( target );
}
} else if ( unit == DateMath.UNIT_HOUR ) {
if ( position == DateMath.POSITION_START ) {
target.set( Calendar.MINUTE, 0 );
target.set( Calendar.SECOND, 0 );
target.set( Calendar.MILLISECOND, 0 );
} else if ( position == DateMath.POSITION_END ) {
target.set( Calendar.MINUTE, 59 );
target.set( Calendar.SECOND, 59 );
target.set( Calendar.MILLISECOND, 999 );
}
} else if ( unit == DateMath.UNIT_MINUTE ) {
if ( position == DateMath.POSITION_START ) {
target.set( Calendar.SECOND, 0 );
target.set( Calendar.MILLISECOND, 0 );
} else if ( position == DateMath.POSITION_END ) {
target.set( Calendar.SECOND, 59 );
target.set( Calendar.MILLISECOND, 999 );
}
} else if ( unit == DateMath.UNIT_SECOND ) {
if ( position == DateMath.POSITION_START ) {
target.set( Calendar.MILLISECOND, 0 );
} else if ( position == DateMath.POSITION_END ) {
target.set( Calendar.MILLISECOND, 999 );
}
}
target.getTimeInMillis(); // force calculations
return target;
}
/**
* Sets the time to the start of the day (00:00:00.000).
*
* @param target
* the target calendar for which the time will be set.
*/
private static void setTimeToStart( final Calendar target ) {
target.set( Calendar.MILLISECOND, 0 );
target.set( Calendar.SECOND, 0 );
target.set( Calendar.MINUTE, 0 );
target.set( Calendar.HOUR_OF_DAY, 0 );
}
/**
* Sets the time to the endof the day (23:59:59.999).
*
* @param target
* the target calendar for which the time will be set.
*/
private static void setTimeToEnd( final Calendar target ) {
target.set( Calendar.MILLISECOND, 999 );
target.set( Calendar.SECOND, 59 );
target.set( Calendar.MINUTE, 59 );
target.set( Calendar.HOUR_OF_DAY, 23 );
}
}