/*
* 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) 2006 - 2013 Pentaho Corporation and Contributors. All rights reserved.
*/
package org.pentaho.reporting.libraries.formula.function.datetime;
import org.pentaho.reporting.libraries.formula.EvaluationException;
import org.pentaho.reporting.libraries.formula.FormulaContext;
import org.pentaho.reporting.libraries.formula.LibFormulaErrorValue;
import org.pentaho.reporting.libraries.formula.LocalizationContext;
import org.pentaho.reporting.libraries.formula.function.Function;
import org.pentaho.reporting.libraries.formula.function.ParameterCallback;
import org.pentaho.reporting.libraries.formula.lvalues.TypeValuePair;
import org.pentaho.reporting.libraries.formula.typing.TypeRegistry;
import org.pentaho.reporting.libraries.formula.typing.coretypes.NumberType;
import org.pentaho.reporting.libraries.formula.util.NumberUtil;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
/**
* This function returns the number of years, months, or days between two date numbers.<br/>
* <p/>
* The Format is a code from the following table, entered as text, that specifies the format you want: <TABLE> <TR>
* <TH>format</TH> <TH>Returns the number of</TH> </TR> <TR> <TD>y</TD> <TD>Years</TD> </TR> <TR> <TD>m</TD> <TD>Months.
* If there is not a complete month between the dates, 0 will be returned.</TD> </TR> <TR> <TD>d</TD> <TD>Days</TD>
* </TR> <TR> <TD>md</TD> <TD>Days, ignoring months and years</TD> </TR> <TR> <TD>ym</TD> <TD>Months, ignoring
* years</TD> </TR> <TR> <TD>yd</TD> <TD>Days, ignoring years</TD> </TR> <TR> <TD></TD> <TD></TD> </TR> </TABLE>
*
* @author Cedric Pronzato
*/
public class DateDifFunction implements Function {
public static final String YEARS_CODE = "y";
public static final String MONTHS_CODE = "m";
public static final String DAYS_CODE = "d";
public static final String DAYS_IGNORING_YEARS = "yd";
public static final String MONTHS_IGNORING_YEARS = "ym";
public static final String DAYS_IGNORING_MONTHS_YEARS = "md";
private static final long serialVersionUID = 81013707499607068L;
public DateDifFunction() {
}
public String getCanonicalName() {
return "DATEDIF"; // NON-NLS
}
public TypeValuePair evaluate( final FormulaContext context,
final ParameterCallback parameters )
throws EvaluationException {
if ( parameters.getParameterCount() != 3 ) {
throw EvaluationException.getInstance( LibFormulaErrorValue.ERROR_ARGUMENTS_VALUE );
}
final TypeRegistry typeRegistry = context.getTypeRegistry();
final String formatCode = typeRegistry.convertToText
( parameters.getType( 2 ), parameters.getValue( 2 ) );
if ( formatCode == null || "".equals( formatCode ) ) {
throw EvaluationException.getInstance(
LibFormulaErrorValue.ERROR_INVALID_ARGUMENT_VALUE );
}
long days = computeDays( parameters, typeRegistry );
if ( DateDifFunction.DAYS_CODE.equals( formatCode ) ) {
return new TypeValuePair( NumberType.GENERIC_NUMBER, new BigDecimal( days ) );
}
final Date date1 = typeRegistry.convertToDate( parameters.getType( 0 ), parameters.getValue( 0 ) );
final Date date2 = typeRegistry.convertToDate( parameters.getType( 1 ), parameters.getValue( 1 ) );
if ( date1 == null || date2 == null ) {
throw EvaluationException.getInstance( LibFormulaErrorValue.ERROR_INVALID_ARGUMENT_VALUE );
}
final LocalizationContext localizationContext = context.getLocalizationContext();
final TimeZone timeZone = localizationContext.getTimeZone();
final Locale locale = localizationContext.getLocale();
final GregorianCalendar calandar1 = new GregorianCalendar( timeZone, locale );
calandar1.setTime( min( date1, date2 ) );
final GregorianCalendar calandar2 = new GregorianCalendar( timeZone, locale );
calandar2.setTime( max( date1, date2 ) );
int sign = ( date1.getTime() < date2.getTime() ) ? 1 : -1;
final long res = sign * computeDateDifference( formatCode, calandar1, calandar2, days );
return new TypeValuePair( NumberType.GENERIC_NUMBER, new BigDecimal( res ) );
}
protected long computeDays( final ParameterCallback parameters,
final TypeRegistry typeRegistry ) throws EvaluationException {
final Number date1 = typeRegistry.convertToNumber( parameters.getType( 0 ), parameters.getValue( 0 ) );
final Number date2 = typeRegistry.convertToNumber( parameters.getType( 1 ), parameters.getValue( 1 ) );
final BigDecimal dn1 = NumberUtil.performIntRounding( NumberUtil.getAsBigDecimal( date1 ) );
final BigDecimal dn2 = NumberUtil.performIntRounding( NumberUtil.getAsBigDecimal( date2 ) );
return dn2.longValue() - dn1.longValue();
}
protected long computeDateDifference( final String formatCode,
final GregorianCalendar min,
final GregorianCalendar max,
final long days ) throws EvaluationException {
if ( DateDifFunction.YEARS_CODE.equals( formatCode ) ) {
// done
return computeYears( min, max );
} else if ( DateDifFunction.MONTHS_CODE.equals( formatCode ) ) {
// done
return computeMonths( min, max );
} else if ( DateDifFunction.DAYS_IGNORING_MONTHS_YEARS.equals( formatCode ) ) {
return computeMonthDays( min, max );
} else if ( DateDifFunction.MONTHS_IGNORING_YEARS.equals( formatCode ) ) {
// done
return computeYearMonth( min, max );
} else if ( DateDifFunction.DAYS_IGNORING_YEARS.equals( formatCode ) ) {
// done
return computeYearDays( min, max, days );
} else {
throw EvaluationException.getInstance( LibFormulaErrorValue.ERROR_INVALID_ARGUMENT_VALUE );
}
}
private long computeYearDays( final GregorianCalendar min, final GregorianCalendar max, final long dayDiff ) {
final int year1 = min.get( Calendar.YEAR );
final int year2 = max.get( Calendar.YEAR );
if ( year1 == year2 ) {
// simple case: We are within the same year
return Math.abs( dayDiff );
}
final int dayMinDate = min.get( Calendar.DAY_OF_YEAR );
final int dayMaxDate = max.get( Calendar.DAY_OF_YEAR );
if ( dayMinDate <= dayMaxDate ) {
return dayMaxDate - dayMinDate;
}
int daysInMinYear = min.getActualMaximum( Calendar.DAY_OF_YEAR );
int daysToEndOfYear = daysInMinYear - dayMinDate;
return dayMaxDate + daysToEndOfYear;
}
private long computeYearMonth( final GregorianCalendar min, final GregorianCalendar max ) {
return computeMonths( min, max ) % 12;
}
private long computeMonthDays( final GregorianCalendar min, final GregorianCalendar max ) {
// The number of days between Date1 and Date2, as if Date1 and
// Date2 were in the same month and the same year.
int dayMin = min.get( Calendar.DAY_OF_MONTH );
int dayMax = max.get( Calendar.DAY_OF_MONTH );
if ( dayMin <= dayMax ) {
return dayMax - dayMin;
}
int maxDaysInMonth = max.getActualMaximum( Calendar.DAY_OF_MONTH );
return maxDaysInMonth + dayMax - dayMin;
}
private int addFieldLoop( final GregorianCalendar c, final GregorianCalendar target, final int field ) {
c.set( Calendar.MILLISECOND, 0 );
c.set( Calendar.SECOND, 0 );
c.set( Calendar.MINUTE, 0 );
c.set( Calendar.HOUR_OF_DAY, 0 );
target.set( Calendar.MILLISECOND, 0 );
target.set( Calendar.SECOND, 0 );
target.set( Calendar.MINUTE, 0 );
target.set( Calendar.HOUR_OF_DAY, 0 );
if ( c.getTimeInMillis() == target.getTimeInMillis() ) {
return 0;
}
int count = 0;
while ( true ) {
c.add( field, 1 );
if ( c.getTimeInMillis() > target.getTimeInMillis() ) {
return count;
}
count += 1;
}
}
private long computeMonths( final GregorianCalendar min, final GregorianCalendar max ) {
return addFieldLoop( min, max, Calendar.MONTH );
}
private long computeYears( final GregorianCalendar min, final GregorianCalendar max ) {
return addFieldLoop( min, max, Calendar.YEAR );
}
private Date min( final Date d1, final Date d2 ) {
if ( d1.getTime() < d2.getTime() ) {
return d1;
}
return d2;
}
private Date max( final Date d1, final Date d2 ) {
if ( d1.getTime() >= d2.getTime() ) {
return d1;
}
return d2;
}
}