]> source.dussan.org Git - poi.git/commitdiff
Initial support for evaluating external add-in functions like YEARFRAC
authorJosh Micich <josh@apache.org>
Mon, 25 Aug 2008 08:09:02 +0000 (08:09 +0000)
committerJosh Micich <josh@apache.org>
Mon, 25 Aug 2008 08:09:02 +0000 (08:09 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@688650 13f79535-47bb-0310-9956-ffa450edef68

16 files changed:
src/documentation/content/xdocs/changes.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/record/SupBookRecord.java
src/java/org/apache/poi/hssf/record/formula/NameXPtg.java
src/java/org/apache/poi/hssf/record/formula/atp/AnalysisToolPak.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/formula/atp/YearFrac.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/formula/atp/YearFracCalculator.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/record/formula/eval/ExternalFunction.java
src/java/org/apache/poi/hssf/record/formula/eval/NameXEval.java [new file with mode: 0644]
src/java/org/apache/poi/hssf/usermodel/HSSFDateUtil.java
src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java
src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/record/formula/TestExternalFunctionFormulas.java
src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculator.java [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/record/formula/atp/TestYearFracCalculatorFromSpreadsheet.java [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java

index 26a9c721093b399ef7fcb1ff34d918925a13be63..f58cf5430a2091f58a4a116d70a560b9e73ce880 100644 (file)
@@ -37,6 +37,7 @@
 
                <!-- Don't forget to update status.xml too! -->
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="add">Initial support for evaluating external add-in functions like YEARFRAC</action>
            <action dev="POI-DEVELOPERS" type="fix">45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present</action>
            <action dev="POI-DEVELOPERS" type="fix">45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE</action>
            <action dev="POI-DEVELOPERS" type="add">45623 - Support for additional HSSF header and footer fields, including bold and full file path</action>
index 1b086fddc7fc50d9139f6d1d953e30cbe79ee94e..a3548f5ece8f5c42f87b3067821fe901bc664ed5 100644 (file)
@@ -34,6 +34,7 @@
        <!-- Don't forget to update changes.xml too! -->
     <changes>
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="add">Initial support for evaluating external add-in functions like YEARFRAC</action>
            <action dev="POI-DEVELOPERS" type="fix">45672 - Fix for MissingRecordAwareHSSFListener to prevent multiple LastCellOfRowDummyRecords when shared formulas are present</action>
            <action dev="POI-DEVELOPERS" type="fix">45645 - Fix for HSSFSheet.autoSizeColumn() for widths exceeding Short.MAX_VALUE</action>
            <action dev="POI-DEVELOPERS" type="add">45623 - Support for additional HSSF header and footer fields, including bold and full file path</action>
index cb4eff84068fe709ae9a98ab9f7262135323e0aa..c75e2db8944fef9f3231c29b5ecf1cf1ae3d1129 100644 (file)
@@ -150,6 +150,7 @@ public final class SupBookRecord extends Record {
             sb.append("Internal References ");
             sb.append(" nSheets= ").append(field_1_number_of_sheets);
         }
+        sb.append("]");
         return sb.toString();
     }
     private int getDataSize() {
index 98ab39f05b6c6cb6fa1150fcfbd54d5ec9bd3794..45a75fb2a02e3883a07f1281b4dbf8ecfe514e31 100644 (file)
@@ -30,11 +30,11 @@ public final class NameXPtg extends OperandPtg {
        private final static int SIZE = 7;
 
        /** index to REF entry in externsheet record */
-       private int _sheetRefIndex;
+       private final int _sheetRefIndex;
        /** index to defined name or externname table(1 based) */
-       private int _nameNumber;
+       private final int _nameNumber;
        /** reserved must be 0 */
-       private int _reserved;
+       private final int _reserved;
 
        private NameXPtg(int sheetRefIndex, int nameNumber, int reserved) {
                _sheetRefIndex = sheetRefIndex;
@@ -73,4 +73,11 @@ public final class NameXPtg extends OperandPtg {
        public byte getDefaultOperandClass() {
                return Ptg.CLASS_VALUE;
        }
+
+       public int getSheetRefIndex() {
+               return _sheetRefIndex;
+       }
+       public int getNameIndex() {
+               return _nameNumber - 1;
+       }
 }
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 (file)
index 0000000..60f0669
--- /dev/null
@@ -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 (file)
index 0000000..5861871
--- /dev/null
@@ -0,0 +1,160 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.record.formula.atp;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.regex.Pattern;
+
+import org.apache.poi.hssf.record.formula.eval.ErrorEval;
+import org.apache.poi.hssf.record.formula.eval.Eval;
+import org.apache.poi.hssf.record.formula.eval.EvaluationException;
+import org.apache.poi.hssf.record.formula.eval.NumberEval;
+import org.apache.poi.hssf.record.formula.eval.OperandResolver;
+import org.apache.poi.hssf.record.formula.eval.StringEval;
+import org.apache.poi.hssf.record.formula.eval.ValueEval;
+import org.apache.poi.hssf.record.formula.functions.FreeRefFunction;
+import org.apache.poi.hssf.usermodel.HSSFDateUtil;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+/**
+ * Implementation of Excel 'Analysis ToolPak' function YEARFRAC()<br/>
+ * 
+ * Returns the fraction of the year spanned by two dates.<p/>
+ * 
+ * <b>Syntax</b><br/>
+ * <b>YEARFRAC</b>(<b>startDate</b>, <b>endDate</b>, basis)<p/>
+ * 
+ * The <b>basis</b> optionally specifies the behaviour of YEARFRAC as follows:
+ * 
+ * <table border="0" cellpadding="1" cellspacing="0" summary="basis parameter description">
+ *   <tr><th>Value</th><th>Days per Month</th><th>Days per Year</th></tr>
+ *   <tr align='center'><td>0 (default)</td><td>30</td><td>360</td></tr>
+ *   <tr align='center'><td>1</td><td>actual</td><td>actual</td></tr>
+ *   <tr align='center'><td>2</td><td>actual</td><td>360</td></tr>
+ *   <tr align='center'><td>3</td><td>actual</td><td>365</td></tr>
+ *   <tr align='center'><td>4</td><td>30</td><td>360</td></tr>
+ * </table>
+ * 
+ */
+final class YearFrac implements FreeRefFunction {
+
+       public static final FreeRefFunction instance = new YearFrac();
+       
+       private YearFrac() {
+               // enforce singleton
+       }
+
+       public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook,
+                       HSSFSheet sheet) {
+
+               double result;
+               try {
+                       int basis = 0; // default
+                       switch(args.length) {
+                               case 3:
+                                       basis = evaluateIntArg(args[2], srcCellRow, srcCellCol);
+                               case 2:
+                                       break;
+                               default:
+                                       return ErrorEval.VALUE_INVALID;  
+                       }
+                       double startDateVal = evaluateDateArg(args[0], srcCellRow, srcCellCol);
+                       double endDateVal = evaluateDateArg(args[1], srcCellRow, srcCellCol);
+                       result = YearFracCalculator.calculate(startDateVal, endDateVal, basis);
+               } catch (EvaluationException e) {
+                       return e.getErrorEval();
+               }
+               
+               return new NumberEval(result);
+       }
+
+       private static double evaluateDateArg(Eval arg, int srcCellRow, short srcCellCol) throws EvaluationException {
+               ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
+
+               if (ve instanceof StringEval) {
+                       String strVal = ((StringEval) ve).getStringValue();
+                       Double dVal = OperandResolver.parseDouble(strVal);
+                       if (dVal != null) {
+                               return dVal.doubleValue();
+                       }
+                       Calendar date = parseDate(strVal);
+                       return HSSFDateUtil.getExcelDate(date, false);
+               }
+               return OperandResolver.coerceValueToDouble(ve);
+       }
+
+       private static Calendar parseDate(String strVal) throws EvaluationException {
+               String[] parts = Pattern.compile("/").split(strVal);
+               if (parts.length != 3) {
+                       throw new EvaluationException(ErrorEval.VALUE_INVALID);
+               }
+               String part2 = parts[2];
+               int spacePos = part2.indexOf(' ');
+               if (spacePos > 0) {
+                       // drop time portion if present
+                       part2 = part2.substring(0, spacePos);
+               }
+               int f0;
+               int f1;
+               int f2;
+               try {
+                       f0 = Integer.parseInt(parts[0]);
+                       f1 = Integer.parseInt(parts[1]);
+                       f2 = Integer.parseInt(part2);
+               } catch (NumberFormatException e) {
+                       throw new EvaluationException(ErrorEval.VALUE_INVALID);
+               }
+               if (f0<0 || f1<0 || f2<0 || f0>12 || f1>12 || f2>12) {
+                       // easy to see this cannot be a valid date
+                       throw new EvaluationException(ErrorEval.VALUE_INVALID);
+               }
+               
+               if (f0 >= 1900 && f0 < 9999) {
+                       // when 4 digit value appears first, the format is YYYY/MM/DD, regardless of OS settings
+                       return makeDate(f0, f1, f2);
+               }
+               // otherwise the format seems to depend on OS settings (default date format)
+               if (false) {
+                       // MM/DD/YYYY is probably a good guess, if the in the US
+                       return makeDate(f2, f0, f1);
+               }
+               // TODO - find a way to choose the correct date format
+               throw new RuntimeException("Unable to determine date format for text '" + strVal + "'");
+       }
+
+       /**
+        * @param month 1-based
+        */
+       private static Calendar makeDate(int year, int month, int day) throws EvaluationException {
+               if (month < 1 || month > 12) {
+                       throw new EvaluationException(ErrorEval.VALUE_INVALID);
+               }
+               Calendar cal = new GregorianCalendar(year, month-1, 1, 0, 0, 0);
+               cal.set(Calendar.MILLISECOND, 0);
+               if (day <1 || day>cal.getActualMaximum(Calendar.DAY_OF_MONTH)) {
+                       throw new EvaluationException(ErrorEval.VALUE_INVALID);
+               }
+               return cal;
+       }
+
+       private static int evaluateIntArg(Eval arg, int srcCellRow, short srcCellCol) throws EvaluationException {
+               ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol);
+               return OperandResolver.coerceValueToInt(ve);
+       }
+}
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 (file)
index 0000000..40a5eb8
--- /dev/null
@@ -0,0 +1,344 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.record.formula.atp;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.apache.poi.hssf.record.formula.eval.ErrorEval;
+import org.apache.poi.hssf.record.formula.eval.EvaluationException;
+import org.apache.poi.hssf.usermodel.HSSFDateUtil;
+
+
+/**
+ * Internal calculation methods for Excel 'Analysis ToolPak' function YEARFRAC()<br/>
+ *  
+ * Algorithm inspired by www.dwheeler.com/yearfrac
+ * 
+ * @author Josh Micich
+ */
+final class YearFracCalculator {
+       /** use UTC time-zone to avoid daylight savings issues */
+       private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
+       private static final int MS_PER_HOUR = 60 * 60 * 1000;
+       private static final int MS_PER_DAY = 24 * MS_PER_HOUR;
+       private static final int DAYS_PER_NORMAL_YEAR = 365;
+       private static final int DAYS_PER_LEAP_YEAR = DAYS_PER_NORMAL_YEAR + 1;
+
+       /** the length of normal long months i.e. 31 */
+       private static final int LONG_MONTH_LEN = 31;
+       /** the length of normal short months i.e. 30 */
+       private static final int SHORT_MONTH_LEN = 30;
+       private static final int SHORT_FEB_LEN = 28;
+       private static final int LONG_FEB_LEN = SHORT_FEB_LEN + 1;
+
+       private YearFracCalculator() {
+               // no instances of this class
+       }
+
+
+       public static double calculate(double pStartDateVal, double pEndDateVal, int basis) throws EvaluationException {
+
+               if (basis < 0 || basis >= 5) {
+                       // if basis is invalid the result is #NUM!
+                       throw new EvaluationException(ErrorEval.NUM_ERROR);
+               }
+
+               // common logic for all bases
+
+               // truncate day values
+               int startDateVal = (int) Math.floor(pStartDateVal);
+               int endDateVal = (int) Math.floor(pEndDateVal);
+               if (startDateVal == endDateVal) {
+                       // when dates are equal, result is zero 
+                       return 0;
+               }
+               // swap start and end if out of order
+               if (startDateVal > endDateVal) {
+                       int temp = startDateVal;
+                       startDateVal = endDateVal;
+                       endDateVal = temp;
+               }
+
+               switch (basis) {
+                       case 0: return basis0(startDateVal, endDateVal);
+                       case 1: return basis1(startDateVal, endDateVal);
+                       case 2: return basis2(startDateVal, endDateVal);
+                       case 3: return basis3(startDateVal, endDateVal);
+                       case 4: return basis4(startDateVal, endDateVal);
+               }
+               throw new IllegalStateException("cannot happen");
+       }
+
+
+       /**
+        * @param startDateVal assumed to be less than or equal to endDateVal
+        * @param endDateVal assumed to be greater than or equal to startDateVal
+        */
+       public static double basis0(int startDateVal, int endDateVal) {
+               SimpleDate startDate = createDate(startDateVal);
+               SimpleDate endDate = createDate(endDateVal);
+               int date1day = startDate.day;
+               int date2day = endDate.day;
+
+               // basis zero has funny adjustments to the day-of-month fields when at end-of-month 
+               if (date1day == LONG_MONTH_LEN && date2day == LONG_MONTH_LEN) {
+                       date1day = SHORT_MONTH_LEN;
+                       date2day = SHORT_MONTH_LEN;
+               } else if (date1day == LONG_MONTH_LEN) {
+                       date1day = SHORT_MONTH_LEN;
+               } else if (date1day == SHORT_MONTH_LEN && date2day == LONG_MONTH_LEN) {
+                       date2day = SHORT_MONTH_LEN;
+                       // Note: If date2day==31, it STAYS 31 if date1day < 30.
+                       // Special fixes for February:
+               } else if (startDate.month == 2 && isLastDayOfMonth(startDate)) {
+                       // Note - these assignments deliberately set Feb 30 date.
+                       date1day = SHORT_MONTH_LEN;
+                       if (endDate.month == 2 && isLastDayOfMonth(endDate)) {
+                               // only adjusted when first date is last day in Feb
+                               date2day = SHORT_MONTH_LEN;
+                       }
+               }
+               return calculateAdjusted(startDate, endDate, date1day, date2day);
+       }
+       /**
+        * @param startDateVal assumed to be less than or equal to endDateVal
+        * @param endDateVal assumed to be greater than or equal to startDateVal
+        */
+       public static double basis1(int startDateVal, int endDateVal) {
+               SimpleDate startDate = createDate(startDateVal);
+               SimpleDate endDate = createDate(endDateVal);
+               double yearLength;
+               if (isGreaterThanOneYear(startDate, endDate)) {
+                       yearLength = averageYearLength(startDate.year, endDate.year);
+               } else if (shouldCountFeb29(startDate, endDate)) {
+                       yearLength = DAYS_PER_LEAP_YEAR;
+               } else {
+                       yearLength = DAYS_PER_NORMAL_YEAR;
+               }
+               return dateDiff(startDate.tsMilliseconds, endDate.tsMilliseconds) / yearLength;
+       }
+
+       /**
+        * @param startDateVal assumed to be less than or equal to endDateVal
+        * @param endDateVal assumed to be greater than or equal to startDateVal
+        */
+       public static double basis2(int startDateVal, int endDateVal) {
+               return (endDateVal - startDateVal) / 360.0;
+       }
+       /**
+        * @param startDateVal assumed to be less than or equal to endDateVal
+        * @param endDateVal assumed to be greater than or equal to startDateVal
+        */
+       public static double basis3(double startDateVal, double endDateVal) {
+               return (endDateVal - startDateVal) / 365.0;
+       }
+       /**
+        * @param startDateVal assumed to be less than or equal to endDateVal
+        * @param endDateVal assumed to be greater than or equal to startDateVal
+        */
+       public static double basis4(int startDateVal, int endDateVal) {
+               SimpleDate startDate = createDate(startDateVal);
+               SimpleDate endDate = createDate(endDateVal);
+               int date1day = startDate.day;
+               int date2day = endDate.day;
+
+
+               // basis four has funny adjustments to the day-of-month fields when at end-of-month 
+               if (date1day == LONG_MONTH_LEN) {
+                       date1day = SHORT_MONTH_LEN;
+               }
+               if (date2day == LONG_MONTH_LEN) {
+                       date2day = SHORT_MONTH_LEN;
+               }
+               // Note - no adjustments for end of Feb
+               return calculateAdjusted(startDate, endDate, date1day, date2day);
+       }
+
+
+       private static double calculateAdjusted(SimpleDate startDate, SimpleDate endDate, int date1day,
+                       int date2day) {
+               double dayCount 
+                       = (endDate.year - startDate.year) * 360
+                       + (endDate.month - startDate.month) * SHORT_MONTH_LEN
+                       + (date2day - date1day) * 1;
+               return dayCount / 360;
+       }
+
+       private static boolean isLastDayOfMonth(SimpleDate date) {
+               if (date.day < SHORT_FEB_LEN) {
+                       return false;
+               }
+               return date.day == getLastDayOfMonth(date);
+       }
+
+       private static int getLastDayOfMonth(SimpleDate date) {
+               switch (date.month) {
+                       case 1:
+                       case 3:
+                       case 5:
+                       case 7:
+                       case 8:
+                       case 10:
+                       case 12:
+                               return LONG_MONTH_LEN;
+                       case 4:
+                       case 6:
+                       case 9:
+                       case 11:
+                               return SHORT_MONTH_LEN;
+               }
+               if (isLeapYear(date.year)) {
+                       return LONG_FEB_LEN;
+               }
+               return SHORT_FEB_LEN;
+       }
+
+       /**
+        * Assumes dates are no more than 1 year apart.
+        * @return <code>true</code> if dates both within a leap year, or span a period including Feb 29
+        */
+       private static boolean shouldCountFeb29(SimpleDate start, SimpleDate end) {
+               boolean startIsLeapYear = isLeapYear(start.year);
+               if (startIsLeapYear && start.year == end.year) {
+                       // note - dates may not actually span Feb-29, but it gets counted anyway in this case
+                       return true;
+               }
+
+               boolean endIsLeapYear = isLeapYear(end.year);
+               if (!startIsLeapYear && !endIsLeapYear) {
+                       return false;
+               }
+               if (startIsLeapYear) {
+                       switch (start.month) {
+                               case SimpleDate.JANUARY:
+                               case SimpleDate.FEBRUARY:
+                                       return true;
+                       }
+                       return false;
+               }
+               if (endIsLeapYear) {
+                       switch (end.month) {
+                               case SimpleDate.JANUARY:
+                                       return false;
+                               case SimpleDate.FEBRUARY:
+                                       break;
+                               default:
+                                       return true;
+                       }
+                       return end.day == LONG_FEB_LEN;
+               }
+               return false;
+       }
+
+       /**
+        * @return the whole number of days between the two time-stamps.  Both time-stamps are
+        * assumed to represent 12:00 midnight on the respective day.
+        */
+       private static int dateDiff(long startDateMS, long endDateMS) {
+               long msDiff = endDateMS - startDateMS;
+
+               // some extra checks to make sure we don't hide some other bug with the rounding 
+               int remainderHours = (int) ((msDiff % MS_PER_DAY) / MS_PER_HOUR);
+               switch (remainderHours) {
+                       case 0:  // normal case
+                               break;
+                       case 1:  // transition from normal time to daylight savings adjusted
+                       case 23: // transition from daylight savings adjusted to normal time
+                               // Unexpected since we are using UTC_TIME_ZONE 
+                       default:
+                               throw new RuntimeException("Unexpected date diff between " + startDateMS + " and " + endDateMS);
+
+               }
+               return (int) (0.5 + ((double)msDiff / MS_PER_DAY));
+       }
+
+       private static double averageYearLength(int startYear, int endYear) {
+               int dayCount = 0;
+               for (int i=startYear; i<=endYear; i++) {
+                       dayCount += DAYS_PER_NORMAL_YEAR;
+                       if (isLeapYear(i)) {
+                               dayCount++;
+                       }
+               }
+               double numberOfYears = endYear-startYear+1;
+               return dayCount / numberOfYears;
+       }
+
+       private static boolean isLeapYear(int i) {
+               // leap years are always divisible by 4
+               if (i % 4 != 0) {
+                       return false;
+               }
+               // each 4th century is a leap year
+               if (i % 400 == 0) {
+                       return true;
+               }
+               // all other centuries are *not* leap years
+               if (i % 100 == 0) {
+                       return false;
+               }
+               return true;
+       }
+
+       private static boolean isGreaterThanOneYear(SimpleDate start, SimpleDate end) {
+               if (start.year == end.year) {
+                       return false;
+               }
+               if (start.year + 1 != end.year) {
+                       return true;
+               }
+
+               if (start.month > end.month) {
+                       return false;
+               }
+               if (start.month < end.month) {
+                       return true;
+               }
+
+               return start.day < end.day;
+       }
+
+       private static SimpleDate createDate(int dayCount) {
+               GregorianCalendar calendar = new GregorianCalendar(UTC_TIME_ZONE);
+               HSSFDateUtil.setCalendar(calendar, dayCount, 0, false);
+               return new SimpleDate(calendar);
+       }
+
+       private static final class SimpleDate {
+
+               public static final int JANUARY = 1;
+               public static final int FEBRUARY = 2;
+
+               public final int year;
+               /** 1-based month */
+               public final int month;
+               /** day of month */
+               public final int day;
+               /** milliseconds since 1970 */
+               public long tsMilliseconds;
+
+               public SimpleDate(Calendar cal) {
+                       year = cal.get(Calendar.YEAR);
+                       month = cal.get(Calendar.MONTH) + 1;
+                       day = cal.get(Calendar.DAY_OF_MONTH);
+                       tsMilliseconds = cal.getTimeInMillis();
+               }
+       }
+}
index b1d81e652476d18244c6f4c04e6095e75868ff85..6959d146f984eb61547d017a2585552a95733bcf 100755 (executable)
 
 package org.apache.poi.hssf.record.formula.eval;
 
+import org.apache.poi.hssf.record.formula.atp.AnalysisToolPak;
 import org.apache.poi.hssf.record.formula.functions.FreeRefFunction;
 import org.apache.poi.hssf.usermodel.HSSFSheet;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 /**
  * 
- * Common entry point for all external functions (where 
+ * Common entry point for all user-defined (non-built-in) functions (where 
  * <tt>AbstractFunctionPtg.field_2_fnc_index</tt> == 255)
  * 
+ * TODO rename to UserDefinedFunction
  * @author Josh Micich
  */
 final class ExternalFunction implements FreeRefFunction {
@@ -36,27 +38,43 @@ final class ExternalFunction implements FreeRefFunction {
                        throw new RuntimeException("function name argument missing");
                }
                
-               if (!(args[0] instanceof NameEval)) {
-                       throw new RuntimeException("First argument should be a NameEval, but got ("
-                                       + args[0].getClass().getName() + ")");
-               }
-               NameEval functionNameEval = (NameEval) args[0];
-               
-               int nOutGoingArgs = nIncomingArgs -1;
-               Eval[] outGoingArgs = new Eval[nOutGoingArgs];
-               System.arraycopy(args, 1, outGoingArgs, 0, nOutGoingArgs);
-               
+               Eval nameArg = args[0];
                FreeRefFunction targetFunc;
                try {
-                       targetFunc = findTargetFunction(workbook, functionNameEval);
+                       if (nameArg instanceof NameEval) {
+                               targetFunc = findInternalUserDefinedFunction(workbook, (NameEval) nameArg);
+                       } else if (nameArg instanceof NameXEval) {
+                               targetFunc = findExternalUserDefinedFunction(workbook, (NameXEval) nameArg);
+                       } else {
+                               throw new RuntimeException("First argument should be a NameEval, but got ("
+                                               + nameArg.getClass().getName() + ")");
+                       }
                } catch (EvaluationException e) {
                        return e.getErrorEval();
                }
-               
+               int nOutGoingArgs = nIncomingArgs -1;
+               Eval[] outGoingArgs = new Eval[nOutGoingArgs];
+               System.arraycopy(args, 1, outGoingArgs, 0, nOutGoingArgs);
                return targetFunc.evaluate(outGoingArgs, srcCellRow, srcCellCol, workbook, sheet);
        }
 
-       private FreeRefFunction findTargetFunction(HSSFWorkbook workbook, NameEval functionNameEval) throws EvaluationException {
+       private FreeRefFunction findExternalUserDefinedFunction(HSSFWorkbook workbook,
+                       NameXEval n) throws EvaluationException {
+               String functionName = workbook.resolveNameXText(n.getSheetRefIndex(), n.getNameNumber());
+
+               if(false) {
+                       System.out.println("received call to external user defined function (" + functionName + ")");
+               }
+               // currently only looking for functions from the 'Analysis TookPak'
+               // not sure how much this logic would need to change to support other or multiple add-ins.
+               FreeRefFunction result = AnalysisToolPak.findFunction(functionName);
+               if (result != null) {
+                       return result;
+               }
+               throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED);
+       }
+
+       private FreeRefFunction findInternalUserDefinedFunction(HSSFWorkbook workbook, NameEval functionNameEval) throws EvaluationException {
 
                int numberOfNames = workbook.getNumberOfNames();
                
@@ -68,7 +86,7 @@ final class ExternalFunction implements FreeRefFunction {
                
                String functionName = workbook.getNameName(nameIndex);
                if(false) {
-                       System.out.println("received call to external function index (" + functionName + ")");
+                       System.out.println("received call to internal user defined function  (" + functionName + ")");
                }
                // TODO - detect if the NameRecord corresponds to a named range, function, or something undefined
                // throw the right errors in these cases
@@ -77,5 +95,5 @@ final class ExternalFunction implements FreeRefFunction {
                
                throw new EvaluationException(ErrorEval.FUNCTION_NOT_IMPLEMENTED);
        }
-
 }
+
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 (file)
index 0000000..12b6be3
--- /dev/null
@@ -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();
+       }
+}
index 457f0b9ed19be9b09e390e96fba4292dff23a5e1..7f45be23b4a064f78bbe10d66cbf722e72786dec 100644 (file)
@@ -158,9 +158,16 @@ public final class HSSFDateUtil {
         if (!isValidExcelDate(date)) {
             return null;
         }
+        int wholeDays = (int)Math.floor(date);
+        int millisecondsInDay = (int)((date - wholeDays) * DAY_MILLISECONDS + 0.5);
+        Calendar calendar = new GregorianCalendar(); // using default time-zone
+        setCalendar(calendar, wholeDays, millisecondsInDay, use1904windowing);
+        return calendar.getTime();
+    }
+    public static void setCalendar(Calendar calendar, int wholeDays, int millisecondsInDay,
+            boolean use1904windowing) {
         int startYear = 1900;
         int dayAdjust = -1; // Excel thinks 2/29/1900 is a valid date, which it isn't
-        int wholeDays = (int)Math.floor(date);
         if (use1904windowing) {
             startYear = 1904;
             dayAdjust = 1; // 1904 date windowing uses 1/2/1904 as the first day
@@ -170,12 +177,8 @@ public final class HSSFDateUtil {
             // If Excel date == 2/29/1900, will become 3/1/1900 in Java representation
             dayAdjust = 0;
         }
-        GregorianCalendar calendar = new GregorianCalendar(startYear,0,
-                                                 wholeDays + dayAdjust);
-        int millisecondsInDay = (int)((date - Math.floor(date)) *
-                                      DAY_MILLISECONDS + 0.5);
+        calendar.set(startYear,0, wholeDays + dayAdjust, 0, 0, 0);
         calendar.set(GregorianCalendar.MILLISECOND, millisecondsInDay);
-        return calendar.getTime();
     }
 
     /**
index fac66b2cc589c2fd25aa25c54fd2451202e85437..df38237fd4fd78a74b10cc06f1101c52aa941178 100644 (file)
@@ -51,6 +51,7 @@ import org.apache.poi.hssf.record.formula.eval.ErrorEval;
 import org.apache.poi.hssf.record.formula.eval.Eval;
 import org.apache.poi.hssf.record.formula.eval.FunctionEval;
 import org.apache.poi.hssf.record.formula.eval.NameEval;
+import org.apache.poi.hssf.record.formula.eval.NameXEval;
 import org.apache.poi.hssf.record.formula.eval.NumberEval;
 import org.apache.poi.hssf.record.formula.eval.OperationEval;
 import org.apache.poi.hssf.record.formula.eval.Ref2DEval;
@@ -61,10 +62,10 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
 
 /**
  * @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
- * 
+ *
  */
 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.
      * <pre>
      * int evaluatedCellType = evaluator.evaluateFormulaCell(cell);
      * </pre>
@@ -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:
      * <pre>
      * 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 <br/>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 &lt; amolweb at ya hoo dot com &gt;
@@ -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("</ptg-group>");
     }
-
 }
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 (file)
index 0000000..2b2be7d
Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/yearfracExamples.xls differ
index 1f7fb42ad3a936e2974936bbcbdd71d3188d941f..2b60e23c3becbe605feba0f2cdd908c02293cd50 100755 (executable)
@@ -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 (file)
index 0000000..aaf03a2
--- /dev/null
@@ -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 (file)
index 0000000..2cad8e3
--- /dev/null
@@ -0,0 +1,178 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hssf.record.formula.atp;
+
+import java.io.PrintStream;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+
+import junit.framework.Assert;
+import junit.framework.AssertionFailedError;
+import junit.framework.ComparisonFailure;
+import junit.framework.TestCase;
+
+import org.apache.poi.hssf.HSSFTestDataSamples;
+import org.apache.poi.hssf.record.formula.eval.EvaluationException;
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFDateUtil;
+import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+
+/**
+ * Tests YearFracCalculator using test-cases listed in a sample spreadsheet
+ * 
+ * @author Josh Micich
+ */
+public final class TestYearFracCalculatorFromSpreadsheet extends TestCase {
+       
+       private static final class SS {
+
+               public static final int BASIS_COLUMN = 1; // "B"
+               public static final int START_YEAR_COLUMN = 2; // "C"
+               public static final int END_YEAR_COLUMN = 5; // "F"
+               public static final int YEARFRAC_FORMULA_COLUMN = 11; // "L"
+               public static final int EXPECTED_RESULT_COLUMN = 13; // "N"
+       }
+
+       public void testAll() {
+               
+               HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("yearfracExamples.xls");
+               HSSFSheet sheet = wb.getSheetAt(0);
+               HSSFFormulaEvaluator formulaEvaluator = new HSSFFormulaEvaluator(sheet, wb);
+               int nSuccess = 0;
+               int nFailures = 0;
+               int nUnexpectedErrors = 0;
+               Iterator rowIterator = sheet.rowIterator();
+               while(rowIterator.hasNext()) {
+                       HSSFRow row = (HSSFRow) rowIterator.next();
+                       
+                       HSSFCell cell = row.getCell(SS.YEARFRAC_FORMULA_COLUMN);
+                       if (cell == null || cell.getCellType() != HSSFCell.CELL_TYPE_FORMULA) {
+                               continue;
+                       }
+                       try {
+                               processRow(row, cell, formulaEvaluator);
+                               nSuccess++;
+                       } catch (RuntimeException e) {
+                               nUnexpectedErrors ++;
+                               printShortStackTrace(System.err, e);
+                       } catch (AssertionFailedError e) {
+                               nFailures ++;
+                               printShortStackTrace(System.err, e);
+                       }
+               }
+               if (nUnexpectedErrors + nFailures > 0) {
+                       String msg = nFailures + " failures(s) and " + nUnexpectedErrors 
+                               + " unexpected errors(s) occurred. See stderr for details";
+                       throw new AssertionFailedError(msg);
+               }
+               if (nSuccess < 1) {
+                       throw new RuntimeException("No test sample cases found");
+               }
+       }
+       
+       private static void processRow(HSSFRow row, HSSFCell cell, HSSFFormulaEvaluator formulaEvaluator) {
+               
+               double startDate = makeDate(row, SS.START_YEAR_COLUMN);
+               double endDate = makeDate(row, SS.END_YEAR_COLUMN);
+               
+               int basis = getIntCell(row, SS.BASIS_COLUMN);
+               
+               double expectedValue = getDoubleCell(row, SS.EXPECTED_RESULT_COLUMN);
+               
+               double actualValue;
+               try {
+                       actualValue = YearFracCalculator.calculate(startDate, endDate, basis);
+               } catch (EvaluationException e) {
+                       throw new RuntimeException(e);
+               }
+               if (expectedValue != actualValue) {
+                       throw new ComparisonFailure("Direct calculate failed - row " + (row.getRowNum()+1), 
+                                       String.valueOf(expectedValue), String.valueOf(actualValue));
+               }
+               actualValue = formulaEvaluator.evaluate(cell).getNumberValue();
+               if (expectedValue != actualValue) {
+                       throw new ComparisonFailure("Formula evaluate failed - row " + (row.getRowNum()+1), 
+                                       String.valueOf(expectedValue), String.valueOf(actualValue));
+               }
+       }
+
+       private static double makeDate(HSSFRow row, int yearColumn) {
+               int year = getIntCell(row, yearColumn + 0);
+               int month = getIntCell(row, yearColumn + 1);
+               int day = getIntCell(row, yearColumn + 2);
+               Calendar c = new GregorianCalendar(year, month-1, day, 0, 0, 0);
+               c.set(Calendar.MILLISECOND, 0);
+               return HSSFDateUtil.getExcelDate(c.getTime());
+       }
+
+       private static int getIntCell(HSSFRow row, int colIx) {
+               double dVal = getDoubleCell(row, colIx);
+               if (Math.floor(dVal) != dVal) {
+                       throw new RuntimeException("Non integer value (" + dVal 
+                                       + ") cell found at column " + (char)('A' + colIx));
+               }
+               return (int)dVal;
+       }
+
+       private static double getDoubleCell(HSSFRow row, int colIx) {
+               HSSFCell cell = row.getCell(colIx);
+               if (cell == null) {
+                       throw new RuntimeException("No cell found at column " + colIx);
+               }
+               double dVal = cell.getNumericCellValue();
+               return dVal;
+       }
+
+       /**
+        * Useful to keep output concise when expecting many failures to be reported by this test case
+        * TODO - refactor duplicates in other Test~FromSpreadsheet classes
+        */
+       private static void printShortStackTrace(PrintStream ps, Throwable e) {
+               StackTraceElement[] stes = e.getStackTrace();
+               
+               int startIx = 0;
+               // skip any top frames inside junit.framework.Assert
+               while(startIx<stes.length) {
+                       if(!stes[startIx].getClassName().equals(Assert.class.getName())) {
+                               break;
+                       }
+                       startIx++;
+               }
+               // skip bottom frames (part of junit framework)
+               int endIx = startIx+1;
+               while(endIx < stes.length) {
+                       if(stes[endIx].getClassName().equals(TestCase.class.getName())) {
+                               break;
+                       }
+                       endIx++;
+               }
+               if(startIx >= endIx) {
+                       // something went wrong. just print the whole stack trace
+                       e.printStackTrace(ps);
+               }
+               endIx -= 4; // skip 4 frames of reflection invocation
+               ps.println(e.toString());
+               for(int i=startIx; i<endIx; i++) {
+                       ps.println("\tat " + stes[i].toString());
+               }
+       }
+}
index 4fce0af1d3a2e87d6b28fa5958b4d65a916045b8..30baba84929294a07130746250c33c8fd9b93cb3 100644 (file)
@@ -16,7 +16,6 @@
    limitations under the License.
 ==================================================================== */
 
-
 package org.apache.poi.hssf.usermodel;
 
 import java.util.Calendar;
@@ -52,9 +51,7 @@ public final class TestHSSFDateUtil extends TestCase {
      * Checks the date conversion functions in the HSSFDateUtil class.
      */
 
-    public void testDateConversion()
-            throws Exception
-    {
+    public void testDateConversion() {
 
         // Iteratating over the hours exposes any rounding issues.
         for (int hour = 0; hour < 23; hour++)
@@ -131,7 +128,7 @@ public final class TestHSSFDateUtil extends TestCase {
         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;
             }
@@ -186,7 +183,7 @@ public final class TestHSSFDateUtil extends TestCase {
                     HSSFDateUtil.getExcelDate(javaDate, false), oneMinute);
         }
     }
-    
+
     /**
      * Tests that we deal with time-zones properly
      */
@@ -207,8 +204,7 @@ public final class TestHSSFDateUtil extends TestCase {
             assertEquals("Checking timezone " + id, expected.getTime(), javaDate.getTime());
         }
     }
-    
-    
+
     /**
      * Tests that we correctly detect date formats as such
      */
@@ -220,7 +216,7 @@ public final class TestHSSFDateUtil extends TestCase {
             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++) {
@@ -228,14 +224,14 @@ public final class TestHSSFDateUtil extends TestCase {
             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",
@@ -243,7 +239,7 @@ public final class TestHSSFDateUtil extends TestCase {
                 "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;@",
@@ -257,14 +253,14 @@ public final class TestHSSFDateUtil extends TestCase {
         };
         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",
@@ -272,30 +268,30 @@ public final class TestHSSFDateUtil extends TestCase {
         };
         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]) );
@@ -306,63 +302,63 @@ public final class TestHSSFDateUtil extends TestCase {
      * Test that against a real, test file, we still do everything
      *  correctly
      */
-    public void testOnARealFile() throws Exception {
+    public void testOnARealFile() {
 
         HSSFWorkbook workbook = HSSFTestDataSamples.openSampleWorkbook("DateFormats.xls");
         HSSFSheet sheet       = workbook.getSheetAt(0);
         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);
@@ -372,41 +368,49 @@ public final class TestHSSFDateUtil extends TestCase {
         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.
      */
@@ -420,16 +424,27 @@ public final class TestHSSFDateUtil extends TestCase {
     }
 
     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);
+    }
 }