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:
Josh Micich 2008-08-25 08:09:02 +00:00
parent 313d71cc51
commit 02608ba18d
16 changed files with 1124 additions and 126 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
/**

View File

@ -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>");
}
}

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}