]> source.dussan.org Git - poi.git/commitdiff
Bugzilla 53644: XLS formula bugfix (CalFieldFunc) + WeekDay addon
authorYegor Kozlov <yegor@apache.org>
Sat, 11 Aug 2012 16:14:49 +0000 (16:14 +0000)
committerYegor Kozlov <yegor@apache.org>
Sat, 11 Aug 2012 16:14:49 +0000 (16:14 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1371969 13f79535-47bb-0310-9956-ffa450edef68

patch.xml
src/documentation/content/xdocs/spreadsheet/eval-devguide.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/ss/formula/eval/FunctionEval.java
src/java/org/apache/poi/ss/formula/functions/Address.java
src/java/org/apache/poi/ss/formula/functions/BooleanFunction.java
src/java/org/apache/poi/ss/formula/functions/CalendarFieldFunction.java
src/java/org/apache/poi/ss/formula/functions/WeekdayFunc.java [new file with mode: 0644]
src/java/org/apache/poi/ss/usermodel/DateUtil.java
src/testcases/org/apache/poi/ss/formula/functions/TestAddress.java
src/testcases/org/apache/poi/ss/formula/functions/TestCalendarFieldFunction.java

index 56852f4b5f4d818ecc9ef9ce415e4b283d116a47..61b3cb7a95a5dab0f1a89c17c581f1d55146152d 100755 (executable)
--- a/patch.xml
+++ b/patch.xml
          <filterchain>  \r
             <!-- capture any new files -->\r
             <linecontainsregexp>\r
-                <regexp pattern="(\?|A)......"/>\r
+                <regexp pattern="^(\?|A)......"/>\r
             </linecontainsregexp>\r
             <!-- filter out the first six characters -->\r
             <tokenfilter>\r
-              <replaceregex pattern="(.......)" replace=""/>\r
+              <replaceregex pattern="^(.......)" replace=""/>\r
             </tokenfilter>\r
             <!--remove line breaks -->\r
             <striplinebreaks/>\r
index f7c17650bd2b9663bc3247753228e58444375afb..99eecff4e0d769f7f5110773876627d35d42f477 100644 (file)
             <p>
                 As of Feb 2012, POI supports about 140 built-in functions,
                 see <link href="#appendixA">Appendix A</link> for the full list.
-                You can programmatically list supported / unsuported functions using trhe following helper methods:
+                You can programmatically list supported / unsuported functions using the following helper methods:
             </p>
             <source>
     // list of functions that POI can evaluate
-    Collection&lt;String&gt; suportedFuncs = WorkbookEvaluator.getSupportedFunctionNames();
+    Collection&lt;String&gt; supportedFuncs = WorkbookEvaluator.getSupportedFunctionNames();
 
     // list of functions that are not supported by POI
     Collection&lt;String&gt; unsupportedFuncs = WorkbookEvaluator.getNotSupportedFunctionNames();
     VAR
     VARP
     VLOOKUP
+    WEEKDAY
     WORKDAY
     YEAR
     YEARFRAC
index d3a744b48ebd98392c7e076b372ac11528387a0f..57109eeb9f2572de2cbfa2a8d71077b44ec83460 100644 (file)
@@ -34,6 +34,7 @@
 
     <changes>
         <release version="3.9-beta1" date="2012-??-??">
+          <action dev="poi-developers" type="add">53644 - XLS formula bugfix (CalFieldFunc) + WeekDay addon </action>
           <action dev="poi-developers" type="add">53446 - Fixed some problems extracting PNGs </action>
           <action dev="poi-developers" type="fix">53205 - Fixed some parsing errors and encoding issues in HDGF </action>
           <action dev="poi-developers" type="add">53204 - Improved performanceof PageSettingsBlock in HSSF </action>
index fb4f8d7252721124f8b29adcf647c5463b3207a2..c76da52c17807d2f9da5ba071e15123071a3be6e 100644 (file)
@@ -117,6 +117,7 @@ public final class FunctionEval {
                retval[68] = CalendarFieldFunction.MONTH;
                retval[69] = CalendarFieldFunction.YEAR;
 
+               retval[70] = WeekdayFunc.instance;
                retval[71] = CalendarFieldFunction.HOUR;
                retval[72] = CalendarFieldFunction.MINUTE;
                retval[73] = CalendarFieldFunction.SECOND;
index 288a88edc9a5c47fa3e12711f022a51a71ce7613..379135f14846774e95da0833edee817f84712ef0 100644 (file)
@@ -43,10 +43,10 @@ public class Address implements Function {
             int col =  (int)NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex);\r
 \r
             int refType;\r
-            if(args.length > 2){\r
+            if (args.length > 2  &&  args[2] != MissingArgEval.instance) {\r
                 refType = (int)NumericFunction.singleOperandEvaluate(args[2], srcRowIndex, srcColumnIndex);\r
             } else {\r
-                refType = REF_ABSOLUTE;\r
+                refType = REF_ABSOLUTE;                // this is also the default if parameter is not given\r
             }\r
             switch (refType){\r
                 case REF_ABSOLUTE:\r
index b90f929fb921beac7faa0e6a494e083b42a04d5c..9e796e704027d956c660039ef0729c24f000f2ad 100644 (file)
@@ -20,6 +20,7 @@ package org.apache.poi.ss.formula.functions;
 import org.apache.poi.ss.formula.eval.BoolEval;
 import org.apache.poi.ss.formula.eval.ErrorEval;
 import org.apache.poi.ss.formula.eval.EvaluationException;
+import org.apache.poi.ss.formula.eval.MissingArgEval;
 import org.apache.poi.ss.formula.eval.OperandResolver;
 import org.apache.poi.ss.formula.eval.RefEval;
 import org.apache.poi.ss.formula.eval.ValueEval;
@@ -81,6 +82,8 @@ public abstract class BooleanFunction implements Function {
                        if (arg instanceof RefEval) {
                                ValueEval ve = ((RefEval) arg).getInnerValueEval();
                                tempVe = OperandResolver.coerceValueToBoolean(ve, true);
+                       } else if (arg == MissingArgEval.instance) {
+                               tempVe = null;          // you can leave out parameters, they are simply ignored
                        } else {
                                tempVe = OperandResolver.coerceValueToBoolean(arg, false);
                        }
index 73a4b2341ef835ad42c77261c86c9c3a73f91bd2..28af57a2a5944d19dd922d6aec648be531ad6ff1 100644 (file)
@@ -18,8 +18,6 @@
 package org.apache.poi.ss.formula.functions;
 
 import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
 
 import org.apache.poi.ss.formula.eval.ErrorEval;
 import org.apache.poi.ss.formula.eval.EvaluationException;
@@ -32,14 +30,17 @@ import org.apache.poi.ss.usermodel.DateUtil;
  * Implementation of Excel functions Date parsing functions:
  *  Date - DAY, MONTH and YEAR
  *  Time - HOUR, MINUTE and SECOND
+ *
+ * @author Others (not mentioned in code)
+ * @author Thies Wellpott
  */
 public final class CalendarFieldFunction extends Fixed1ArgFunction {
        public static final Function YEAR = new CalendarFieldFunction(Calendar.YEAR);
        public static final Function MONTH = new CalendarFieldFunction(Calendar.MONTH);
        public static final Function DAY = new CalendarFieldFunction(Calendar.DAY_OF_MONTH);
        public static final Function HOUR = new CalendarFieldFunction(Calendar.HOUR_OF_DAY);
-   public static final Function MINUTE = new CalendarFieldFunction(Calendar.MINUTE);
-   public static final Function SECOND = new CalendarFieldFunction(Calendar.SECOND);
+    public static final Function MINUTE = new CalendarFieldFunction(Calendar.MINUTE);
+    public static final Function SECOND = new CalendarFieldFunction(Calendar.SECOND);
 
        private final int _dateFieldId;
 
@@ -64,7 +65,7 @@ public final class CalendarFieldFunction extends Fixed1ArgFunction {
        private int getCalField(double serialDate) {
           // For some reason, a date of 0 in Excel gets shown
           //  as the non existant 1900-01-00
-               if(((int)serialDate) == 0) {
+               if (((int)serialDate) == 0) {
                        switch (_dateFieldId) {
                                case Calendar.YEAR: return 1900;
                                case Calendar.MONTH: return 1;
@@ -74,10 +75,9 @@ public final class CalendarFieldFunction extends Fixed1ArgFunction {
                }
 
                // TODO Figure out if we're in 1900 or 1904
-               Date d = DateUtil.getJavaDate(serialDate, false);
-
-               Calendar c = new GregorianCalendar();
-               c.setTime(d);
+               // EXCEL functions round up nearly a half second (probably to prevent floating point
+               // rounding issues); use UTC here to prevent daylight saving issues for HOUR
+               Calendar c = DateUtil.getJavaCalendarUTC(serialDate + 0.4995 / DateUtil.SECONDS_PER_DAY, false);
                int result = c.get(_dateFieldId);
                
                // Month is a special case due to C semantics
diff --git a/src/java/org/apache/poi/ss/formula/functions/WeekdayFunc.java b/src/java/org/apache/poi/ss/formula/functions/WeekdayFunc.java
new file mode 100644 (file)
index 0000000..0720b11
--- /dev/null
@@ -0,0 +1,127 @@
+/* ====================================================================
+   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.ss.formula.functions;
+
+import java.util.Calendar;
+
+import org.apache.poi.ss.formula.eval.BlankEval;
+import org.apache.poi.ss.formula.eval.ErrorEval;
+import org.apache.poi.ss.formula.eval.EvaluationException;
+import org.apache.poi.ss.formula.eval.MissingArgEval;
+import org.apache.poi.ss.formula.eval.NumberEval;
+import org.apache.poi.ss.formula.eval.OperandResolver;
+import org.apache.poi.ss.formula.eval.ValueEval;
+import org.apache.poi.ss.usermodel.DateUtil;
+
+
+/**
+ * Implementation for the Excel function WEEKDAY
+ *
+ * @author Thies Wellpott
+ */
+public final class WeekdayFunc implements Function {
+//or:  extends Var1or2ArgFunction {
+
+       public static final Function instance = new WeekdayFunc();
+
+       private WeekdayFunc() {
+               // no fields to initialise
+       }
+
+       /* for Var1or2ArgFunction:
+       @Override
+       public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) {
+       }
+
+       @Override
+       public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {
+       }
+       */
+
+
+       /**
+        * Perform WEEKDAY(date, returnOption) function.
+        * Note: Parameter texts are from German EXCEL-2010 help.
+        * Parameters in args[]:
+        * @param serialDate
+        * EXCEL-date value
+        * Standardmaessig ist der 1. Januar 1900 die fortlaufende Zahl 1 und
+        * der 1. Januar 2008 die fortlaufende Zahl 39.448, da dieser Tag nach 39.448 Tagen
+        * auf den 01.01.1900 folgt.
+        * @param returnOption (optional)
+        * Bestimmt den Rueckgabewert:
+               1       oder nicht angegeben Zahl 1 (Sonntag) bis 7 (Samstag). Verhaelt sich wie fruehere Microsoft Excel-Versionen.
+               2       Zahl 1 (Montag) bis 7 (Sonntag).
+               3       Zahl 0 (Montag) bis 6 (Sonntag).
+               11      Die Zahlen 1 (Montag) bis 7 (Sonntag)
+               12      Die Zahlen 1 (Dienstag) bis 7 (Montag)
+               13      Die Zahlen 1 (Mittwoch) bis 7 (Dienstag)
+               14      Die Zahlen 1 (Donnerstag) bis 7 (Mittwoch)
+               15      Die Zahlen 1 (Freitag) bis 7 (Donnerstag)
+               16      Die Zahlen 1 (Samstag) bis 7 (Freitag)
+               17      Die Zahlen 1 (Sonntag) bis 7 (Samstag)
+        */
+       @Override
+       public ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex) {
+               try {
+                       if (args.length < 1  ||  args.length > 2) {
+                               return ErrorEval.VALUE_INVALID;
+                       }
+
+                       // extract first parameter
+                       ValueEval serialDateVE = OperandResolver.getSingleValue(args[0], srcRowIndex, srcColumnIndex);
+                       double serialDate = OperandResolver.coerceValueToDouble(serialDateVE);
+                       if (!DateUtil.isValidExcelDate(serialDate)) {
+                               return ErrorEval.NUM_ERROR;                                             // EXCEL uses this and no VALUE_ERROR
+                       }
+                       Calendar date = DateUtil.getJavaCalendar(serialDate, false);            // (XXX 1904-windowing not respected)
+                       int weekday = date.get(Calendar.DAY_OF_WEEK);           // => sunday = 1, monday = 2, ..., saturday = 7
+
+                       // extract second parameter
+                       int returnOption = 1;                                   // default value
+                       if (args.length == 2) {
+                               ValueEval ve = OperandResolver.getSingleValue(args[1], srcRowIndex, srcColumnIndex);
+                               if (ve == MissingArgEval.instance  ||  ve == BlankEval.instance) {
+                                       return ErrorEval.NUM_ERROR;             // EXCEL uses this and no VALUE_ERROR
+                               }
+                               returnOption = OperandResolver.coerceValueToInt(ve);
+                               if (returnOption == 2) {
+                                       returnOption = 11;                              // both mean the same
+                               }
+                       } // if
+
+                       // perform calculation
+                       double result;
+                       if (returnOption == 1) {
+                               result = weekday;
+                       // value 2 is handled above (as value 11)
+                       } else if (returnOption == 3) {
+                               result = (weekday + 6 - 1) % 7;
+                       } else if (returnOption >= 11  &&  returnOption <= 17) {
+                               result = (weekday + 6 - (returnOption - 10)) % 7 + 1;           // rotate in the value range 1 to 7
+                       } else {
+                               return ErrorEval.NUM_ERROR;             // EXCEL uses this and no VALUE_ERROR
+                       }
+
+                       return new NumberEval(result);
+               } catch (EvaluationException e) {
+                       return e.getErrorEval();
+               }
+       } // evaluate()
+
+}
index 9d1af834dc88288d6e7240390e1d3550741f17ea..31942449ffc20dd61bc2ad97724f20589486eb03 100644 (file)
@@ -33,18 +33,20 @@ import java.util.regex.Pattern;
  * @author  Hack Kampbjorn (hak at 2mba.dk)
  * @author  Alex Jacoby (ajacoby at gmail.com)
  * @author  Pavel Krupets (pkrupets at palmtreebusiness dot com)
+ * @author  Thies Wellpott
  */
 public class DateUtil {
     protected DateUtil() {
         // no instances of this class
     }
-    private static final int SECONDS_PER_MINUTE = 60;
-    private static final int MINUTES_PER_HOUR = 60;
-    private static final int HOURS_PER_DAY = 24;
-    private static final int SECONDS_PER_DAY = (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
+
+    public static final int SECONDS_PER_MINUTE = 60;
+    public static final int MINUTES_PER_HOUR = 60;
+    public static final int HOURS_PER_DAY = 24;
+    public static final int SECONDS_PER_DAY = (HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
 
     private static final int    BAD_DATE         = -1;   // used to specify that date is invalid
-    private static final long   DAY_MILLISECONDS = SECONDS_PER_DAY * 1000L;
+    public static final long   DAY_MILLISECONDS = SECONDS_PER_DAY * 1000L;
 
     private static final Pattern TIME_SEPARATOR_PATTERN = Pattern.compile(":");
 
@@ -57,6 +59,10 @@ public class DateUtil {
     //  elapsed time patterns: [h],[m] and [s]
     private static final Pattern date_ptrn4 = Pattern.compile("^\\[([hH]+|[mM]+|[sS]+)\\]");
 
+    // only get this static info once (because operations are not really cheap)
+    private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone("UTC");
+
+
     /**
      * Given a Date, converts it into a double representing its internal Excel representation,
      *   which is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
@@ -178,19 +184,7 @@ public class DateUtil {
      *  @return Java representation of the date, or null if date is not a valid Excel date
      */
     public static Date getJavaDate(double date, boolean use1904windowing, TimeZone tz) {
-       if (!isValidExcelDate(date)) {
-          return null;
-       }
-       Calendar calendar;
-       if (tz != null)
-          calendar = new GregorianCalendar(tz);
-       else
-          calendar = new GregorianCalendar(); // using default time-zone
-       
-      int wholeDays = (int)Math.floor(date);
-      int millisecondsInDay = (int)((date - wholeDays) * DAY_MILLISECONDS + 0.5);
-      setCalendar(calendar, wholeDays, millisecondsInDay, use1904windowing);
-      return calendar.getTime();
+        return getJavaCalendar(date, use1904windowing, tz).getTime();
     }
     /**
      *  Given an Excel date with either 1900 or 1904 date windowing,
@@ -212,8 +206,10 @@ public class DateUtil {
      *  @see java.util.TimeZone
      */
     public static Date getJavaDate(double date, boolean use1904windowing) {
-       return getJavaDate(date, use1904windowing, (TimeZone)null);
+        return getJavaCalendar(date, use1904windowing).getTime();
     }
+
+
     public static void setCalendar(Calendar calendar, int wholeDays,
             int millisecondsInDay, boolean use1904windowing) {
         int startYear = 1900;
@@ -232,6 +228,54 @@ public class DateUtil {
     }
 
 
+    /**
+     * Get EXCEL date as Java Calendar (with default time zone).
+     * This is like {@link #getJavaDate(double, boolean)} but returns a Calendar object.
+     *  @param date  The Excel date.
+     *  @param use1904windowing  true if date uses 1904 windowing,
+     *   or false if using 1900 date windowing.
+     *  @return Java representation of the date, or null if date is not a valid Excel date
+     */
+    public static Calendar getJavaCalendar(double date, boolean use1904windowing) {
+       return getJavaCalendar(date, use1904windowing, (TimeZone)null);
+    }
+
+    /**
+     * Get EXCEL date as Java Calendar with UTC time zone.
+     * This is similar to {@link #getJavaDate(double, boolean)} but returns a
+     * Calendar object that has UTC as time zone, so no daylight saving hassle.
+     *  @param date  The Excel date.
+     *  @param use1904windowing  true if date uses 1904 windowing,
+     *   or false if using 1900 date windowing.
+     *  @return Java representation of the date in UTC, or null if date is not a valid Excel date
+     */
+    public static Calendar getJavaCalendarUTC(double date, boolean use1904windowing) {
+       return getJavaCalendar(date, use1904windowing, TIMEZONE_UTC);
+    }
+
+
+    /**
+     * Get EXCEL date as Java Calendar with given time zone.
+     * @see getJavaDate(double, TimeZone)
+     * @return Java representation of the date, or null if date is not a valid Excel date
+     */
+    public static Calendar getJavaCalendar(double date, boolean use1904windowing, TimeZone timeZone) {
+        if (!isValidExcelDate(date)) {
+            return null;
+        }
+        int wholeDays = (int)Math.floor(date);
+        int millisecondsInDay = (int)((date - wholeDays) * DAY_MILLISECONDS + 0.5);
+        Calendar calendar;
+        if (timeZone != null) {
+            calendar = new GregorianCalendar(timeZone);
+        } else {
+            calendar = new GregorianCalendar();     // using default time-zone
+        }
+        setCalendar(calendar, wholeDays, millisecondsInDay, use1904windowing);
+        return calendar;
+    }
+
+
     /**
      * Given a format ID and its format String, will check to see if the
      *  format represents a date format or not.
@@ -257,7 +301,7 @@ public class DateUtil {
         }
 
         String fs = formatString;
-        if (false) {
+        /*if (false) {
             // Normalize the format string. The code below is equivalent
             // to the following consecutive regexp replacements:
 
@@ -276,7 +320,7 @@ public class DateUtil {
 
              // The code above was reworked as suggested in bug 48425:
              // simple loop is more efficient than consecutive regexp replacements.
-        }
+        }*/
         StringBuilder sb = new StringBuilder(fs.length());
         for (int i = 0; i < fs.length(); i++) {
             char c = fs.charAt(i);
index 07fb6aa895808f5b134a45884c2f33a94afa88ce..013b15806c68ac616cf0da74c4c0fa511d127a46 100644 (file)
@@ -33,6 +33,9 @@ public final class TestAddress extends TestCase {
         String formulaText = "ADDRESS(1,2)";\r
         confirmResult(fe, cell, formulaText, "$B$1");\r
 \r
+        formulaText = "ADDRESS(1,2,)";                                 // with explicitly empty third parameter\r
+        confirmResult(fe, cell, formulaText, "$B$1");\r
+\r
         formulaText = "ADDRESS(22,44)";\r
         confirmResult(fe, cell, formulaText, "$AR$22");\r
 \r
index ede20de393caf1b3ecd28f0807e827c9b44b8649..636c72a427c2bbec5060eb4221441442ee7bca82 100644 (file)
@@ -59,6 +59,33 @@ public final class TestCalendarFieldFunction extends TestCase {
         confirm("SECOND(40627.4860417)", 54);
     }
 
+    public void testRounding() {
+               // 41484.999994200 = 23:59:59,499
+               // 41484.9999942129 = 23:59:59,500  (but sub-milliseconds are below 0.5 (0.49999453965575), XLS-second results in 59)
+               // 41484.9999942130 = 23:59:59,500  (sub-milliseconds are 0.50000334065408, XLS-second results in 00)
+
+        confirm("DAY(41484.999994200)", 29);
+        confirm("SECOND(41484.999994200)", 59);
+
+        confirm("DAY(41484.9999942129)", 29);
+        confirm("HOUR(41484.9999942129)", 23);
+        confirm("MINUTE(41484.9999942129)", 59);
+        confirm("SECOND(41484.9999942129)", 59);
+               
+        confirm("DAY(41484.9999942130)", 30);
+        confirm("HOUR(41484.9999942130)", 0);
+        confirm("MINUTE(41484.9999942130)", 0);
+        confirm("SECOND(41484.9999942130)", 0);
+       }
+
+    public void testDaylightSaving() {
+        confirm("HOUR(41364.08263888890000)", 1);              // 31.03.2013 01:59:00,000
+        confirm("HOUR(41364.08333333330000)", 2);              // 31.03.2013 02:00:00,000 (this time does not exist in TZ CET, but EXCEL does not care)
+        confirm("HOUR(41364.08402777780000)", 2);              // 31.03.2013 02:01:00,000
+        confirm("HOUR(41364.12430555560000)", 2);              // 31.03.2013 02:59:00,000
+        confirm("HOUR(41364.12500000000000)", 3);              // 31.03.2013 03:00:00,000
+       }
+
     public void testBugDate() {
         confirm("YEAR(0.0)", 1900);
         confirm("MONTH(0.0)", 1);