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
This commit is contained in:
Yegor Kozlov 2012-08-11 16:14:49 +00:00
parent c91082dc80
commit e2385e48b6
11 changed files with 243 additions and 36 deletions

View File

@ -50,11 +50,11 @@
<filterchain>
<!-- capture any new files -->
<linecontainsregexp>
<regexp pattern="(\?|A)......"/>
<regexp pattern="^(\?|A)......"/>
</linecontainsregexp>
<!-- filter out the first six characters -->
<tokenfilter>
<replaceregex pattern="(.......)" replace=""/>
<replaceregex pattern="^(.......)" replace=""/>
</tokenfilter>
<!--remove line breaks -->
<striplinebreaks/>

View File

@ -88,11 +88,11 @@
<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();
@ -366,6 +366,7 @@
VAR
VARP
VLOOKUP
WEEKDAY
WORKDAY
YEAR
YEARFRAC

View 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>

View 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;

View File

@ -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:

View 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);
}

View 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

View File

@ -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()
}

View 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);

View File

@ -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");

View 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);