From e2385e48b6fbf2ae9d10f8736062e3c027df55e6 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Sat, 11 Aug 2012 16:14:49 +0000 Subject: [PATCH] Bugzilla 53644: XLS formula bugfix (CalFieldFunc) + WeekDay addon git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1371969 13f79535-47bb-0310-9956-ffa450edef68 --- patch.xml | 4 +- .../xdocs/spreadsheet/eval-devguide.xml | 5 +- src/documentation/content/xdocs/status.xml | 1 + .../poi/ss/formula/eval/FunctionEval.java | 1 + .../poi/ss/formula/functions/Address.java | 4 +- .../ss/formula/functions/BooleanFunction.java | 3 + .../functions/CalendarFieldFunction.java | 18 +-- .../poi/ss/formula/functions/WeekdayFunc.java | 127 ++++++++++++++++++ .../org/apache/poi/ss/usermodel/DateUtil.java | 86 +++++++++--- .../poi/ss/formula/functions/TestAddress.java | 3 + .../functions/TestCalendarFieldFunction.java | 27 ++++ 11 files changed, 243 insertions(+), 36 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/functions/WeekdayFunc.java diff --git a/patch.xml b/patch.xml index 56852f4b5..61b3cb7a9 100755 --- a/patch.xml +++ b/patch.xml @@ -50,11 +50,11 @@ - + - + diff --git a/src/documentation/content/xdocs/spreadsheet/eval-devguide.xml b/src/documentation/content/xdocs/spreadsheet/eval-devguide.xml index f7c17650b..99eecff4e 100644 --- a/src/documentation/content/xdocs/spreadsheet/eval-devguide.xml +++ b/src/documentation/content/xdocs/spreadsheet/eval-devguide.xml @@ -88,11 +88,11 @@

As of Feb 2012, POI supports about 140 built-in functions, see Appendix A 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:

// list of functions that POI can evaluate - Collection<String> suportedFuncs = WorkbookEvaluator.getSupportedFunctionNames(); + Collection<String> supportedFuncs = WorkbookEvaluator.getSupportedFunctionNames(); // list of functions that are not supported by POI Collection<String> unsupportedFuncs = WorkbookEvaluator.getNotSupportedFunctionNames(); @@ -366,6 +366,7 @@ VAR VARP VLOOKUP + WEEKDAY WORKDAY YEAR YEARFRAC diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index d3a744b48..57109eeb9 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 53644 - XLS formula bugfix (CalFieldFunc) + WeekDay addon 53446 - Fixed some problems extracting PNGs 53205 - Fixed some parsing errors and encoding issues in HDGF 53204 - Improved performanceof PageSettingsBlock in HSSF diff --git a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java index fb4f8d725..c76da52c1 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -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; diff --git a/src/java/org/apache/poi/ss/formula/functions/Address.java b/src/java/org/apache/poi/ss/formula/functions/Address.java index 288a88edc..379135f14 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Address.java +++ b/src/java/org/apache/poi/ss/formula/functions/Address.java @@ -43,10 +43,10 @@ public class Address implements Function { int col = (int)NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex); int refType; - if(args.length > 2){ + if (args.length > 2 && args[2] != MissingArgEval.instance) { refType = (int)NumericFunction.singleOperandEvaluate(args[2], srcRowIndex, srcColumnIndex); } else { - refType = REF_ABSOLUTE; + refType = REF_ABSOLUTE; // this is also the default if parameter is not given } switch (refType){ case REF_ABSOLUTE: diff --git a/src/java/org/apache/poi/ss/formula/functions/BooleanFunction.java b/src/java/org/apache/poi/ss/formula/functions/BooleanFunction.java index b90f929fb..9e796e704 100644 --- a/src/java/org/apache/poi/ss/formula/functions/BooleanFunction.java +++ b/src/java/org/apache/poi/ss/formula/functions/BooleanFunction.java @@ -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); } diff --git a/src/java/org/apache/poi/ss/formula/functions/CalendarFieldFunction.java b/src/java/org/apache/poi/ss/formula/functions/CalendarFieldFunction.java index 73a4b2341..28af57a2a 100644 --- a/src/java/org/apache/poi/ss/formula/functions/CalendarFieldFunction.java +++ b/src/java/org/apache/poi/ss/formula/functions/CalendarFieldFunction.java @@ -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 index 000000000..0720b11f1 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/WeekdayFunc.java @@ -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() + +} diff --git a/src/java/org/apache/poi/ss/usermodel/DateUtil.java b/src/java/org/apache/poi/ss/usermodel/DateUtil.java index 9d1af834d..31942449f 100644 --- a/src/java/org/apache/poi/ss/usermodel/DateUtil.java +++ b/src/java/org/apache/poi/ss/usermodel/DateUtil.java @@ -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); diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestAddress.java b/src/testcases/org/apache/poi/ss/formula/functions/TestAddress.java index 07fb6aa89..013b15806 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestAddress.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestAddress.java @@ -33,6 +33,9 @@ public final class TestAddress extends TestCase { String formulaText = "ADDRESS(1,2)"; confirmResult(fe, cell, formulaText, "$B$1"); + formulaText = "ADDRESS(1,2,)"; // with explicitly empty third parameter + confirmResult(fe, cell, formulaText, "$B$1"); + formulaText = "ADDRESS(22,44)"; confirmResult(fe, cell, formulaText, "$AR$22"); diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestCalendarFieldFunction.java b/src/testcases/org/apache/poi/ss/formula/functions/TestCalendarFieldFunction.java index ede20de39..636c72a42 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestCalendarFieldFunction.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestCalendarFieldFunction.java @@ -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);