/*
* 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 java.math.BigDecimal;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
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;
/**
* 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;
}
}