Initial support for evaluating external add-in functions like YEARFRAC
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@688650 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
313d71cc51
commit
02608ba18d
@ -37,6 +37,7 @@
|
||||
|
||||
<!-- Don't forget to update status.xml too! -->
|
||||
<release version="3.1.1-alpha1" date="2008-??-??">
|
||||
<action dev="POI-DEVELOPERS" type="add">Initial support for evaluating external add-in functions like YEARFRAC</action>
|
||||
<action dev="POI-DEVELOPERS" type="fix">45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present</action>
|
||||
<action dev="POI-DEVELOPERS" type="fix">45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE</action>
|
||||
<action dev="POI-DEVELOPERS" type="add">45623 - Support for additional HSSF header and footer fields, including bold and full file path</action>
|
||||
|
@ -34,6 +34,7 @@
|
||||
<!-- Don't forget to update changes.xml too! -->
|
||||
<changes>
|
||||
<release version="3.1.1-alpha1" date="2008-??-??">
|
||||
<action dev="POI-DEVELOPERS" type="add">Initial support for evaluating external add-in functions like YEARFRAC</action>
|
||||
<action dev="POI-DEVELOPERS" type="fix">45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present</action>
|
||||
<action dev="POI-DEVELOPERS" type="fix">45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE</action>
|
||||
<action dev="POI-DEVELOPERS" type="add">45623 - Support for additional HSSF header and footer fields, including bold and full file path</action>
|
||||
|
@ -150,6 +150,7 @@ public final class SupBookRecord extends Record {
|
||||
sb.append("Internal References ");
|
||||
sb.append(" nSheets= ").append(field_1_number_of_sheets);
|
||||
}
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
private int getDataSize() {
|
||||
|
@ -30,11 +30,11 @@ public final class NameXPtg extends OperandPtg {
|
||||
private final static int SIZE = 7;
|
||||
|
||||
/** index to REF entry in externsheet record */
|
||||
private int _sheetRefIndex;
|
||||
private final int _sheetRefIndex;
|
||||
/** index to defined name or externname table(1 based) */
|
||||
private int _nameNumber;
|
||||
private final int _nameNumber;
|
||||
/** reserved must be 0 */
|
||||
private int _reserved;
|
||||
private final int _reserved;
|
||||
|
||||
private NameXPtg(int sheetRefIndex, int nameNumber, int reserved) {
|
||||
_sheetRefIndex = sheetRefIndex;
|
||||
@ -73,4 +73,11 @@ public final class NameXPtg extends OperandPtg {
|
||||
public byte getDefaultOperandClass() {
|
||||
return Ptg.CLASS_VALUE;
|
||||
}
|
||||
|
||||
public int getSheetRefIndex() {
|
||||
return _sheetRefIndex;
|
||||
}
|
||||
public int getNameIndex() {
|
||||
return _nameNumber - 1;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,154 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.atp;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.Eval;
|
||||
import org.apache.poi.hssf.record.formula.eval.ValueEval;
|
||||
import org.apache.poi.hssf.record.formula.functions.FreeRefFunction;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
|
||||
public final class AnalysisToolPak {
|
||||
|
||||
private static final FreeRefFunction NotImplemented = new FreeRefFunction() {
|
||||
|
||||
public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol,
|
||||
HSSFWorkbook workbook, HSSFSheet sheet) {
|
||||
return ErrorEval.FUNCTION_NOT_IMPLEMENTED;
|
||||
}
|
||||
};
|
||||
|
||||
private static Map _functionsByName = createFunctionsMap();
|
||||
|
||||
private AnalysisToolPak() {
|
||||
// no instances of this class
|
||||
}
|
||||
|
||||
public static FreeRefFunction findFunction(String name) {
|
||||
return (FreeRefFunction)_functionsByName.get(name);
|
||||
}
|
||||
|
||||
private static Map createFunctionsMap() {
|
||||
Map m = new HashMap(100);
|
||||
|
||||
r(m, "ACCRINT", null);
|
||||
r(m, "ACCRINTM", null);
|
||||
r(m, "AMORDEGRC", null);
|
||||
r(m, "AMORLINC", null);
|
||||
r(m, "BESSELI", null);
|
||||
r(m, "BESSELJ", null);
|
||||
r(m, "BESSELK", null);
|
||||
r(m, "BESSELY", null);
|
||||
r(m, "BIN2DEC", null);
|
||||
r(m, "BIN2HEX", null);
|
||||
r(m, "BIN2OCT", null);
|
||||
r(m, "CO MPLEX", null);
|
||||
r(m, "CONVERT", null);
|
||||
r(m, "COUPDAYBS", null);
|
||||
r(m, "COUPDAYS", null);
|
||||
r(m, "COUPDAYSNC", null);
|
||||
r(m, "COUPNCD", null);
|
||||
r(m, "COUPNUM", null);
|
||||
r(m, "COUPPCD", null);
|
||||
r(m, "CUMIPMT", null);
|
||||
r(m, "CUMPRINC", null);
|
||||
r(m, "DEC2BIN", null);
|
||||
r(m, "DEC2HEX", null);
|
||||
r(m, "DEC2OCT", null);
|
||||
r(m, "DELTA", null);
|
||||
r(m, "DISC", null);
|
||||
r(m, "DOLLARDE", null);
|
||||
r(m, "DOLLARFR", null);
|
||||
r(m, "DURATION", null);
|
||||
r(m, "EDATE", null);
|
||||
r(m, "EFFECT", null);
|
||||
r(m, "EOMONTH", null);
|
||||
r(m, "ERF", null);
|
||||
r(m, "ERFC", null);
|
||||
r(m, "FACTDOUBLE", null);
|
||||
r(m, "FVSCHEDULE", null);
|
||||
r(m, "GCD", null);
|
||||
r(m, "GESTEP", null);
|
||||
r(m, "HEX2BIN", null);
|
||||
r(m, "HEX2DEC", null);
|
||||
r(m, "HEX2OCT", null);
|
||||
r(m, "IMABS", null);
|
||||
r(m, "IMAGINARY", null);
|
||||
r(m, "IMARGUMENT", null);
|
||||
r(m, "IMCONJUGATE", null);
|
||||
r(m, "IMCOS", null);
|
||||
r(m, "IMDIV", null);
|
||||
r(m, "IMEXP", null);
|
||||
r(m, "IMLN", null);
|
||||
r(m, "IMLOG10", null);
|
||||
r(m, "IMLOG2", null);
|
||||
r(m, "IMPOWER", null);
|
||||
r(m, "IMPRODUCT", null);
|
||||
r(m, "IMREAL", null);
|
||||
r(m, "IMSIN", null);
|
||||
r(m, "IMSQRT", null);
|
||||
r(m, "IMSUB", null);
|
||||
r(m, "IMSUM", null);
|
||||
r(m, "INTRATE", null);
|
||||
r(m, "ISEVEN", null);
|
||||
r(m, "ISODD", null);
|
||||
r(m, "LCM", null);
|
||||
r(m, "MDURATION", null);
|
||||
r(m, "MROUND", null);
|
||||
r(m, "MULTINOMIAL", null);
|
||||
r(m, "NETWORKDAYS", null);
|
||||
r(m, "NOMINAL", null);
|
||||
r(m, "OCT2BIN", null);
|
||||
r(m, "OCT2DEC", null);
|
||||
r(m, "OCT2HEX", null);
|
||||
r(m, "ODDFPRICE", null);
|
||||
r(m, "ODDFYIELD", null);
|
||||
r(m, "ODDLPRICE", null);
|
||||
r(m, "ODDLYIELD", null);
|
||||
r(m, "PRICE", null);
|
||||
r(m, "PRICEDISC", null);
|
||||
r(m, "PRICEMAT", null);
|
||||
r(m, "QUOTIENT", null);
|
||||
r(m, "RAND BETWEEN", null);
|
||||
r(m, "RECEIVED", null);
|
||||
r(m, "SERIESSUM", null);
|
||||
r(m, "SQRTPI", null);
|
||||
r(m, "TBILLEQ", null);
|
||||
r(m, "TBILLPRICE", null);
|
||||
r(m, "TBILLYIELD", null);
|
||||
r(m, "WEEKNUM", null);
|
||||
r(m, "WORKDAY", null);
|
||||
r(m, "XIRR", null);
|
||||
r(m, "XNPV", null);
|
||||
r(m, "YEARFRAC", YearFrac.instance);
|
||||
r(m, "YIELD", null);
|
||||
r(m, "YIELDDISC", null);
|
||||
r(m, "YIELDMAT", null);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private static void r(Map m, String functionName, FreeRefFunction pFunc) {
|
||||
FreeRefFunction func = pFunc == null ? NotImplemented : pFunc;
|
||||
m.put(functionName, func);
|
||||
}
|
||||
}
|
160
src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java
Normal file
160
src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java
Normal file
@ -0,0 +1,160 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.atp;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.Eval;
|
||||
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
|
||||
import org.apache.poi.hssf.record.formula.eval.NumberEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.OperandResolver;
|
||||
import org.apache.poi.hssf.record.formula.eval.StringEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.ValueEval;
|
||||
import org.apache.poi.hssf.record.formula.functions.FreeRefFunction;
|
||||
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
/**
|
||||
* Implementation of Excel 'Analysis ToolPak' function YEARFRAC()<br/>
|
||||
*
|
||||
* Returns the fraction of the year spanned by two dates.<p/>
|
||||
*
|
||||
* <b>Syntax</b><br/>
|
||||
* <b>YEARFRAC</b>(<b>startDate</b>, <b>endDate</b>, basis)<p/>
|
||||
*
|
||||
* The <b>basis</b> optionally specifies the behaviour of YEARFRAC as follows:
|
||||
*
|
||||
* <table border="0" cellpadding="1" cellspacing="0" summary="basis parameter description">
|
||||
* <tr><th>Value</th><th>Days per Month</th><th>Days per Year</th></tr>
|
||||
* <tr align='center'><td>0 (default)</td><td>30</td><td>360</td></tr>
|
||||
* <tr align='center'><td>1</td><td>actual</td><td>actual</td></tr>
|
||||
* <tr align='center'><td>2</td><td>actual</td><td>360</td></tr>
|
||||
* <tr align='center'><td>3</td><td>actual</td><td>365</td></tr>
|
||||
* <tr align='center'><td>4</td><td>30</td><td>360</td></tr>
|
||||
* </table>
|
||||
*
|
||||
*/
|
||||
final class YearFrac implements FreeRefFunction {
|
||||
|
||||
public static final FreeRefFunction instance = new YearFrac();
|
||||
|
||||
private YearFrac() {
|
||||
// enforce singleton
|
||||
}
|
||||
|
||||
public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook,
|
||||
HSSFSheet sheet) {
|
||||
|
||||
double result;
|
||||
try {
|
||||
int basis = 0; // default
|
||||
switch(args.length) {
|
||||
case 3:
|
||||
basis = evaluateIntArg(args[2], srcCellRow, srcCellCol);
|
||||
case 2:
|
||||
break;
|
||||
default:
|
||||
return ErrorEval.VALUE_INVALID;
|
||||
}
|
||||
double startDateVal = evaluateDateArg(args[0], srcCellRow, srcCellCol);
|
||||
double endDateVal = evaluateDateArg(args[1], srcCellRow, srcCellCol);
|
||||
result = YearFracCalculator.calculate(startDateVal, endDateVal, basis);
|
||||
} catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
|
||||
return new NumberEval(result);
|
||||
}
|
||||
|
||||
private static double evaluateDateArg(Eval arg, int srcCellRow, short srcCellCol) throws EvaluationException {
|
||||
ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
|
||||
|
||||
if (ve instanceof StringEval) {
|
||||
String strVal = ((StringEval) ve).getStringValue();
|
||||
Double dVal = OperandResolver.parseDouble(strVal);
|
||||
if (dVal != null) {
|
||||
return dVal.doubleValue();
|
||||
}
|
||||
Calendar date = parseDate(strVal);
|
||||
return HSSFDateUtil.getExcelDate(date, false);
|
||||
}
|
||||
return OperandResolver.coerceValueToDouble(ve);
|
||||
}
|
||||
|
||||
private static Calendar parseDate(String strVal) throws EvaluationException {
|
||||
String[] parts = Pattern.compile("/").split(strVal);
|
||||
if (parts.length != 3) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
String part2 = parts[2];
|
||||
int spacePos = part2.indexOf(' ');
|
||||
if (spacePos > 0) {
|
||||
// drop time portion if present
|
||||
part2 = part2.substring(0, spacePos);
|
||||
}
|
||||
int f0;
|
||||
int f1;
|
||||
int f2;
|
||||
try {
|
||||
f0 = Integer.parseInt(parts[0]);
|
||||
f1 = Integer.parseInt(parts[1]);
|
||||
f2 = Integer.parseInt(part2);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
if (f0<0 || f1<0 || f2<0 || f0>12 || f1>12 || f2>12) {
|
||||
// easy to see this cannot be a valid date
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
|
||||
if (f0 >= 1900 && f0 < 9999) {
|
||||
// when 4 digit value appears first, the format is YYYY/MM/DD, regardless of OS settings
|
||||
return makeDate(f0, f1, f2);
|
||||
}
|
||||
// otherwise the format seems to depend on OS settings (default date format)
|
||||
if (false) {
|
||||
// MM/DD/YYYY is probably a good guess, if the in the US
|
||||
return makeDate(f2, f0, f1);
|
||||
}
|
||||
// TODO - find a way to choose the correct date format
|
||||
throw new RuntimeException("Unable to determine date format for text '" + strVal + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param month 1-based
|
||||
*/
|
||||
private static Calendar makeDate(int year, int month, int day) throws EvaluationException {
|
||||
if (month < 1 || month > 12) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
Calendar cal = new GregorianCalendar(year, month-1, 1, 0, 0, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
if (day <1 || day>cal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
|
||||
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
||||
}
|
||||
return cal;
|
||||
}
|
||||
|
||||
private static int evaluateIntArg(Eval arg, int srcCellRow, short srcCellCol) throws EvaluationException {
|
||||
ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
|
||||
return OperandResolver.coerceValueToInt(ve);
|
||||
}
|
||||
}
|
@ -0,0 +1,344 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.atp;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
|
||||
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
|
||||
|
||||
|
||||
/**
|
||||
* Internal calculation methods for Excel 'Analysis ToolPak' function YEARFRAC()<br/>
|
||||
*
|
||||
* Algorithm inspired by www.dwheeler.com/yearfrac
|
||||
*
|
||||
* @author Josh Micich
|
||||
*/
|
||||
final class YearFracCalculator {
|
||||
/** use UTC time-zone to avoid daylight savings issues */
|
||||
private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
|
||||
private static final int MS_PER_HOUR = 60 * 60 * 1000;
|
||||
private static final int MS_PER_DAY = 24 * MS_PER_HOUR;
|
||||
private static final int DAYS_PER_NORMAL_YEAR = 365;
|
||||
private static final int DAYS_PER_LEAP_YEAR = DAYS_PER_NORMAL_YEAR + 1;
|
||||
|
||||
/** the length of normal long months i.e. 31 */
|
||||
private static final int LONG_MONTH_LEN = 31;
|
||||
/** the length of normal short months i.e. 30 */
|
||||
private static final int SHORT_MONTH_LEN = 30;
|
||||
private static final int SHORT_FEB_LEN = 28;
|
||||
private static final int LONG_FEB_LEN = SHORT_FEB_LEN + 1;
|
||||
|
||||
private YearFracCalculator() {
|
||||
// no instances of this class
|
||||
}
|
||||
|
||||
|
||||
public static double calculate(double pStartDateVal, double pEndDateVal, int basis) throws EvaluationException {
|
||||
|
||||
if (basis < 0 || basis >= 5) {
|
||||
// if basis is invalid the result is #NUM!
|
||||
throw new EvaluationException(ErrorEval.NUM_ERROR);
|
||||
}
|
||||
|
||||
// common logic for all bases
|
||||
|
||||
// truncate day values
|
||||
int startDateVal = (int) Math.floor(pStartDateVal);
|
||||
int endDateVal = (int) Math.floor(pEndDateVal);
|
||||
if (startDateVal == endDateVal) {
|
||||
// when dates are equal, result is zero
|
||||
return 0;
|
||||
}
|
||||
// swap start and end if out of order
|
||||
if (startDateVal > endDateVal) {
|
||||
int temp = startDateVal;
|
||||
startDateVal = endDateVal;
|
||||
endDateVal = temp;
|
||||
}
|
||||
|
||||
switch (basis) {
|
||||
case 0: return basis0(startDateVal, endDateVal);
|
||||
case 1: return basis1(startDateVal, endDateVal);
|
||||
case 2: return basis2(startDateVal, endDateVal);
|
||||
case 3: return basis3(startDateVal, endDateVal);
|
||||
case 4: return basis4(startDateVal, endDateVal);
|
||||
}
|
||||
throw new IllegalStateException("cannot happen");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param startDateVal assumed to be less than or equal to endDateVal
|
||||
* @param endDateVal assumed to be greater than or equal to startDateVal
|
||||
*/
|
||||
public static double basis0(int startDateVal, int endDateVal) {
|
||||
SimpleDate startDate = createDate(startDateVal);
|
||||
SimpleDate endDate = createDate(endDateVal);
|
||||
int date1day = startDate.day;
|
||||
int date2day = endDate.day;
|
||||
|
||||
// basis zero has funny adjustments to the day-of-month fields when at end-of-month
|
||||
if (date1day == LONG_MONTH_LEN && date2day == LONG_MONTH_LEN) {
|
||||
date1day = SHORT_MONTH_LEN;
|
||||
date2day = SHORT_MONTH_LEN;
|
||||
} else if (date1day == LONG_MONTH_LEN) {
|
||||
date1day = SHORT_MONTH_LEN;
|
||||
} else if (date1day == SHORT_MONTH_LEN && date2day == LONG_MONTH_LEN) {
|
||||
date2day = SHORT_MONTH_LEN;
|
||||
// Note: If date2day==31, it STAYS 31 if date1day < 30.
|
||||
// Special fixes for February:
|
||||
} else if (startDate.month == 2 && isLastDayOfMonth(startDate)) {
|
||||
// Note - these assignments deliberately set Feb 30 date.
|
||||
date1day = SHORT_MONTH_LEN;
|
||||
if (endDate.month == 2 && isLastDayOfMonth(endDate)) {
|
||||
// only adjusted when first date is last day in Feb
|
||||
date2day = SHORT_MONTH_LEN;
|
||||
}
|
||||
}
|
||||
return calculateAdjusted(startDate, endDate, date1day, date2day);
|
||||
}
|
||||
/**
|
||||
* @param startDateVal assumed to be less than or equal to endDateVal
|
||||
* @param endDateVal assumed to be greater than or equal to startDateVal
|
||||
*/
|
||||
public static double basis1(int startDateVal, int endDateVal) {
|
||||
SimpleDate startDate = createDate(startDateVal);
|
||||
SimpleDate endDate = createDate(endDateVal);
|
||||
double yearLength;
|
||||
if (isGreaterThanOneYear(startDate, endDate)) {
|
||||
yearLength = averageYearLength(startDate.year, endDate.year);
|
||||
} else if (shouldCountFeb29(startDate, endDate)) {
|
||||
yearLength = DAYS_PER_LEAP_YEAR;
|
||||
} else {
|
||||
yearLength = DAYS_PER_NORMAL_YEAR;
|
||||
}
|
||||
return dateDiff(startDate.tsMilliseconds, endDate.tsMilliseconds) / yearLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startDateVal assumed to be less than or equal to endDateVal
|
||||
* @param endDateVal assumed to be greater than or equal to startDateVal
|
||||
*/
|
||||
public static double basis2(int startDateVal, int endDateVal) {
|
||||
return (endDateVal - startDateVal) / 360.0;
|
||||
}
|
||||
/**
|
||||
* @param startDateVal assumed to be less than or equal to endDateVal
|
||||
* @param endDateVal assumed to be greater than or equal to startDateVal
|
||||
*/
|
||||
public static double basis3(double startDateVal, double endDateVal) {
|
||||
return (endDateVal - startDateVal) / 365.0;
|
||||
}
|
||||
/**
|
||||
* @param startDateVal assumed to be less than or equal to endDateVal
|
||||
* @param endDateVal assumed to be greater than or equal to startDateVal
|
||||
*/
|
||||
public static double basis4(int startDateVal, int endDateVal) {
|
||||
SimpleDate startDate = createDate(startDateVal);
|
||||
SimpleDate endDate = createDate(endDateVal);
|
||||
int date1day = startDate.day;
|
||||
int date2day = endDate.day;
|
||||
|
||||
|
||||
// basis four has funny adjustments to the day-of-month fields when at end-of-month
|
||||
if (date1day == LONG_MONTH_LEN) {
|
||||
date1day = SHORT_MONTH_LEN;
|
||||
}
|
||||
if (date2day == LONG_MONTH_LEN) {
|
||||
date2day = SHORT_MONTH_LEN;
|
||||
}
|
||||
// Note - no adjustments for end of Feb
|
||||
return calculateAdjusted(startDate, endDate, date1day, date2day);
|
||||
}
|
||||
|
||||
|
||||
private static double calculateAdjusted(SimpleDate startDate, SimpleDate endDate, int date1day,
|
||||
int date2day) {
|
||||
double dayCount
|
||||
= (endDate.year - startDate.year) * 360
|
||||
+ (endDate.month - startDate.month) * SHORT_MONTH_LEN
|
||||
+ (date2day - date1day) * 1;
|
||||
return dayCount / 360;
|
||||
}
|
||||
|
||||
private static boolean isLastDayOfMonth(SimpleDate date) {
|
||||
if (date.day < SHORT_FEB_LEN) {
|
||||
return false;
|
||||
}
|
||||
return date.day == getLastDayOfMonth(date);
|
||||
}
|
||||
|
||||
private static int getLastDayOfMonth(SimpleDate date) {
|
||||
switch (date.month) {
|
||||
case 1:
|
||||
case 3:
|
||||
case 5:
|
||||
case 7:
|
||||
case 8:
|
||||
case 10:
|
||||
case 12:
|
||||
return LONG_MONTH_LEN;
|
||||
case 4:
|
||||
case 6:
|
||||
case 9:
|
||||
case 11:
|
||||
return SHORT_MONTH_LEN;
|
||||
}
|
||||
if (isLeapYear(date.year)) {
|
||||
return LONG_FEB_LEN;
|
||||
}
|
||||
return SHORT_FEB_LEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes dates are no more than 1 year apart.
|
||||
* @return <code>true</code> if dates both within a leap year, or span a period including Feb 29
|
||||
*/
|
||||
private static boolean shouldCountFeb29(SimpleDate start, SimpleDate end) {
|
||||
boolean startIsLeapYear = isLeapYear(start.year);
|
||||
if (startIsLeapYear && start.year == end.year) {
|
||||
// note - dates may not actually span Feb-29, but it gets counted anyway in this case
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean endIsLeapYear = isLeapYear(end.year);
|
||||
if (!startIsLeapYear && !endIsLeapYear) {
|
||||
return false;
|
||||
}
|
||||
if (startIsLeapYear) {
|
||||
switch (start.month) {
|
||||
case SimpleDate.JANUARY:
|
||||
case SimpleDate.FEBRUARY:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (endIsLeapYear) {
|
||||
switch (end.month) {
|
||||
case SimpleDate.JANUARY:
|
||||
return false;
|
||||
case SimpleDate.FEBRUARY:
|
||||
break;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
return end.day == LONG_FEB_LEN;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the whole number of days between the two time-stamps. Both time-stamps are
|
||||
* assumed to represent 12:00 midnight on the respective day.
|
||||
*/
|
||||
private static int dateDiff(long startDateMS, long endDateMS) {
|
||||
long msDiff = endDateMS - startDateMS;
|
||||
|
||||
// some extra checks to make sure we don't hide some other bug with the rounding
|
||||
int remainderHours = (int) ((msDiff % MS_PER_DAY) / MS_PER_HOUR);
|
||||
switch (remainderHours) {
|
||||
case 0: // normal case
|
||||
break;
|
||||
case 1: // transition from normal time to daylight savings adjusted
|
||||
case 23: // transition from daylight savings adjusted to normal time
|
||||
// Unexpected since we are using UTC_TIME_ZONE
|
||||
default:
|
||||
throw new RuntimeException("Unexpected date diff between " + startDateMS + " and " + endDateMS);
|
||||
|
||||
}
|
||||
return (int) (0.5 + ((double)msDiff / MS_PER_DAY));
|
||||
}
|
||||
|
||||
private static double averageYearLength(int startYear, int endYear) {
|
||||
int dayCount = 0;
|
||||
for (int i=startYear; i<=endYear; i++) {
|
||||
dayCount += DAYS_PER_NORMAL_YEAR;
|
||||
if (isLeapYear(i)) {
|
||||
dayCount++;
|
||||
}
|
||||
}
|
||||
double numberOfYears = endYear-startYear+1;
|
||||
return dayCount / numberOfYears;
|
||||
}
|
||||
|
||||
private static boolean isLeapYear(int i) {
|
||||
// leap years are always divisible by 4
|
||||
if (i % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
// each 4th century is a leap year
|
||||
if (i % 400 == 0) {
|
||||
return true;
|
||||
}
|
||||
// all other centuries are *not* leap years
|
||||
if (i % 100 == 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isGreaterThanOneYear(SimpleDate start, SimpleDate end) {
|
||||
if (start.year == end.year) {
|
||||
return false;
|
||||
}
|
||||
if (start.year + 1 != end.year) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (start.month > end.month) {
|
||||
return false;
|
||||
}
|
||||
if (start.month < end.month) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return start.day < end.day;
|
||||
}
|
||||
|
||||
private static SimpleDate createDate(int dayCount) {
|
||||
GregorianCalendar calendar = new GregorianCalendar(UTC_TIME_ZONE);
|
||||
HSSFDateUtil.setCalendar(calendar, dayCount, 0, false);
|
||||
return new SimpleDate(calendar);
|
||||
}
|
||||
|
||||
private static final class SimpleDate {
|
||||
|
||||
public static final int JANUARY = 1;
|
||||
public static final int FEBRUARY = 2;
|
||||
|
||||
public final int year;
|
||||
/** 1-based month */
|
||||
public final int month;
|
||||
/** day of month */
|
||||
public final int day;
|
||||
/** milliseconds since 1970 */
|
||||
public long tsMilliseconds;
|
||||
|
||||
public SimpleDate(Calendar cal) {
|
||||
year = cal.get(Calendar.YEAR);
|
||||
month = cal.get(Calendar.MONTH) + 1;
|
||||
day = cal.get(Calendar.DAY_OF_MONTH);
|
||||
tsMilliseconds = cal.getTimeInMillis();
|
||||
}
|
||||
}
|
||||
}
|
@ -17,14 +17,16 @@
|
||||
|
||||
package org.apache.poi.hssf.record.formula.eval;
|
||||
|
||||
import org.apache.poi.hssf.record.formula.atp.AnalysisToolPak;
|
||||
import org.apache.poi.hssf.record.formula.functions.FreeRefFunction;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
/**
|
||||
*
|
||||
* Common entry point for all external functions (where
|
||||
* Common entry point for all user-defined (non-built-in) functions (where
|
||||
* <tt>AbstractFunctionPtg.field_2_fnc_index</tt> == 255)
|
||||
*
|
||||
* TODO rename to UserDefinedFunction
|
||||
* @author Josh Micich
|
||||
*/
|
||||
final class ExternalFunction implements FreeRefFunction {
|
||||
@ -36,27 +38,43 @@ final class ExternalFunction implements FreeRefFunction {
|
||||
throw new RuntimeException("function name argument missing");
|
||||
}
|
||||
|
||||
if (!(args[0] instanceof NameEval)) {
|
||||
throw new RuntimeException("First argument should be a NameEval, but got ("
|
||||
+ args[0].getClass().getName() + ")");
|
||||
}
|
||||
NameEval functionNameEval = (NameEval) args[0];
|
||||
|
||||
int nOutGoingArgs = nIncomingArgs -1;
|
||||
Eval[] outGoingArgs = new Eval[nOutGoingArgs];
|
||||
System.arraycopy(args, 1, outGoingArgs, 0, nOutGoingArgs);
|
||||
|
||||
Eval nameArg = args[0];
|
||||
FreeRefFunction targetFunc;
|
||||
try {
|
||||
targetFunc = findTargetFunction(workbook, functionNameEval);
|
||||
if (nameArg instanceof NameEval) {
|
||||
targetFunc = findInternalUserDefinedFunction(workbook, (NameEval) nameArg);
|
||||
} else if (nameArg instanceof NameXEval) {
|
||||
targetFunc = findExternalUserDefinedFunction(workbook, (NameXEval) nameArg);
|
||||
} else {
|
||||
throw new RuntimeException("First argument should be a NameEval, but got ("
|
||||
+ nameArg.getClass().getName() + ")");
|
||||
}
|
||||
} catch (EvaluationException e) {
|
||||
return e.getErrorEval();
|
||||
}
|
||||
|
||||
int nOutGoingArgs = nIncomingArgs -1;
|
||||
Eval[] outGoingArgs = new Eval[nOutGoingArgs];
|
||||
System.arraycopy(args, 1, outGoingArgs, 0, nOutGoingArgs);
|
||||
return targetFunc.evaluate(outGoingArgs, srcCellRow, srcCellCol, workbook, sheet);
|
||||
}
|
||||
|
||||
private FreeRefFunction findTargetFunction(HSSFWorkbook workbook, NameEval functionNameEval) throws EvaluationException {
|
||||
private FreeRefFunction findExternalUserDefinedFunction(HSSFWorkbook workbook,
|
||||
NameXEval n) throws EvaluationException {
|
||||
String functionName = workbook.resolveNameXText(n.getSheetRefIndex(), n.getNameNumber());
|
||||
|
||||
if(false) {
|
||||
System.out.println("received call to external user defined function (" + functionName + ")");
|
||||
}
|
||||
// currently only looking for functions from the 'Analysis TookPak'
|
||||
// not sure how much this logic would need to change to support other or multiple add-ins.
|
||||
FreeRefFunction result = AnalysisToolPak.findFunction(functionName);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
private FreeRefFunction findInternalUserDefinedFunction(HSSFWorkbook workbook, NameEval functionNameEval) throws EvaluationException {
|
||||
|
||||
int numberOfNames = workbook.getNumberOfNames();
|
||||
|
||||
@ -68,7 +86,7 @@ final class ExternalFunction implements FreeRefFunction {
|
||||
|
||||
String functionName = workbook.getNameName(nameIndex);
|
||||
if(false) {
|
||||
System.out.println("received call to external function index (" + functionName + ")");
|
||||
System.out.println("received call to internal user defined function (" + functionName + ")");
|
||||
}
|
||||
// TODO - detect if the NameRecord corresponds to a named range, function, or something undefined
|
||||
// throw the right errors in these cases
|
||||
@ -77,5 +95,5 @@ final class ExternalFunction implements FreeRefFunction {
|
||||
|
||||
throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.eval;
|
||||
|
||||
/**
|
||||
* @author Josh Micich
|
||||
*/
|
||||
public final class NameXEval implements Eval {
|
||||
|
||||
/** index to REF entry in externsheet record */
|
||||
private final int _sheetRefIndex;
|
||||
/** index to defined name or externname table(1 based) */
|
||||
private final int _nameNumber;
|
||||
|
||||
public NameXEval(int sheetRefIndex, int nameNumber) {
|
||||
_sheetRefIndex = sheetRefIndex;
|
||||
_nameNumber = nameNumber;
|
||||
}
|
||||
|
||||
public int getSheetRefIndex() {
|
||||
return _sheetRefIndex;
|
||||
}
|
||||
public int getNameNumber() {
|
||||
return _nameNumber;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuffer sb = new StringBuffer(64);
|
||||
sb.append(getClass().getName()).append(" [");
|
||||
sb.append(_sheetRefIndex).append(", ").append(_nameNumber);
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -158,9 +158,16 @@ public final class HSSFDateUtil {
|
||||
if (!isValidExcelDate(date)) {
|
||||
return null;
|
||||
}
|
||||
int wholeDays = (int)Math.floor(date);
|
||||
int millisecondsInDay = (int)((date - wholeDays) * DAY_MILLISECONDS + 0.5);
|
||||
Calendar calendar = new GregorianCalendar(); // using default time-zone
|
||||
setCalendar(calendar, wholeDays, millisecondsInDay, use1904windowing);
|
||||
return calendar.getTime();
|
||||
}
|
||||
public static void setCalendar(Calendar calendar, int wholeDays, int millisecondsInDay,
|
||||
boolean use1904windowing) {
|
||||
int startYear = 1900;
|
||||
int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't
|
||||
int wholeDays = (int)Math.floor(date);
|
||||
if (use1904windowing) {
|
||||
startYear = 1904;
|
||||
dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day
|
||||
@ -170,12 +177,8 @@ public final class HSSFDateUtil {
|
||||
// If Excel date == 2/29/1900, will become 3/1/1900 in Java representation
|
||||
dayAdjust = 0;
|
||||
}
|
||||
GregorianCalendar calendar = new GregorianCalendar(startYear,0,
|
||||
wholeDays + dayAdjust);
|
||||
int millisecondsInDay = (int)((date - Math.floor(date)) *
|
||||
DAY_MILLISECONDS + 0.5);
|
||||
calendar.set(startYear,0, wholeDays + dayAdjust, 0, 0, 0);
|
||||
calendar.set(GregorianCalendar.MILLISECOND, millisecondsInDay);
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +51,7 @@ import org.apache.poi.hssf.record.formula.eval.ErrorEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.Eval;
|
||||
import org.apache.poi.hssf.record.formula.eval.FunctionEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.NameEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.NameXEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.NumberEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.OperationEval;
|
||||
import org.apache.poi.hssf.record.formula.eval.Ref2DEval;
|
||||
@ -363,7 +364,8 @@ public class HSSFFormulaEvaluator {
|
||||
continue;
|
||||
}
|
||||
if (ptg instanceof NameXPtg) {
|
||||
// TODO - external functions
|
||||
NameXPtg nameXPtg = (NameXPtg) ptg;
|
||||
stack.push(new NameXEval(nameXPtg.getSheetRefIndex(), nameXPtg.getNameIndex()));
|
||||
continue;
|
||||
}
|
||||
if (ptg instanceof UnknownPtg) { continue; }
|
||||
@ -770,5 +772,4 @@ public class HSSFFormulaEvaluator {
|
||||
}
|
||||
System.out.println("</ptg-group>");
|
||||
}
|
||||
|
||||
}
|
||||
|
BIN
src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls
Normal file
BIN
src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls
Normal file
Binary file not shown.
@ -71,7 +71,7 @@ public final class TestExternalFunctionFormulas extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public void DISABLEDtestEvaluate() {
|
||||
public void testEvaluate() {
|
||||
HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("externalFunctionExample.xls");
|
||||
HSSFSheet sheet = wb.getSheetAt(0);
|
||||
HSSFCell cell = sheet.getRow(0).getCell(0);
|
||||
|
@ -0,0 +1,66 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.atp;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
|
||||
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
|
||||
|
||||
/**
|
||||
* Specific test cases for YearFracCalculator
|
||||
*/
|
||||
public final class TestYearFracCalculator extends TestCase {
|
||||
|
||||
public void testBasis1() {
|
||||
confirm(md(1999, 1, 1), md(1999, 4, 5), 1, 0.257534247);
|
||||
confirm(md(1999, 4, 1), md(1999, 4, 5), 1, 0.010958904);
|
||||
confirm(md(1999, 4, 1), md(1999, 4, 4), 1, 0.008219178);
|
||||
confirm(md(1999, 4, 2), md(1999, 4, 5), 1, 0.008219178);
|
||||
confirm(md(1999, 3, 31), md(1999, 4, 3), 1, 0.008219178);
|
||||
confirm(md(1999, 4, 5), md(1999, 4, 8), 1, 0.008219178);
|
||||
confirm(md(1999, 4, 4), md(1999, 4, 7), 1, 0.008219178);
|
||||
}
|
||||
|
||||
private void confirm(double startDate, double endDate, int basis, double expectedValue) {
|
||||
double actualValue;
|
||||
try {
|
||||
actualValue = YearFracCalculator.calculate(startDate, endDate, basis);
|
||||
} catch (EvaluationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
double diff = actualValue - expectedValue;
|
||||
if (Math.abs(diff) > 0.000000001) {
|
||||
double hours = diff * 365 * 24;
|
||||
System.out.println(startDate + " " + endDate + " off by " + hours + " hours");
|
||||
assertEquals(expectedValue, actualValue, 0.000000001);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static double md(int year, int month, int day) {
|
||||
Calendar c = new GregorianCalendar();
|
||||
|
||||
c.set(year, month-1, day, 0, 0, 0);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
return HSSFDateUtil.getExcelDate(c.getTime());
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
/* ====================================================================
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
package org.apache.poi.hssf.record.formula.atp;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Iterator;
|
||||
|
||||
import junit.framework.Assert;
|
||||
import junit.framework.AssertionFailedError;
|
||||
import junit.framework.ComparisonFailure;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.apache.poi.hssf.HSSFTestDataSamples;
|
||||
import org.apache.poi.hssf.record.formula.eval.EvaluationException;
|
||||
import org.apache.poi.hssf.usermodel.HSSFCell;
|
||||
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
|
||||
import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
|
||||
import org.apache.poi.hssf.usermodel.HSSFRow;
|
||||
import org.apache.poi.hssf.usermodel.HSSFSheet;
|
||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||
|
||||
/**
|
||||
* Tests YearFracCalculator using test-cases listed in a sample spreadsheet
|
||||
*
|
||||
* @author Josh Micich
|
||||
*/
|
||||
public final class TestYearFracCalculatorFromSpreadsheet extends TestCase {
|
||||
|
||||
private static final class SS {
|
||||
|
||||
public static final int BASIS_COLUMN = 1; // "B"
|
||||
public static final int START_YEAR_COLUMN = 2; // "C"
|
||||
public static final int END_YEAR_COLUMN = 5; // "F"
|
||||
public static final int YEARFRAC_FORMULA_COLUMN = 11; // "L"
|
||||
public static final int EXPECTED_RESULT_COLUMN = 13; // "N"
|
||||
}
|
||||
|
||||
public void testAll() {
|
||||
|
||||
HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("yearfracExamples.xls");
|
||||
HSSFSheet sheet = wb.getSheetAt(0);
|
||||
HSSFFormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator(sheet, wb);
|
||||
int nSuccess = 0;
|
||||
int nFailures = 0;
|
||||
int nUnexpectedErrors = 0;
|
||||
Iterator rowIterator = sheet.rowIterator();
|
||||
while(rowIterator.hasNext()) {
|
||||
HSSFRow row = (HSSFRow) rowIterator.next();
|
||||
|
||||
HSSFCell cell = row.getCell(SS.YEARFRAC_FORMULA_COLUMN);
|
||||
if (cell == null || cell.getCellType() != HSSFCell.CELL_TYPE_FORMULA) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
processRow(row, cell, formulaEvaluator);
|
||||
nSuccess++;
|
||||
} catch (RuntimeException e) {
|
||||
nUnexpectedErrors ++;
|
||||
printShortStackTrace(System.err, e);
|
||||
} catch (AssertionFailedError e) {
|
||||
nFailures ++;
|
||||
printShortStackTrace(System.err, e);
|
||||
}
|
||||
}
|
||||
if (nUnexpectedErrors + nFailures > 0) {
|
||||
String msg = nFailures + " failures(s) and " + nUnexpectedErrors
|
||||
+ " unexpected errors(s) occurred. See stderr for details";
|
||||
throw new AssertionFailedError(msg);
|
||||
}
|
||||
if (nSuccess < 1) {
|
||||
throw new RuntimeException("No test sample cases found");
|
||||
}
|
||||
}
|
||||
|
||||
private static void processRow(HSSFRow row, HSSFCell cell, HSSFFormulaEvaluator formulaEvaluator) {
|
||||
|
||||
double startDate = makeDate(row, SS.START_YEAR_COLUMN);
|
||||
double endDate = makeDate(row, SS.END_YEAR_COLUMN);
|
||||
|
||||
int basis = getIntCell(row, SS.BASIS_COLUMN);
|
||||
|
||||
double expectedValue = getDoubleCell(row, SS.EXPECTED_RESULT_COLUMN);
|
||||
|
||||
double actualValue;
|
||||
try {
|
||||
actualValue = YearFracCalculator.calculate(startDate, endDate, basis);
|
||||
} catch (EvaluationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (expectedValue != actualValue) {
|
||||
throw new ComparisonFailure("Direct calculate failed - row " + (row.getRowNum()+1),
|
||||
String.valueOf(expectedValue), String.valueOf(actualValue));
|
||||
}
|
||||
actualValue = formulaEvaluator.evaluate(cell).getNumberValue();
|
||||
if (expectedValue != actualValue) {
|
||||
throw new ComparisonFailure("Formula evaluate failed - row " + (row.getRowNum()+1),
|
||||
String.valueOf(expectedValue), String.valueOf(actualValue));
|
||||
}
|
||||
}
|
||||
|
||||
private static double makeDate(HSSFRow row, int yearColumn) {
|
||||
int year = getIntCell(row, yearColumn + 0);
|
||||
int month = getIntCell(row, yearColumn + 1);
|
||||
int day = getIntCell(row, yearColumn + 2);
|
||||
Calendar c = new GregorianCalendar(year, month-1, day, 0, 0, 0);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
return HSSFDateUtil.getExcelDate(c.getTime());
|
||||
}
|
||||
|
||||
private static int getIntCell(HSSFRow row, int colIx) {
|
||||
double dVal = getDoubleCell(row, colIx);
|
||||
if (Math.floor(dVal) != dVal) {
|
||||
throw new RuntimeException("Non integer value (" + dVal
|
||||
+ ") cell found at column " + (char)('A' + colIx));
|
||||
}
|
||||
return (int)dVal;
|
||||
}
|
||||
|
||||
private static double getDoubleCell(HSSFRow row, int colIx) {
|
||||
HSSFCell cell = row.getCell(colIx);
|
||||
if (cell == null) {
|
||||
throw new RuntimeException("No cell found at column " + colIx);
|
||||
}
|
||||
double dVal = cell.getNumericCellValue();
|
||||
return dVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful to keep output concise when expecting many failures to be reported by this test case
|
||||
* TODO - refactor duplicates in other Test~FromSpreadsheet classes
|
||||
*/
|
||||
private static void printShortStackTrace(PrintStream ps, Throwable e) {
|
||||
StackTraceElement[] stes = e.getStackTrace();
|
||||
|
||||
int startIx = 0;
|
||||
// skip any top frames inside junit.framework.Assert
|
||||
while(startIx<stes.length) {
|
||||
if(!stes[startIx].getClassName().equals(Assert.class.getName())) {
|
||||
break;
|
||||
}
|
||||
startIx++;
|
||||
}
|
||||
// skip bottom frames (part of junit framework)
|
||||
int endIx = startIx+1;
|
||||
while(endIx < stes.length) {
|
||||
if(stes[endIx].getClassName().equals(TestCase.class.getName())) {
|
||||
break;
|
||||
}
|
||||
endIx++;
|
||||
}
|
||||
if(startIx >= endIx) {
|
||||
// something went wrong. just print the whole stack trace
|
||||
e.printStackTrace(ps);
|
||||
}
|
||||
endIx -= 4; // skip 4 frames of reflection invocation
|
||||
ps.println(e.toString());
|
||||
for(int i=startIx; i<endIx; i++) {
|
||||
ps.println("\tat " + stes[i].toString());
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,6 @@
|
||||
limitations under the License.
|
||||
==================================================================== */
|
||||
|
||||
|
||||
package org.apache.poi.hssf.usermodel;
|
||||
|
||||
import java.util.Calendar;
|
||||
@ -52,9 +51,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
* Checks the date conversion functions in the HSSFDateUtil class.
|
||||
*/
|
||||
|
||||
public void testDateConversion()
|
||||
throws Exception
|
||||
{
|
||||
public void testDateConversion() {
|
||||
|
||||
// Iteratating over the hours exposes any rounding issues.
|
||||
for (int hour = 0; hour < 23; hour++)
|
||||
@ -208,7 +205,6 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that we correctly detect date formats as such
|
||||
*/
|
||||
@ -306,7 +302,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
* Test that against a real, test file, we still do everything
|
||||
* correctly
|
||||
*/
|
||||
public void testOnARealFile() throws Exception {
|
||||
public void testOnARealFile() {
|
||||
|
||||
HSSFWorkbook workbook = HSSFTestDataSamples.openSampleWorkbook("DateFormats.xls");
|
||||
HSSFSheet sheet = workbook.getSheetAt(0);
|
||||
@ -322,7 +318,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
// All of them are the 10th of August
|
||||
// 2 US dates, 3 UK dates
|
||||
row = sheet.getRow(0);
|
||||
cell = row.getCell((short)1);
|
||||
cell = row.getCell(1);
|
||||
style = cell.getCellStyle();
|
||||
assertEquals(aug_10_2007, cell.getNumericCellValue(), 0.0001);
|
||||
assertEquals("d-mmm-yy", style.getDataFormatString(wb));
|
||||
@ -331,7 +327,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
assertTrue(HSSFDateUtil.isCellDateFormatted(cell));
|
||||
|
||||
row = sheet.getRow(1);
|
||||
cell = row.getCell((short)1);
|
||||
cell = row.getCell(1);
|
||||
style = cell.getCellStyle();
|
||||
assertEquals(aug_10_2007, cell.getNumericCellValue(), 0.0001);
|
||||
assertFalse(HSSFDateUtil.isInternalDateFormat(cell.getCellStyle().getDataFormat()));
|
||||
@ -339,7 +335,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
assertTrue(HSSFDateUtil.isCellDateFormatted(cell));
|
||||
|
||||
row = sheet.getRow(2);
|
||||
cell = row.getCell((short)1);
|
||||
cell = row.getCell(1);
|
||||
style = cell.getCellStyle();
|
||||
assertEquals(aug_10_2007, cell.getNumericCellValue(), 0.0001);
|
||||
assertTrue(HSSFDateUtil.isInternalDateFormat(cell.getCellStyle().getDataFormat()));
|
||||
@ -347,7 +343,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
assertTrue(HSSFDateUtil.isCellDateFormatted(cell));
|
||||
|
||||
row = sheet.getRow(3);
|
||||
cell = row.getCell((short)1);
|
||||
cell = row.getCell(1);
|
||||
style = cell.getCellStyle();
|
||||
assertEquals(aug_10_2007, cell.getNumericCellValue(), 0.0001);
|
||||
assertFalse(HSSFDateUtil.isInternalDateFormat(cell.getCellStyle().getDataFormat()));
|
||||
@ -355,7 +351,7 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
assertTrue(HSSFDateUtil.isCellDateFormatted(cell));
|
||||
|
||||
row = sheet.getRow(4);
|
||||
cell = row.getCell((short)1);
|
||||
cell = row.getCell(1);
|
||||
style = cell.getCellStyle();
|
||||
assertEquals(aug_10_2007, cell.getNumericCellValue(), 0.0001);
|
||||
assertFalse(HSSFDateUtil.isInternalDateFormat(cell.getCellStyle().getDataFormat()));
|
||||
@ -401,8 +397,16 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
* @param day one based
|
||||
*/
|
||||
private static Date createDate(int year, int month, int day) {
|
||||
return createDate(year, month, day, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param month zero based
|
||||
* @param day one based
|
||||
*/
|
||||
private static Date createDate(int year, int month, int day, int hour, int minute, int second) {
|
||||
Calendar c = new GregorianCalendar();
|
||||
c.set(year, month, day, 0, 0, 0);
|
||||
c.set(year, month, day, hour, minute, second);
|
||||
c.set(Calendar.MILLISECOND, 0);
|
||||
return c.getTime();
|
||||
}
|
||||
@ -432,4 +436,15 @@ public final class TestHSSFDateUtil extends TestCase {
|
||||
assertEquals(createDate(2008, Calendar.AUGUST, 3), HSSFDateUtil.parseYYYYMMDDDate("2008/08/03"));
|
||||
assertEquals(createDate(1994, Calendar.MAY, 1), HSSFDateUtil.parseYYYYMMDDDate("1994/05/01"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that date values *with* a fractional portion get the right time of day
|
||||
*/
|
||||
public void testConvertDateTime() {
|
||||
// Excel day 30000 is date 18-Feb-1982
|
||||
// 0.7 corresponds to time 16:48:00
|
||||
Date actual = HSSFDateUtil.getJavaDate(30000.7);
|
||||
Date expected = createDate(1982, 1, 18, 16, 48, 0);
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user