<!-- 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>
<!-- 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>
sb.append("Internal References ");
sb.append(" nSheets= ").append(field_1_number_of_sheets);
}
+ sb.append("]");
return sb.toString();
}
private int getDataSize() {
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;
public byte getDefaultOperandClass() {
return Ptg.CLASS_VALUE;
}
+
+ public int getSheetRefIndex() {
+ return _sheetRefIndex;
+ }
+ public int getNameIndex() {
+ return _nameNumber - 1;
+ }
}
--- /dev/null
+/* ====================================================================
+ 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);
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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);
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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();
+ }
+ }
+}
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 {
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();
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
throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED);
}
-
}
+
--- /dev/null
+/* ====================================================================
+ 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();
+ }
+}
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
// 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();
}
/**
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;
/**
* @author Amol S. Deshmukh < amolweb at ya hoo dot com >
- *
+ *
*/
public class HSSFFormulaEvaluator {
-
+
// params to lookup the right constructor using reflection
private static final Class[] VALUE_CONTRUCTOR_CLASS_ARRAY = new Class[] { Ptg.class };
private static final Map VALUE_EVALS_MAP = new HashMap();
/*
- * Following is the mapping between the Ptg tokens returned
- * by the FormulaParser and the *Eval classes that are used
+ * Following is the mapping between the Ptg tokens returned
+ * by the FormulaParser and the *Eval classes that are used
* by the FormulaEvaluator
*/
static {
}
-
+
protected HSSFSheet _sheet;
protected HSSFWorkbook _workbook;
-
+
public HSSFFormulaEvaluator(HSSFSheet sheet, HSSFWorkbook workbook) {
_sheet = sheet;
_workbook = workbook;
}
-
+
/**
* Does nothing
* @deprecated - not needed, since the current row can be derived from the cell
// do nothing
}
-
+
/**
* Returns an underlying FormulaParser, for the specified
* Formula String and HSSFWorkbook.
* This will allow you to generate the Ptgs yourself, if
* your needs are more complex than just having the
- * formula evaluated.
+ * formula evaluated.
*/
public static FormulaParser getUnderlyingParser(HSSFWorkbook workbook, String formula) {
return new FormulaParser(formula, workbook);
}
-
+
/**
* If cell contains a formula, the formula is evaluated and returned,
* else the CellValue simply copies the appropriate cell value from
* the cell and also its cell type. This method should be preferred over
* evaluateInCell() when the call should not modify the contents of the
- * original cell.
+ * original cell.
* @param cell
*/
public CellValue evaluate(HSSFCell cell) {
}
return retval;
}
-
-
+
+
/**
* If cell contains formula, it evaluates the formula,
* and saves the result of the formula. The cell
* remains as a formula cell.
* Else if cell does not contain formula, this method leaves
- * the cell unchanged.
+ * the cell unchanged.
* Note that the type of the formula result is returned,
* so you know what kind of value is also stored with
- * the formula.
+ * the formula.
* <pre>
* int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
* </pre>
}
return -1;
}
-
+
/**
* If cell contains formula, it evaluates the formula, and
* puts the formula result back into the cell, in place
* of the old formula.
* Else if cell does not contain formula, this method leaves
- * the cell unchanged.
- * Note that the same instance of HSSFCell is returned to
+ * the cell unchanged.
+ * Note that the same instance of HSSFCell is returned to
* allow chained calls like:
* <pre>
* int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
}
return cell;
}
-
+
/**
* Loops over all cells in all sheets of the supplied
* workbook.
* remain as formula cells.
* For cells that do not contain formulas, no changes
* are made.
- * This is a helpful wrapper around looping over all
+ * This is a helpful wrapper around looping over all
* cells, and calling evaluateFormulaCell on each one.
*/
public static void evaluateAllFormulaCells(HSSFWorkbook wb) {
}
}
}
-
-
+
+
/**
* Returns a CellValue wrapper around the supplied ValueEval instance.
* @param eval
}
return retval;
}
-
+
/**
- * Dev. Note: Internal evaluate must be passed only a formula cell
+ * Dev. Note: Internal evaluate must be passed only a formula cell
* else a runtime exception will be thrown somewhere inside the method.
* (Hence this is a private method.)
*/
private static ValueEval internalEvaluate(HSSFCell srcCell, HSSFSheet sheet, HSSFWorkbook workbook) {
int srcRowNum = srcCell.getRowIndex();
short srcColNum = srcCell.getCellNum();
-
-
+
+
EvaluationCycleDetector tracker = EvaluationCycleDetectorManager.getTracker();
-
+
if(!tracker.startEvaluate(workbook, sheet, srcRowNum, srcColNum)) {
return ErrorEval.CIRCULAR_REF_ERROR;
}
tracker.endEvaluate(workbook, sheet, srcRowNum, srcColNum);
}
}
- private static ValueEval evaluateCell(HSSFWorkbook workbook, HSSFSheet sheet,
+ private static ValueEval evaluateCell(HSSFWorkbook workbook, HSSFSheet sheet,
int srcRowNum, short srcColNum, String cellFormulaText) {
Ptg[] ptgs = FormulaParser.parse(cellFormulaText, workbook);
// since we don't know how to handle these yet :(
Ptg ptg = ptgs[i];
- if (ptg instanceof ControlPtg) {
+ if (ptg instanceof ControlPtg) {
// skip Parentheses, Attr, etc
- continue;
+ continue;
}
if (ptg instanceof MemErrPtg) { continue; }
if (ptg instanceof MissingArgPtg) { continue; }
- if (ptg instanceof NamePtg) {
+ if (ptg instanceof NamePtg) {
// named ranges, macro functions
NamePtg namePtg = (NamePtg) ptg;
stack.push(new NameEval(namePtg.getIndex()));
- continue;
+ 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; }
}
value = dereferenceValue(value, srcRowNum, srcColNum);
if (value instanceof BlankEval) {
- // Note Excel behaviour here. A blank final final value is converted to zero.
+ // Note Excel behaviour here. A blank final final value is converted to zero.
return NumberEval.ZERO;
- // Formulas _never_ evaluate to blank. If a formula appears to have evaluated to
+ // Formulas _never_ evaluate to blank. If a formula appears to have evaluated to
// blank, the actual value is empty string. This can be verified with ISBLANK().
}
return value;
}
return operation.evaluate(ops, srcRowNum, srcColNum);
}
-
+
public static AreaEval evaluateAreaPtg(HSSFSheet sheet, HSSFWorkbook workbook, AreaPtg ap) {
int row0 = ap.getFirstRow();
int col0 = ap.getFirstColumn();
int row1 = ap.getLastRow();
int col1 = ap.getLastColumn();
-
+
// If the last row is -1, then the
// reference is for the rest of the column
// (eg C:C)
int col1 = a3dp.getLastColumn();
Workbook wb = workbook.getWorkbook();
HSSFSheet xsheet = workbook.getSheetAt(wb.getSheetIndexFromExternSheetIndex(a3dp.getExternSheetIndex()));
-
+
// If the last row is -1, then the
// reference is for the rest of the column
// (eg C:C)
if(row1 == -1 && row0 >= 0) {
row1 = (short)xsheet.getLastRowNum();
}
-
+
ValueEval[] values = evalArea(workbook, xsheet, row0, col0, row1, col1);
return new Area3DEval(a3dp, values);
}
-
- private static ValueEval[] evalArea(HSSFWorkbook workbook, HSSFSheet sheet,
+
+ private static ValueEval[] evalArea(HSSFWorkbook workbook, HSSFSheet sheet,
int row0, int col0, int row1, int col1) {
ValueEval[] values = new ValueEval[(row1 - row0 + 1) * (col1 - col0 + 1)];
for (int x = row0; sheet != null && x < row1 + 1; x++) {
* one of: Area3DPtg, AreaPtg, ReferencePtg, Ref3DPtg, IntPtg, NumberPtg,
* StringPtg, BoolPtg <br/>special Note: OperationPtg subtypes cannot be
* passed here!
- *
+ *
* @param ptg
*/
protected static Eval getEvalForPtg(Ptg ptg) {
* Creates a Ref2DEval for ReferencePtg.
* Non existent cells are treated as RefEvals containing BlankEval.
*/
- private static Ref2DEval createRef2DEval(RefPtg ptg, HSSFCell cell,
+ private static Ref2DEval createRef2DEval(RefPtg ptg, HSSFCell cell,
HSSFSheet sheet, HSSFWorkbook workbook) {
if (cell == null) {
return new Ref2DEval(ptg, BlankEval.INSTANCE);
}
-
+
switch (cell.getCellType()) {
case HSSFCell.CELL_TYPE_NUMERIC:
return new Ref2DEval(ptg, new NumberEval(cell.getNumericCellValue()));
/**
* create a Ref3DEval for Ref3DPtg.
*/
- private static Ref3DEval createRef3DEval(Ref3DPtg ptg, HSSFCell cell,
+ private static Ref3DEval createRef3DEval(Ref3DPtg ptg, HSSFCell cell,
HSSFSheet sheet, HSSFWorkbook workbook) {
if (cell == null) {
return new Ref3DEval(ptg, BlankEval.INSTANCE);
}
throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")");
}
-
+
/**
- * Mimics the 'data view' of a cell. This allows formula evaluator
+ * Mimics the 'data view' of a cell. This allows formula evaluator
* to return a CellValue instead of precasting the value to String
* or Number or boolean type.
* @author Amol S. Deshmukh < amolweb at ya hoo dot com >
private double numberValue;
private boolean booleanValue;
private byte errorValue;
-
+
/**
* CellType should be one of the types defined in HSSFCell
* @param cellType
/**
* debug method
- *
+ *
* @param formula
* @param sheet
* @param workbook
}
System.out.println("</ptg-group>");
}
-
}
}
}
- public void DISABLEDtestEvaluate() {
+ public void testEvaluate() {
HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("externalFunctionExample.xls");
HSSFSheet sheet = wb.getSheetAt(0);
HSSFCell cell = sheet.getRow(0).getCell(0);
--- /dev/null
+/* ====================================================================
+ 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());
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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());
+ }
+ }
+}
limitations under the License.
==================================================================== */
-
package org.apache.poi.hssf.usermodel;
import java.util.Calendar;
* 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++)
for (int hour = 0; hour < 24; hour++, excelDate += oneHour) {
// Skip 02:00 CET as that is the Daylight change time
- // and Java converts it automatically to 03:00 CEST
+ // and Java converts it automatically to 03:00 CEST
if (hour == 2) {
continue;
}
HSSFDateUtil.getExcelDate(javaDate, false), oneMinute);
}
}
-
+
/**
* Tests that we deal with time-zones properly
*/
assertEquals("Checking timezone " + id, expected.getTime(), javaDate.getTime());
}
}
-
-
+
/**
* Tests that we correctly detect date formats as such
*/
assertTrue( HSSFDateUtil.isInternalDateFormat(builtins[i]) );
assertTrue( HSSFDateUtil.isADateFormat(builtins[i],formatStr) );
}
-
+
// Now try a few built-in non date formats
builtins = new short[] { 0x01, 0x02, 0x17, 0x1f, 0x30 };
for(int i=0; i<builtins.length; i++) {
assertFalse( HSSFDateUtil.isInternalDateFormat(builtins[i]) );
assertFalse( HSSFDateUtil.isADateFormat(builtins[i],formatStr) );
}
-
+
// Now for some non-internal ones
// These come after the real ones
int numBuiltins = HSSFDataFormat.getNumberOfBuiltinBuiltinFormats();
assertTrue(numBuiltins < 60);
short formatId = 60;
assertFalse( HSSFDateUtil.isInternalDateFormat(formatId) );
-
+
// Valid ones first
String[] formats = new String[] {
"yyyy-mm-dd", "yyyy/mm/dd", "yy/mm/dd", "yy/mmm/dd",
"dd-mm-yy", "dd-mm-yyyy",
"DD-MM-YY", "DD-mm-YYYY",
"dd\\-mm\\-yy", // Sometimes escaped
-
+
// These crazy ones are valid
"yyyy-mm-dd;@", "yyyy/mm/dd;@",
"dd-mm-yy;@", "dd-mm-yyyy;@",
};
for(int i=0; i<formats.length; i++) {
assertTrue(
- formats[i] + " is a date format",
- HSSFDateUtil.isADateFormat(formatId, formats[i])
+ formats[i] + " is a date format",
+ HSSFDateUtil.isADateFormat(formatId, formats[i])
);
}
-
+
// Then time based ones too
formats = new String[] {
- "yyyy-mm-dd hh:mm:ss", "yyyy/mm/dd HH:MM:SS",
+ "yyyy-mm-dd hh:mm:ss", "yyyy/mm/dd HH:MM:SS",
"mm/dd HH:MM", "yy/mmm/dd SS",
"mm/dd HH:MM AM", "mm/dd HH:MM am",
"mm/dd HH:MM PM", "mm/dd HH:MM pm",
};
for(int i=0; i<formats.length; i++) {
assertTrue(
- formats[i] + " is a datetime format",
- HSSFDateUtil.isADateFormat(formatId, formats[i])
+ formats[i] + " is a datetime format",
+ HSSFDateUtil.isADateFormat(formatId, formats[i])
);
}
-
+
// Then invalid ones
formats = new String[] {
- "yyyy*mm*dd",
+ "yyyy*mm*dd",
"0.0", "0.000",
"0%", "0.0%",
"[]Foo", "[BLACK]0.00%",
"", null
};
for(int i=0; i<formats.length; i++) {
- assertFalse(
- formats[i] + " is not a date or datetime format",
- HSSFDateUtil.isADateFormat(formatId, formats[i])
+ assertFalse(
+ formats[i] + " is not a date or datetime format",
+ HSSFDateUtil.isADateFormat(formatId, formats[i])
);
}
-
+
// And these are ones we probably shouldn't allow,
// but would need a better regexp
formats = new String[] {
- "yyyy:mm:dd",
+ "yyyy:mm:dd",
};
for(int i=0; i<formats.length; i++) {
// assertFalse( HSSFDateUtil.isADateFormat(formatId, formats[i]) );
* 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);
Workbook wb = workbook.getWorkbook();
-
+
HSSFRow row;
HSSFCell cell;
HSSFCellStyle style;
-
+
double aug_10_2007 = 39304.0;
-
+
// Should have dates in 2nd column
// 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));
assertTrue(HSSFDateUtil.isInternalDateFormat(style.getDataFormat()));
assertTrue(HSSFDateUtil.isADateFormat(style.getDataFormat(), style.getDataFormatString(wb)));
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()));
assertTrue(HSSFDateUtil.isADateFormat(style.getDataFormat(), style.getDataFormatString(wb)));
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()));
assertTrue(HSSFDateUtil.isADateFormat(style.getDataFormat(), style.getDataFormatString(wb)));
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()));
assertTrue(HSSFDateUtil.isADateFormat(style.getDataFormat(), style.getDataFormatString(wb)));
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()));
assertTrue(HSSFDateUtil.isADateFormat(style.getDataFormat(), style.getDataFormatString(wb)));
assertTrue(HSSFDateUtil.isCellDateFormatted(cell));
}
-
+
public void testDateBug_2Excel() {
assertEquals(59.0, HSSFDateUtil.getExcelDate(createDate(1900, CALENDAR_FEBRUARY, 28), false), 0.00001);
assertEquals(61.0, HSSFDateUtil.getExcelDate(createDate(1900, CALENDAR_MARCH, 1), false), 0.00001);
assertEquals(37257.00, HSSFDateUtil.getExcelDate(createDate(2002, CALENDAR_JANUARY, 1), false), 0.00001);
assertEquals(38074.00, HSSFDateUtil.getExcelDate(createDate(2004, CALENDAR_MARCH, 28), false), 0.00001);
}
-
+
public void testDateBug_2Java() {
assertEquals(createDate(1900, CALENDAR_FEBRUARY, 28), HSSFDateUtil.getJavaDate(59.0, false));
assertEquals(createDate(1900, CALENDAR_MARCH, 1), HSSFDateUtil.getJavaDate(61.0, false));
-
+
assertEquals(createDate(2002, CALENDAR_FEBRUARY, 28), HSSFDateUtil.getJavaDate(37315.00, false));
assertEquals(createDate(2002, CALENDAR_MARCH, 1), HSSFDateUtil.getJavaDate(37316.00, false));
assertEquals(createDate(2002, CALENDAR_JANUARY, 1), HSSFDateUtil.getJavaDate(37257.00, false));
assertEquals(createDate(2004, CALENDAR_MARCH, 28), HSSFDateUtil.getJavaDate(38074.00, false));
}
-
+
public void testDate1904() {
assertEquals(createDate(1904, CALENDAR_JANUARY, 2), HSSFDateUtil.getJavaDate(1.0, true));
assertEquals(createDate(1904, CALENDAR_JANUARY, 1), HSSFDateUtil.getJavaDate(0.0, true));
assertEquals(0.0, HSSFDateUtil.getExcelDate(createDate(1904, CALENDAR_JANUARY, 1), true), 0.00001);
assertEquals(1.0, HSSFDateUtil.getExcelDate(createDate(1904, CALENDAR_JANUARY, 2), true), 0.00001);
-
+
assertEquals(createDate(1998, CALENDAR_JULY, 5), HSSFDateUtil.getJavaDate(35981, false));
assertEquals(createDate(1998, CALENDAR_JULY, 5), HSSFDateUtil.getJavaDate(34519, true));
-
+
assertEquals(35981.0, HSSFDateUtil.getExcelDate(createDate(1998, CALENDAR_JULY, 5), false), 0.00001);
assertEquals(34519.0, HSSFDateUtil.getExcelDate(createDate(1998, CALENDAR_JULY, 5), true), 0.00001);
}
-
+
/**
- * @param month zero based
+ * @param month zero based
* @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();
}
-
+
/**
* Check if HSSFDateUtil.getAbsoluteDay works as advertised.
*/
}
public void testConvertTime() {
-
+
final double delta = 1E-7; // a couple of digits more accuracy than strictly required
assertEquals(0.5, HSSFDateUtil.convertTime("12:00"), delta);
assertEquals(2.0/3, HSSFDateUtil.convertTime("16:00"), delta);
assertEquals(0.0000116, HSSFDateUtil.convertTime("0:00:01"), delta);
assertEquals(0.7330440, HSSFDateUtil.convertTime("17:35:35"), delta);
}
-
+
public void testParseDate() {
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);
+ }
}