From: Josh Micich Date: Mon, 25 Aug 2008 08:09:02 +0000 (+0000) Subject: Initial support for evaluating external add-in functions like YEARFRAC X-Git-Tag: REL_3_2_FINAL~138 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2ea7bc5eef9b7ec2ab26263e091f9cdb77aac699;p=poi.git 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 --- diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 26a9c72109..f58cf5430a 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -37,6 +37,7 @@ + Initial support for evaluating external add-in functions like YEARFRAC 45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present 45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE 45623 - Support for additional HSSF header and footer fields, including bold and full file path diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 1b086fddc7..a3548f5ece 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + Initial support for evaluating external add-in functions like YEARFRAC 45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present 45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE 45623 - Support for additional HSSF header and footer fields, including bold and full file path diff --git a/src/java/org/apache/poi/hssf/record/SupBookRecord.java b/src/java/org/apache/poi/hssf/record/SupBookRecord.java index cb4eff8406..c75e2db894 100644 --- a/src/java/org/apache/poi/hssf/record/SupBookRecord.java +++ b/src/java/org/apache/poi/hssf/record/SupBookRecord.java @@ -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() { diff --git a/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java b/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java index 98ab39f05b..45a75fb2a0 100644 --- a/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/NameXPtg.java @@ -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; + } } diff --git a/src/java/org/apache/poi/hssf/record/formula/atp/AnalysisToolPak.java b/src/java/org/apache/poi/hssf/record/formula/atp/AnalysisToolPak.java new file mode 100644 index 0000000000..60f06695d0 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/atp/AnalysisToolPak.java @@ -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); + } +} diff --git a/src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java b/src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java new file mode 100644 index 0000000000..5861871920 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java @@ -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()
+ * + * Returns the fraction of the year spanned by two dates.

+ * + * Syntax
+ * YEARFRAC(startDate, endDate, basis)

+ * + * The basis optionally specifies the behaviour of YEARFRAC as follows: + * + * + * + * + * + * + * + * + *
ValueDays per MonthDays per Year
0 (default)30360
1actualactual
2actual360
3actual365
430360
+ * + */ +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); + } +} diff --git a/src/java/org/apache/poi/hssf/record/formula/atp/YearFracCalculator.java b/src/java/org/apache/poi/hssf/record/formula/atp/YearFracCalculator.java new file mode 100644 index 0000000000..40a5eb8dbd --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/atp/YearFracCalculator.java @@ -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()
+ * + * 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 true 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(); + } + } +} diff --git a/src/java/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java b/src/java/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java index b1d81e6524..6959d146f9 100755 --- a/src/java/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java +++ b/src/java/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java @@ -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 * AbstractFunctionPtg.field_2_fnc_index == 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); } - } + diff --git a/src/java/org/apache/poi/hssf/record/formula/eval/NameXEval.java b/src/java/org/apache/poi/hssf/record/formula/eval/NameXEval.java new file mode 100644 index 0000000000..12b6be3805 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/eval/NameXEval.java @@ -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(); + } +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFDateUtil.java b/src/java/org/apache/poi/hssf/usermodel/HSSFDateUtil.java index 457f0b9ed1..7f45be23b4 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFDateUtil.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFDateUtil.java @@ -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(); } /** diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index fac66b2cc5..df38237fd4 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -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; @@ -61,10 +62,10 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval; /** * @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 }; @@ -78,8 +79,8 @@ public class HSSFFormulaEvaluator { 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 { @@ -90,15 +91,15 @@ public class HSSFFormulaEvaluator { } - + 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 @@ -107,24 +108,24 @@ public class HSSFFormulaEvaluator { // 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) { @@ -157,17 +158,17 @@ public class HSSFFormulaEvaluator { } 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. *

      * int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
      * 
@@ -205,14 +206,14 @@ public class HSSFFormulaEvaluator { } 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: *
      * int evaluatedCellType = evaluator.evaluateInCell(cell).getCellType();
@@ -252,7 +253,7 @@ public class HSSFFormulaEvaluator {
         }
         return cell;
     }
-    
+
     /**
      * Loops over all cells in all sheets of the supplied
      *  workbook.
@@ -261,7 +262,7 @@ public class HSSFFormulaEvaluator {
      *  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) {
@@ -280,8 +281,8 @@ public class HSSFFormulaEvaluator {
 			}
 		}
 	}
-        
-    
+
+
     /**
      * Returns a CellValue wrapper around the supplied ValueEval instance.
      * @param eval
@@ -318,19 +319,19 @@ public class HSSFFormulaEvaluator {
         }
         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;
         }
@@ -340,7 +341,7 @@ public class HSSFFormulaEvaluator {
             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);
@@ -350,20 +351,21 @@ public class HSSFFormulaEvaluator {
 
             // 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; }
@@ -426,9 +428,9 @@ public class HSSFFormulaEvaluator {
         }
         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;
@@ -472,13 +474,13 @@ public class HSSFFormulaEvaluator {
         }
         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)
@@ -497,7 +499,7 @@ public class HSSFFormulaEvaluator {
     	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)
@@ -505,12 +507,12 @@ public class HSSFFormulaEvaluator {
         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++) {
@@ -533,7 +535,7 @@ public class HSSFFormulaEvaluator {
      * one of: Area3DPtg, AreaPtg, ReferencePtg, Ref3DPtg, IntPtg, NumberPtg,
      * StringPtg, BoolPtg 
special Note: OperationPtg subtypes cannot be * passed here! - * + * * @param ptg */ protected static Eval getEvalForPtg(Ptg ptg) { @@ -607,12 +609,12 @@ public class HSSFFormulaEvaluator { * 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())); @@ -633,7 +635,7 @@ public class HSSFFormulaEvaluator { /** * 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); @@ -654,9 +656,9 @@ public class HSSFFormulaEvaluator { } 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 > @@ -667,7 +669,7 @@ public class HSSFFormulaEvaluator { private double numberValue; private boolean booleanValue; private byte errorValue; - + /** * CellType should be one of the types defined in HSSFCell * @param cellType @@ -750,7 +752,7 @@ public class HSSFFormulaEvaluator { /** * debug method - * + * * @param formula * @param sheet * @param workbook @@ -770,5 +772,4 @@ public class HSSFFormulaEvaluator { } System.out.println(""); } - } diff --git a/src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls b/src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls new file mode 100644 index 0000000000..2b2be7d7d8 Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls differ diff --git a/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java b/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java index 1f7fb42ad3..2b60e23c3b 100755 --- a/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java @@ -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); diff --git a/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculator.java b/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculator.java new file mode 100644 index 0000000000..aaf03a238e --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculator.java @@ -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()); + } +} diff --git a/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculatorFromSpreadsheet.java b/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculatorFromSpreadsheet.java new file mode 100644 index 0000000000..2cad8e3620 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculatorFromSpreadsheet.java @@ -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= 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