From: Yegor Kozlov Date: Sun, 21 Nov 2010 12:04:56 +0000 (+0000) Subject: moved common formula-related code to org.apache.poi.ss.formula, eliminated dependenci... X-Git-Tag: REL_3_8_BETA1~121 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b34e1e25d6500ed4249ab35486429ecb804e4f52;p=poi.git moved common formula-related code to org.apache.poi.ss.formula, eliminated dependencies on HSSF, reduced the number of eclipse warnings git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1037436 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java new file mode 100644 index 0000000000..58871a64ba --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -0,0 +1,164 @@ +/* ==================================================================== + 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.atp; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.FreeRefFunction; +import org.apache.poi.ss.formula.udf.UDFFinder; +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.NotImplementedException; + +/** + * @author Josh Micich + * @author Petr Udalau - systematized work of add-in libraries and user defined functions. + */ +public final class AnalysisToolPak implements UDFFinder { + + public static final UDFFinder instance = new AnalysisToolPak(); + + private static final class NotImplemented implements FreeRefFunction { + private final String _functionName; + + public NotImplemented(String functionName) { + _functionName = functionName; + } + + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + throw new NotImplementedException(_functionName); + } + }; + + private final Map _functionsByName = createFunctionsMap(); + + + private AnalysisToolPak() { + // enforce singleton + } + + public FreeRefFunction findFunction(String name) { + return _functionsByName.get(name); + } + + private 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", ParityFunction.IS_EVEN); + r(m, "ISODD", ParityFunction.IS_ODD); + 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, "RANDBETWEEN", RandBetween.instance); + 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 ? new NotImplemented(functionName) : pFunc; + m.put(functionName, func); + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/ParityFunction.java b/src/java/org/apache/poi/ss/formula/atp/ParityFunction.java new file mode 100644 index 0000000000..04b5e92239 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/ParityFunction.java @@ -0,0 +1,67 @@ +/* ==================================================================== + 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.atp; + +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.OperandResolver; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.FreeRefFunction; +import org.apache.poi.ss.formula.OperationEvaluationContext; +/** + * Implementation of Excel 'Analysis ToolPak' function ISEVEN() ISODD()
+ * + * @author Josh Micich + */ +final class ParityFunction implements FreeRefFunction { + + public static final FreeRefFunction IS_EVEN = new ParityFunction(0); + public static final FreeRefFunction IS_ODD = new ParityFunction(1); + private final int _desiredParity; + + private ParityFunction(int desiredParity) { + _desiredParity = desiredParity; + } + + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + if (args.length != 1) { + return ErrorEval.VALUE_INVALID; + } + + int val; + try { + val = evaluateArgParity(args[0], ec.getRowIndex(), ec.getColumnIndex()); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + + return BoolEval.valueOf(val == _desiredParity); + } + + private static int evaluateArgParity(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, (short)srcCellCol); + + double d = OperandResolver.coerceValueToDouble(ve); + if (d < 0) { + d = -d; + } + long v = (long) Math.floor(d); + return (int) (v & 0x0001); + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/RandBetween.java b/src/java/org/apache/poi/ss/formula/atp/RandBetween.java new file mode 100644 index 0000000000..4da7a53ef5 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/RandBetween.java @@ -0,0 +1,84 @@ +/* ==================================================================== + 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.atp; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +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.formula.functions.FreeRefFunction; +import org.apache.poi.ss.formula.OperationEvaluationContext; + +/** + * Implementation of Excel 'Analysis ToolPak' function RANDBETWEEN()
+ * + * Returns a random integer number between the numbers you specify.

+ * + * Syntax
+ * RANDBETWEEN(bottom, top)

+ * + * bottom is the smallest integer RANDBETWEEN will return.
+ * top is the largest integer RANDBETWEEN will return.
+ + * @author Brendan Nolan + */ +final class RandBetween implements FreeRefFunction{ + + public static final FreeRefFunction instance = new RandBetween(); + + private RandBetween() { + //enforces singleton + } + + /** + * Evaluate for RANDBETWEEN(). Must be given two arguments. Bottom must be greater than top. + * Bottom is rounded up and top value is rounded down. After rounding top has to be set greater + * than top. + * + * @see org.apache.poi.ss.formula.functions.FreeRefFunction#evaluate(org.apache.poi.ss.formula.eval.ValueEval[], org.apache.poi.ss.formula.OperationEvaluationContext) + */ + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + + double bottom, top; + + if (args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + + try { + bottom = OperandResolver.coerceValueToDouble(OperandResolver.getSingleValue(args[0], ec.getRowIndex(), ec.getColumnIndex())); + top = OperandResolver.coerceValueToDouble(OperandResolver.getSingleValue(args[1], ec.getRowIndex(), ec.getColumnIndex())); + if(bottom > top) { + return ErrorEval.NUM_ERROR; + } + } catch (EvaluationException e) { + return ErrorEval.VALUE_INVALID; + } + + bottom = Math.ceil(bottom); + top = Math.floor(top); + + if(bottom > top) { + top = bottom; + } + + return new NumberEval((bottom + (int)(Math.random() * ((top - bottom) + 1)))); + + } + +} diff --git a/src/java/org/apache/poi/ss/formula/atp/YearFrac.java b/src/java/org/apache/poi/ss/formula/atp/YearFrac.java new file mode 100644 index 0000000000..062fcf6f9e --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/YearFrac.java @@ -0,0 +1,159 @@ +/* ==================================================================== + 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.atp; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.regex.Pattern; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.OperandResolver; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.FreeRefFunction; +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.usermodel.DateUtil; +/** + * Implementation of Excel 'Analysis ToolPak' function YEARFRAC()
+ * + * Returns the fraction of the year spanned by two dates.

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

+ * + * The basis optionally specifies the behaviour of YEARFRAC as follows: + * + * + * + * + * + * + * + * + *
ValueDays per MonthDays per Year
0 (default)30360
1actualactual
2actual360
3actual365
430360
+ * + */ +final class YearFrac implements FreeRefFunction { + + public static final FreeRefFunction instance = new YearFrac(); + + private YearFrac() { + // enforce singleton + } + + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + int srcCellRow = ec.getRowIndex(); + int srcCellCol = ec.getColumnIndex(); + 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(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, (short) 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 DateUtil.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); + } + cal.set(Calendar.DAY_OF_MONTH, day); + return cal; + } + + private static int evaluateIntArg(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, (short) srcCellCol); + return OperandResolver.coerceValueToInt(ve); + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/YearFracCalculator.java b/src/java/org/apache/poi/ss/formula/atp/YearFracCalculator.java new file mode 100644 index 0000000000..765b23ae6c --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/YearFracCalculator.java @@ -0,0 +1,344 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.atp; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.usermodel.DateUtil; + + +/** + * Internal calculation methods for Excel 'Analysis ToolPak' function YEARFRAC()
+ * + * Algorithm inspired by www.dwheeler.com/yearfrac + * + * @author Josh Micich + */ +final class YearFracCalculator { + /** use UTC time-zone to avoid daylight savings issues */ + private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); + private static final int MS_PER_HOUR = 60 * 60 * 1000; + private static final int MS_PER_DAY = 24 * MS_PER_HOUR; + private static final int DAYS_PER_NORMAL_YEAR = 365; + private static final int DAYS_PER_LEAP_YEAR = DAYS_PER_NORMAL_YEAR + 1; + + /** the length of normal long months i.e. 31 */ + private static final int LONG_MONTH_LEN = 31; + /** the length of normal short months i.e. 30 */ + private static final int SHORT_MONTH_LEN = 30; + private static final int SHORT_FEB_LEN = 28; + private static final int LONG_FEB_LEN = SHORT_FEB_LEN + 1; + + private YearFracCalculator() { + // no instances of this class + } + + + public static double calculate(double pStartDateVal, double pEndDateVal, int basis) throws EvaluationException { + + if (basis < 0 || basis >= 5) { + // if basis is invalid the result is #NUM! + throw new EvaluationException(ErrorEval.NUM_ERROR); + } + + // common logic for all bases + + // truncate day values + int startDateVal = (int) Math.floor(pStartDateVal); + int endDateVal = (int) Math.floor(pEndDateVal); + if (startDateVal == endDateVal) { + // when dates are equal, result is zero + return 0; + } + // swap start and end if out of order + if (startDateVal > endDateVal) { + int temp = startDateVal; + startDateVal = endDateVal; + endDateVal = temp; + } + + switch (basis) { + case 0: return basis0(startDateVal, endDateVal); + case 1: return basis1(startDateVal, endDateVal); + case 2: return basis2(startDateVal, endDateVal); + case 3: return basis3(startDateVal, endDateVal); + case 4: return basis4(startDateVal, endDateVal); + } + throw new IllegalStateException("cannot happen"); + } + + + /** + * @param startDateVal assumed to be less than or equal to endDateVal + * @param endDateVal assumed to be greater than or equal to startDateVal + */ + public static double basis0(int startDateVal, int endDateVal) { + SimpleDate startDate = createDate(startDateVal); + SimpleDate endDate = createDate(endDateVal); + int date1day = startDate.day; + int date2day = endDate.day; + + // basis zero has funny adjustments to the day-of-month fields when at end-of-month + if (date1day == LONG_MONTH_LEN && date2day == LONG_MONTH_LEN) { + date1day = SHORT_MONTH_LEN; + date2day = SHORT_MONTH_LEN; + } else if (date1day == LONG_MONTH_LEN) { + date1day = SHORT_MONTH_LEN; + } else if (date1day == SHORT_MONTH_LEN && date2day == LONG_MONTH_LEN) { + date2day = SHORT_MONTH_LEN; + // Note: If date2day==31, it STAYS 31 if date1day < 30. + // Special fixes for February: + } else if (startDate.month == 2 && isLastDayOfMonth(startDate)) { + // Note - these assignments deliberately set Feb 30 date. + date1day = SHORT_MONTH_LEN; + if (endDate.month == 2 && isLastDayOfMonth(endDate)) { + // only adjusted when first date is last day in Feb + date2day = SHORT_MONTH_LEN; + } + } + return calculateAdjusted(startDate, endDate, date1day, date2day); + } + /** + * @param startDateVal assumed to be less than or equal to endDateVal + * @param endDateVal assumed to be greater than or equal to startDateVal + */ + public static double basis1(int startDateVal, int endDateVal) { + SimpleDate startDate = createDate(startDateVal); + SimpleDate endDate = createDate(endDateVal); + double yearLength; + if (isGreaterThanOneYear(startDate, endDate)) { + yearLength = averageYearLength(startDate.year, endDate.year); + } else if (shouldCountFeb29(startDate, endDate)) { + yearLength = DAYS_PER_LEAP_YEAR; + } else { + yearLength = DAYS_PER_NORMAL_YEAR; + } + return dateDiff(startDate.tsMilliseconds, endDate.tsMilliseconds) / yearLength; + } + + /** + * @param startDateVal assumed to be less than or equal to endDateVal + * @param endDateVal assumed to be greater than or equal to startDateVal + */ + public static double basis2(int startDateVal, int endDateVal) { + return (endDateVal - startDateVal) / 360.0; + } + /** + * @param startDateVal assumed to be less than or equal to endDateVal + * @param endDateVal assumed to be greater than or equal to startDateVal + */ + public static double basis3(double startDateVal, double endDateVal) { + return (endDateVal - startDateVal) / 365.0; + } + /** + * @param startDateVal assumed to be less than or equal to endDateVal + * @param endDateVal assumed to be greater than or equal to startDateVal + */ + public static double basis4(int startDateVal, int endDateVal) { + SimpleDate startDate = createDate(startDateVal); + SimpleDate endDate = createDate(endDateVal); + int date1day = startDate.day; + int date2day = endDate.day; + + + // basis four has funny adjustments to the day-of-month fields when at end-of-month + if (date1day == LONG_MONTH_LEN) { + date1day = SHORT_MONTH_LEN; + } + if (date2day == LONG_MONTH_LEN) { + date2day = SHORT_MONTH_LEN; + } + // Note - no adjustments for end of Feb + return calculateAdjusted(startDate, endDate, date1day, date2day); + } + + + private static double calculateAdjusted(SimpleDate startDate, SimpleDate endDate, int date1day, + int date2day) { + double dayCount + = (endDate.year - startDate.year) * 360 + + (endDate.month - startDate.month) * SHORT_MONTH_LEN + + (date2day - date1day) * 1; + return dayCount / 360; + } + + private static boolean isLastDayOfMonth(SimpleDate date) { + if (date.day < SHORT_FEB_LEN) { + return false; + } + return date.day == getLastDayOfMonth(date); + } + + private static int getLastDayOfMonth(SimpleDate date) { + switch (date.month) { + case 1: + case 3: + case 5: + case 7: + case 8: + case 10: + case 12: + return LONG_MONTH_LEN; + case 4: + case 6: + case 9: + case 11: + return SHORT_MONTH_LEN; + } + if (isLeapYear(date.year)) { + return LONG_FEB_LEN; + } + return SHORT_FEB_LEN; + } + + /** + * Assumes dates are no more than 1 year apart. + * @return true if dates both within a leap year, or span a period including Feb 29 + */ + private static boolean shouldCountFeb29(SimpleDate start, SimpleDate end) { + boolean startIsLeapYear = isLeapYear(start.year); + if (startIsLeapYear && start.year == end.year) { + // note - dates may not actually span Feb-29, but it gets counted anyway in this case + return true; + } + + boolean endIsLeapYear = isLeapYear(end.year); + if (!startIsLeapYear && !endIsLeapYear) { + return false; + } + if (startIsLeapYear) { + switch (start.month) { + case SimpleDate.JANUARY: + case SimpleDate.FEBRUARY: + return true; + } + return false; + } + if (endIsLeapYear) { + switch (end.month) { + case SimpleDate.JANUARY: + return false; + case SimpleDate.FEBRUARY: + break; + default: + return true; + } + return end.day == LONG_FEB_LEN; + } + return false; + } + + /** + * @return the whole number of days between the two time-stamps. Both time-stamps are + * assumed to represent 12:00 midnight on the respective day. + */ + private static int dateDiff(long startDateMS, long endDateMS) { + long msDiff = endDateMS - startDateMS; + + // some extra checks to make sure we don't hide some other bug with the rounding + int remainderHours = (int) ((msDiff % MS_PER_DAY) / MS_PER_HOUR); + switch (remainderHours) { + case 0: // normal case + break; + case 1: // transition from normal time to daylight savings adjusted + case 23: // transition from daylight savings adjusted to normal time + // Unexpected since we are using UTC_TIME_ZONE + default: + throw new RuntimeException("Unexpected date diff between " + startDateMS + " and " + endDateMS); + + } + return (int) (0.5 + ((double)msDiff / MS_PER_DAY)); + } + + private static double averageYearLength(int startYear, int endYear) { + int dayCount = 0; + for (int i=startYear; i<=endYear; i++) { + dayCount += DAYS_PER_NORMAL_YEAR; + if (isLeapYear(i)) { + dayCount++; + } + } + double numberOfYears = endYear-startYear+1; + return dayCount / numberOfYears; + } + + private static boolean isLeapYear(int i) { + // leap years are always divisible by 4 + if (i % 4 != 0) { + return false; + } + // each 4th century is a leap year + if (i % 400 == 0) { + return true; + } + // all other centuries are *not* leap years + if (i % 100 == 0) { + return false; + } + return true; + } + + private static boolean isGreaterThanOneYear(SimpleDate start, SimpleDate end) { + if (start.year == end.year) { + return false; + } + if (start.year + 1 != end.year) { + return true; + } + + if (start.month > end.month) { + return false; + } + if (start.month < end.month) { + return true; + } + + return start.day < end.day; + } + + private static SimpleDate createDate(int dayCount) { + GregorianCalendar calendar = new GregorianCalendar(UTC_TIME_ZONE); + DateUtil.setCalendar(calendar, dayCount, 0, false); + return new SimpleDate(calendar); + } + + private static final class SimpleDate { + + public static final int JANUARY = 1; + public static final int FEBRUARY = 2; + + public final int year; + /** 1-based month */ + public final int month; + /** day of month */ + public final int day; + /** milliseconds since 1970 */ + public long tsMilliseconds; + + public SimpleDate(Calendar cal) { + year = cal.get(Calendar.YEAR); + month = cal.get(Calendar.MONTH) + 1; + day = cal.get(Calendar.DAY_OF_MONTH); + tsMilliseconds = cal.getTimeInMillis(); + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/AreaEval.java b/src/java/org/apache/poi/ss/formula/eval/AreaEval.java new file mode 100644 index 0000000000..ec2e41cf22 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/AreaEval.java @@ -0,0 +1,93 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.TwoDEval; +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * + */ +public interface AreaEval extends TwoDEval { + + /** + * returns the 0-based index of the first row in + * this area. + */ + int getFirstRow(); + + /** + * returns the 0-based index of the last row in + * this area. + */ + int getLastRow(); + + /** + * returns the 0-based index of the first col in + * this area. + */ + int getFirstColumn(); + + /** + * returns the 0-based index of the last col in + * this area. + */ + int getLastColumn(); + + /** + * @return the ValueEval from within this area at the specified row and col index. Never + * null (possibly {@link BlankEval}). The specified indexes should be absolute + * indexes in the sheet and not relative indexes within the area. + */ + ValueEval getAbsoluteValue(int row, int col); + + /** + * returns true if the cell at row and col specified + * as absolute indexes in the sheet is contained in + * this area. + * @param row + * @param col + */ + boolean contains(int row, int col); + + /** + * returns true if the specified col is in range + * @param col + */ + boolean containsColumn(int col); + + /** + * returns true if the specified row is in range + * @param row + */ + boolean containsRow(int row); + + int getWidth(); + int getHeight(); + /** + * @return the ValueEval from within this area at the specified relativeRowIndex and + * relativeColumnIndex. Never null (possibly {@link BlankEval}). The + * specified indexes should relative to the top left corner of this area. + */ + ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex); + + /** + * Creates an {@link AreaEval} offset by a relative amount from from the upper left cell + * of this area + */ + AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx); +} diff --git a/src/java/org/apache/poi/ss/formula/eval/AreaEvalBase.java b/src/java/org/apache/poi/ss/formula/eval/AreaEvalBase.java new file mode 100644 index 0000000000..8f09e6e07a --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/AreaEvalBase.java @@ -0,0 +1,117 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.hssf.record.formula.AreaI; + +/** + * @author Josh Micich + */ +public abstract class AreaEvalBase implements AreaEval { + + private final int _firstColumn; + private final int _firstRow; + private final int _lastColumn; + private final int _lastRow; + private final int _nColumns; + private final int _nRows; + + protected AreaEvalBase(int firstRow, int firstColumn, int lastRow, int lastColumn) { + _firstColumn = firstColumn; + _firstRow = firstRow; + _lastColumn = lastColumn; + _lastRow = lastRow; + + _nColumns = _lastColumn - _firstColumn + 1; + _nRows = _lastRow - _firstRow + 1; + } + + protected AreaEvalBase(AreaI ptg) { + _firstRow = ptg.getFirstRow(); + _firstColumn = ptg.getFirstColumn(); + _lastRow = ptg.getLastRow(); + _lastColumn = ptg.getLastColumn(); + + _nColumns = _lastColumn - _firstColumn + 1; + _nRows = _lastRow - _firstRow + 1; + } + + public final int getFirstColumn() { + return _firstColumn; + } + + public final int getFirstRow() { + return _firstRow; + } + + public final int getLastColumn() { + return _lastColumn; + } + + public final int getLastRow() { + return _lastRow; + } + public final ValueEval getAbsoluteValue(int row, int col) { + int rowOffsetIx = row - _firstRow; + int colOffsetIx = col - _firstColumn; + + if(rowOffsetIx < 0 || rowOffsetIx >= _nRows) { + throw new IllegalArgumentException("Specified row index (" + row + + ") is outside the allowed range (" + _firstRow + ".." + _lastRow + ")"); + } + if(colOffsetIx < 0 || colOffsetIx >= _nColumns) { + throw new IllegalArgumentException("Specified column index (" + col + + ") is outside the allowed range (" + _firstColumn + ".." + col + ")"); + } + return getRelativeValue(rowOffsetIx, colOffsetIx); + } + + public final boolean contains(int row, int col) { + return _firstRow <= row && _lastRow >= row + && _firstColumn <= col && _lastColumn >= col; + } + + public final boolean containsRow(int row) { + return _firstRow <= row && _lastRow >= row; + } + + public final boolean containsColumn(int col) { + return _firstColumn <= col && _lastColumn >= col; + } + + public final boolean isColumn() { + return _firstColumn == _lastColumn; + } + + public final boolean isRow() { + return _firstRow == _lastRow; + } + public int getHeight() { + return _lastRow-_firstRow+1; + } + + public final ValueEval getValue(int row, int col) { + return getRelativeValue(row, col); + } + + public abstract ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex); + + public int getWidth() { + return _lastColumn-_firstColumn+1; + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/BlankEval.java b/src/java/org/apache/poi/ss/formula/eval/BlankEval.java new file mode 100644 index 0000000000..b49b2992ab --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/BlankEval.java @@ -0,0 +1,35 @@ +/* ==================================================================== + 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.eval; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > This class is a + * marker class. It is a special value for empty cells. + */ +public final class BlankEval implements ValueEval { + + public static final BlankEval instance = new BlankEval(); + /** + * @deprecated (Nov 2009) use {@link #instance} + */ + public static final BlankEval INSTANCE = instance; + + private BlankEval() { + // enforce singleton + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/BoolEval.java b/src/java/org/apache/poi/ss/formula/eval/BoolEval.java new file mode 100644 index 0000000000..cab4105d0f --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/BoolEval.java @@ -0,0 +1,64 @@ +/* ==================================================================== + 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.eval; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class BoolEval implements NumericValueEval, StringValueEval { + + private boolean _value; + + public static final BoolEval FALSE = new BoolEval(false); + + public static final BoolEval TRUE = new BoolEval(true); + + /** + * Convenience method for the following:
+ * (b ? BoolEval.TRUE : BoolEval.FALSE) + * + * @return the BoolEval instance representing b. + */ + public static final BoolEval valueOf(boolean b) { + return b ? TRUE : FALSE; + } + + private BoolEval(boolean value) { + _value = value; + } + + public boolean getBooleanValue() { + return _value; + } + + public double getNumberValue() { + return _value ? 1 : 0; + } + + public String getStringValue() { + return _value ? "TRUE" : "FALSE"; + } + + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getStringValue()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/ConcatEval.java b/src/java/org/apache/poi/ss/formula/eval/ConcatEval.java new file mode 100644 index 0000000000..5622be8705 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/ConcatEval.java @@ -0,0 +1,60 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class ConcatEval extends Fixed2ArgFunction { + + public static final Function instance = new ConcatEval(); + + private ConcatEval() { + // enforce singleton + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + ValueEval ve0; + ValueEval ve1; + try { + ve0 = OperandResolver.getSingleValue(arg0, srcRowIndex, srcColumnIndex); + ve1 = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + StringBuilder sb = new StringBuilder(); + sb.append(getText(ve0)); + sb.append(getText(ve1)); + return new StringEval(sb.toString()); + } + + private Object getText(ValueEval ve) { + if (ve instanceof StringValueEval) { + StringValueEval sve = (StringValueEval) ve; + return sve.getStringValue(); + } + if (ve == BlankEval.instance) { + return ""; + } + throw new IllegalAccessError("Unexpected value type (" + + ve.getClass().getName() + ")"); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/ErrorEval.java b/src/java/org/apache/poi/ss/formula/eval/ErrorEval.java new file mode 100644 index 0000000000..6cc8136b37 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/ErrorEval.java @@ -0,0 +1,111 @@ +/* +* 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.eval; + +import org.apache.poi.ss.usermodel.ErrorConstants; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * + */ +public final class ErrorEval implements ValueEval { + + // convenient access to namespace + private static final ErrorConstants EC = null; + + /** #NULL! - Intersection of two cell ranges is empty */ + public static final ErrorEval NULL_INTERSECTION = new ErrorEval(EC.ERROR_NULL); + /** #DIV/0! - Division by zero */ + public static final ErrorEval DIV_ZERO = new ErrorEval(EC.ERROR_DIV_0); + /** #VALUE! - Wrong type of operand */ + public static final ErrorEval VALUE_INVALID = new ErrorEval(EC.ERROR_VALUE); + /** #REF! - Illegal or deleted cell reference */ + public static final ErrorEval REF_INVALID = new ErrorEval(EC.ERROR_REF); + /** #NAME? - Wrong function or range name */ + public static final ErrorEval NAME_INVALID = new ErrorEval(EC.ERROR_NAME); + /** #NUM! - Value range overflow */ + public static final ErrorEval NUM_ERROR = new ErrorEval(EC.ERROR_NUM); + /** #N/A - Argument or function not available */ + public static final ErrorEval NA = new ErrorEval(EC.ERROR_NA); + + + // POI internal error codes + private static final int CIRCULAR_REF_ERROR_CODE = 0xFFFFFFC4; + private static final int FUNCTION_NOT_IMPLEMENTED_CODE = 0xFFFFFFE2; + + // Note - Excel does not seem to represent this condition with an error code + public static final ErrorEval CIRCULAR_REF_ERROR = new ErrorEval(CIRCULAR_REF_ERROR_CODE); + + + /** + * Translates an Excel internal error code into the corresponding POI ErrorEval instance + * @param errorCode + */ + public static ErrorEval valueOf(int errorCode) { + switch(errorCode) { + case ErrorConstants.ERROR_NULL: return NULL_INTERSECTION; + case ErrorConstants.ERROR_DIV_0: return DIV_ZERO; + case ErrorConstants.ERROR_VALUE: return VALUE_INVALID; + case ErrorConstants.ERROR_REF: return REF_INVALID; + case ErrorConstants.ERROR_NAME: return NAME_INVALID; + case ErrorConstants.ERROR_NUM: return NUM_ERROR; + case ErrorConstants.ERROR_NA: return NA; + // non-std errors (conditions modeled as errors by POI) + case CIRCULAR_REF_ERROR_CODE: return CIRCULAR_REF_ERROR; + } + throw new RuntimeException("Unexpected error code (" + errorCode + ")"); + } + + /** + * Converts error codes to text. Handles non-standard error codes OK. + * For debug/test purposes (and for formatting error messages). + * @return the String representation of the specified Excel error code. + */ + public static String getText(int errorCode) { + if(ErrorConstants.isValidCode(errorCode)) { + return ErrorConstants.getText(errorCode); + } + // It is desirable to make these (arbitrary) strings look clearly different from any other + // value expression that might appear in a formula. In addition these error strings should + // look unlike the standard Excel errors. Hence tilde ('~') was used. + switch(errorCode) { + case CIRCULAR_REF_ERROR_CODE: return "~CIRCULAR~REF~"; + case FUNCTION_NOT_IMPLEMENTED_CODE: return "~FUNCTION~NOT~IMPLEMENTED~"; + } + return "~non~std~err(" + errorCode + ")~"; + } + + private int _errorCode; + /** + * @param errorCode an 8-bit value + */ + private ErrorEval(int errorCode) { + _errorCode = errorCode; + } + + public int getErrorCode() { + return _errorCode; + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getText(_errorCode)); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/EvaluationException.java b/src/java/org/apache/poi/ss/formula/eval/EvaluationException.java new file mode 100644 index 0000000000..bb9d9574bc --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/EvaluationException.java @@ -0,0 +1,134 @@ +/* ==================================================================== + 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.eval; + +/** + * This class is used to simplify error handling logic within operator and function + * implementations. Note - OperationEval.evaluate() and Function.evaluate() + * method signatures do not throw this exception so it cannot propagate outside.

+ * + * Here is an example coded without EvaluationException, to show how it can help: + *

+ * public Eval evaluate(Eval[] args, int srcRow, short srcCol) {
+ *	// ...
+ *	Eval arg0 = args[0];
+ *	if(arg0 instanceof ErrorEval) {
+ *		return arg0;
+ *	}
+ *	if(!(arg0 instanceof AreaEval)) {
+ *		return ErrorEval.VALUE_INVALID;
+ *	}
+ *	double temp = 0;
+ *	AreaEval area = (AreaEval)arg0;
+ *	ValueEval[] values = area.getValues();
+ *	for (int i = 0; i < values.length; i++) {
+ *		ValueEval ve = values[i];
+ *		if(ve instanceof ErrorEval) {
+ *			return ve;
+ *		}
+ *		if(!(ve instanceof NumericValueEval)) {
+ *			return ErrorEval.VALUE_INVALID;
+ *		}
+ *		temp += ((NumericValueEval)ve).getNumberValue();
+ *	}
+ *	// ...
+ * }	 
+ * 
+ * In this example, if any error is encountered while processing the arguments, an error is + * returned immediately. This code is difficult to refactor due to all the points where errors + * are returned.
+ * Using EvaluationException allows the error returning code to be consolidated to one + * place.

+ *

+ * public Eval evaluate(Eval[] args, int srcRow, short srcCol) {
+ *	try {
+ *		// ...
+ *		AreaEval area = getAreaArg(args[0]);
+ *		double temp = sumValues(area.getValues());
+ *		// ...
+ *	} catch (EvaluationException e) {
+ *		return e.getErrorEval();
+ *	}
+ *}
+ *
+ *private static AreaEval getAreaArg(Eval arg0) throws EvaluationException {
+ *	if (arg0 instanceof ErrorEval) {
+ *		throw new EvaluationException((ErrorEval) arg0);
+ *	}
+ *	if (arg0 instanceof AreaEval) {
+ *		return (AreaEval) arg0;
+ *	}
+ *	throw EvaluationException.invalidValue();
+ *}
+ *
+ *private double sumValues(ValueEval[] values) throws EvaluationException {
+ *	double temp = 0;
+ *	for (int i = 0; i < values.length; i++) {
+ *		ValueEval ve = values[i];
+ *		if (ve instanceof ErrorEval) {
+ *			throw new EvaluationException((ErrorEval) ve);
+ *		}
+ *		if (!(ve instanceof NumericValueEval)) {
+ *			throw EvaluationException.invalidValue();
+ *		}
+ *		temp += ((NumericValueEval) ve).getNumberValue();
+ *	}
+ *	return temp;
+ *}
+ * 
+ * It is not mandatory to use EvaluationException, doing so might give the following advantages:
+ * - Methods can more easily be extracted, allowing for re-use.
+ * - Type management (typecasting etc) is simpler because error conditions have been separated from + * intermediate calculation values.
+ * - Fewer local variables are required. Local variables can have stronger types.
+ * - It is easier to mimic common Excel error handling behaviour (exit upon encountering first + * error), because exceptions conveniently propagate up the call stack regardless of execution + * points or the number of levels of nested calls.

+ * + * Note - Only standard evaluation errors are represented by EvaluationException ( + * i.e. conditions expected to be encountered when evaluating arbitrary Excel formulas). Conditions + * that could never occur in an Excel spreadsheet should result in runtime exceptions. Care should + * be taken to not translate any POI internal error into an Excel evaluation error code. + * + * @author Josh Micich + */ +public final class EvaluationException extends Exception { + private final ErrorEval _errorEval; + + public EvaluationException(ErrorEval errorEval) { + _errorEval = errorEval; + } + // some convenience factory methods + + /** #VALUE! - Wrong type of operand */ + public static EvaluationException invalidValue() { + return new EvaluationException(ErrorEval.VALUE_INVALID); + } + /** #REF! - Illegal or deleted cell reference */ + public static EvaluationException invalidRef() { + return new EvaluationException(ErrorEval.REF_INVALID); + } + /** #NUM! - Value range overflow */ + public static EvaluationException numberError() { + return new EvaluationException(ErrorEval.NUM_ERROR); + } + + public ErrorEval getErrorEval() { + return _errorEval; + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java new file mode 100644 index 0000000000..85957dad48 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -0,0 +1,249 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.function.FunctionMetadata; +import org.apache.poi.ss.formula.function.FunctionMetadataRegistry; +import org.apache.poi.ss.formula.functions.*; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class FunctionEval { + /** + * Some function IDs that require special treatment + */ + private static final class FunctionID { + /** 1 */ + public static final int IF = FunctionMetadataRegistry.FUNCTION_INDEX_IF; + /** 4 */ + public static final int SUM = FunctionMetadataRegistry.FUNCTION_INDEX_SUM; + /** 78 */ + public static final int OFFSET = 78; + /** 100 */ + public static final int CHOOSE = FunctionMetadataRegistry.FUNCTION_INDEX_CHOOSE; + /** 148 */ + public static final int INDIRECT = FunctionMetadataRegistry.FUNCTION_INDEX_INDIRECT; + /** 255 */ + public static final int EXTERNAL_FUNC = FunctionMetadataRegistry.FUNCTION_INDEX_EXTERNAL; + } + // convenient access to namespace + private static final FunctionID ID = null; + + /** + * Array elements corresponding to unimplemented functions are null + */ + protected static final Function[] functions = produceFunctions(); + + private static Function[] produceFunctions() { + Function[] retval = new Function[368]; + + retval[0] = new Count(); + retval[ID.IF] = new IfFunc(); + retval[2] = LogicalFunction.ISNA; + retval[3] = LogicalFunction.ISERROR; + retval[ID.SUM] = AggregateFunction.SUM; + retval[5] = AggregateFunction.AVERAGE; + retval[6] = AggregateFunction.MIN; + retval[7] = AggregateFunction.MAX; + retval[8] = new RowFunc(); // ROW + retval[9] = new Column(); + retval[10] = new Na(); + retval[11] = new Npv(); + retval[12] = AggregateFunction.STDEV; + retval[13] = NumericFunction.DOLLAR; + + retval[15] = NumericFunction.SIN; + retval[16] = NumericFunction.COS; + retval[17] = NumericFunction.TAN; + retval[18] = NumericFunction.ATAN; + retval[19] = NumericFunction.PI; + retval[20] = NumericFunction.SQRT; + retval[21] = NumericFunction.EXP; + retval[22] = NumericFunction.LN; + retval[23] = NumericFunction.LOG10; + retval[24] = NumericFunction.ABS; + retval[25] = NumericFunction.INT; + retval[26] = NumericFunction.SIGN; + retval[27] = NumericFunction.ROUND; + retval[28] = new Lookup(); + retval[29] = new Index(); + + retval[31] = TextFunction.MID; + retval[32] = TextFunction.LEN; + retval[33] = new Value(); + retval[34] = BooleanFunction.TRUE; + retval[35] = BooleanFunction.FALSE; + retval[36] = BooleanFunction.AND; + retval[37] = BooleanFunction.OR; + retval[38] = BooleanFunction.NOT; + retval[39] = NumericFunction.MOD; + retval[48] = TextFunction.TEXT; + + retval[56] = FinanceFunction.PV; + retval[57] = FinanceFunction.FV; + retval[58] = FinanceFunction.NPER; + retval[59] = FinanceFunction.PMT; + + retval[63] = NumericFunction.RAND; + retval[64] = new Match(); + retval[65] = DateFunc.instance; + retval[66] = new TimeFunc(); + retval[67] = CalendarFieldFunction.DAY; + retval[68] = CalendarFieldFunction.MONTH; + retval[69] = CalendarFieldFunction.YEAR; + + retval[74] = new Now(); + + retval[76] = new Rows(); + retval[77] = new Columns(); + retval[82] = TextFunction.SEARCH; + retval[ID.OFFSET] = new Offset(); + retval[82] = TextFunction.SEARCH; + + retval[97] = NumericFunction.ATAN2; + retval[98] = NumericFunction.ASIN; + retval[99] = NumericFunction.ACOS; + retval[ID.CHOOSE] = new Choose(); + retval[101] = new Hlookup(); + retval[102] = new Vlookup(); + + retval[105] = LogicalFunction.ISREF; + + retval[109] = NumericFunction.LOG; + + retval[112] = TextFunction.LOWER; + retval[113] = TextFunction.UPPER; + + retval[115] = TextFunction.LEFT; + retval[116] = TextFunction.RIGHT; + retval[117] = TextFunction.EXACT; + retval[118] = TextFunction.TRIM; + retval[119] = new Replace(); + retval[120] = new Substitute(); + + retval[124] = TextFunction.FIND; + + retval[127] = LogicalFunction.ISTEXT; + retval[128] = LogicalFunction.ISNUMBER; + retval[129] = LogicalFunction.ISBLANK; + retval[130] = new T(); + + retval[ID.INDIRECT] = null; // Indirect.evaluate has different signature + + retval[169] = new Counta(); + + retval[183] = AggregateFunction.PRODUCT; + retval[184] = NumericFunction.FACT; + + retval[190] = LogicalFunction.ISNONTEXT; + retval[197] = NumericFunction.TRUNC; + retval[198] = LogicalFunction.ISLOGICAL; + + retval[212] = NumericFunction.ROUNDUP; + retval[213] = NumericFunction.ROUNDDOWN; + + retval[220] = new Days360(); + retval[221] = new Today(); + + retval[227] = AggregateFunction.MEDIAN; + retval[228] = new Sumproduct(); + retval[229] = NumericFunction.SINH; + retval[230] = NumericFunction.COSH; + retval[231] = NumericFunction.TANH; + retval[232] = NumericFunction.ASINH; + retval[233] = NumericFunction.ACOSH; + retval[234] = NumericFunction.ATANH; + + retval[ID.EXTERNAL_FUNC] = null; // ExternalFunction is a FreeREfFunction + + retval[261] = new Errortype(); + + retval[269] = AggregateFunction.AVEDEV; + + retval[276] = NumericFunction.COMBIN; + + retval[279] = new Even(); + + retval[285] = NumericFunction.FLOOR; + + retval[288] = NumericFunction.CEILING; + + retval[298] = new Odd(); + + retval[300] = NumericFunction.POISSON; + + retval[303] = new Sumxmy2(); + retval[304] = new Sumx2my2(); + retval[305] = new Sumx2py2(); + + retval[318] = AggregateFunction.DEVSQ; + + retval[321] = AggregateFunction.SUMSQ; + + retval[325] = AggregateFunction.LARGE; + retval[326] = AggregateFunction.SMALL; + + retval[330] = new Mode(); + + retval[336] = TextFunction.CONCATENATE; + retval[337] = NumericFunction.POWER; + + retval[342] = NumericFunction.RADIANS; + retval[343] = NumericFunction.DEGREES; + + retval[344] = new Subtotal(); + retval[345] = new Sumif(); + retval[346] = new Countif(); + retval[347] = new Countblank(); + + retval[359] = new Hyperlink(); + + retval[362] = MinaMaxa.MAXA; + retval[363] = MinaMaxa.MINA; + + for (int i = 0; i < retval.length; i++) { + Function f = retval[i]; + if (f == null) { + FunctionMetadata fm = FunctionMetadataRegistry.getFunctionByIndex(i); + if (fm == null) { + continue; + } + retval[i] = new NotImplementedFunction(fm.getName()); + } + } + return retval; + } + /** + * @return null if the specified functionIndex is for INDIRECT() or any external (add-in) function. + */ + public static Function getBasicFunction(int functionIndex) { + // check for 'free ref' functions first + switch (functionIndex) { + case FunctionID.INDIRECT: + case FunctionID.EXTERNAL_FUNC: + return null; + } + // else - must be plain function + Function result = functions[functionIndex]; + if (result == null) { + throw new NotImplementedException("FuncIx=" + functionIndex); + } + return result; + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/IntersectionEval.java b/src/java/org/apache/poi/ss/formula/eval/IntersectionEval.java new file mode 100644 index 0000000000..4063790c73 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/IntersectionEval.java @@ -0,0 +1,96 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + +/** + * @author Josh Micich + */ +public final class IntersectionEval extends Fixed2ArgFunction { + + public static final Function instance = new IntersectionEval(); + + private IntersectionEval() { + // enforces singleton + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + + try { + AreaEval reA = evaluateRef(arg0); + AreaEval reB = evaluateRef(arg1); + AreaEval result = resolveRange(reA, reB); + if (result == null) { + return ErrorEval.NULL_INTERSECTION; + } + return result; + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + /** + * @return simple rectangular {@link AreaEval} which represents the intersection of areas + * aeA and aeB. If the two areas do not intersect, the result is null. + */ + private static AreaEval resolveRange(AreaEval aeA, AreaEval aeB) { + + int aeAfr = aeA.getFirstRow(); + int aeAfc = aeA.getFirstColumn(); + int aeBlc = aeB.getLastColumn(); + if (aeAfc > aeBlc) { + return null; + } + int aeBfc = aeB.getFirstColumn(); + if (aeBfc > aeA.getLastColumn()) { + return null; + } + int aeBlr = aeB.getLastRow(); + if (aeAfr > aeBlr) { + return null; + } + int aeBfr = aeB.getFirstRow(); + int aeAlr = aeA.getLastRow(); + if (aeBfr > aeAlr) { + return null; + } + + + int top = Math.max(aeAfr, aeBfr); + int bottom = Math.min(aeAlr, aeBlr); + int left = Math.max(aeAfc, aeBfc); + int right = Math.min(aeA.getLastColumn(), aeBlc); + + return aeA.offset(top-aeAfr, bottom-aeAfr, left-aeAfc, right-aeAfc); + } + + private static AreaEval evaluateRef(ValueEval arg) throws EvaluationException { + if (arg instanceof AreaEval) { + return (AreaEval) arg; + } + if (arg instanceof RefEval) { + return ((RefEval) arg).offset(0, 0, 0, 0); + } + if (arg instanceof ErrorEval) { + throw new EvaluationException((ErrorEval)arg); + } + throw new IllegalArgumentException("Unexpected ref arg class (" + arg.getClass().getName() + ")"); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/MissingArgEval.java b/src/java/org/apache/poi/ss/formula/eval/MissingArgEval.java new file mode 100644 index 0000000000..70d25040f4 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/MissingArgEval.java @@ -0,0 +1,36 @@ +/* ==================================================================== + 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.eval; + +/** + * Represents the (intermediate) evaluated result of a missing function argument. In most cases + * this can be translated into {@link BlankEval} but there are some notable exceptions. Functions + * COUNT and COUNTA do count their missing args. Note - the differences between + * {@link MissingArgEval} and {@link BlankEval} have not been investigated fully, so the POI + * evaluator may need to be updated to account for these as they are found. + * + * @author Josh Micich + */ +public final class MissingArgEval implements ValueEval { + + public static final MissingArgEval instance = new MissingArgEval(); + + private MissingArgEval() { + // enforce singleton + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/NameEval.java b/src/java/org/apache/poi/ss/formula/eval/NameEval.java new file mode 100644 index 0000000000..decefb9366 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/NameEval.java @@ -0,0 +1,46 @@ +/* ==================================================================== + 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.eval; + +/** + * @author Josh Micich + */ +public final class NameEval implements ValueEval { + + private final String _functionName; + + /** + * Creates a NameEval representing a function name + */ + public NameEval(String functionName) { + _functionName = functionName; + } + + + public String getFunctionName() { + return _functionName; + } + + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(_functionName); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/NameXEval.java b/src/java/org/apache/poi/ss/formula/eval/NameXEval.java new file mode 100644 index 0000000000..951d4619be --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/NameXEval.java @@ -0,0 +1,44 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.hssf.record.formula.NameXPtg; + +/** + * @author Josh Micich + */ +public final class NameXEval implements ValueEval { + + private final NameXPtg _ptg; + + public NameXEval(NameXPtg ptg) { + _ptg = ptg; + } + + public NameXPtg getPtg() { + return _ptg; + } + + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(_ptg.getSheetRefIndex()).append(", ").append(_ptg.getNameIndex()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/NumberEval.java b/src/java/org/apache/poi/ss/formula/eval/NumberEval.java new file mode 100644 index 0000000000..e500f707ce --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/NumberEval.java @@ -0,0 +1,73 @@ +/* +* 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. +*/ +/* + * Created on May 8, 2005 + * + */ +package org.apache.poi.ss.formula.eval; + +import org.apache.poi.hssf.record.formula.IntPtg; +import org.apache.poi.hssf.record.formula.NumberPtg; +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.ss.util.NumberToTextConverter; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * + */ +public final class NumberEval implements NumericValueEval, StringValueEval { + + public static final NumberEval ZERO = new NumberEval(0); + + private final double _value; + private String _stringValue; + + public NumberEval(Ptg ptg) { + if (ptg == null) { + throw new IllegalArgumentException("ptg must not be null"); + } + if (ptg instanceof IntPtg) { + _value = ((IntPtg) ptg).getValue(); + } else if (ptg instanceof NumberPtg) { + _value = ((NumberPtg) ptg).getValue(); + } else { + throw new IllegalArgumentException("bad argument type (" + ptg.getClass().getName() + ")"); + } + } + + public NumberEval(double value) { + _value = value; + } + + public double getNumberValue() { + return _value; + } + + public String getStringValue() { + if (_stringValue == null) { + _stringValue = NumberToTextConverter.toText(_value); + } + return _stringValue; + } + public final String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(getStringValue()); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/NumericValueEval.java b/src/java/org/apache/poi/ss/formula/eval/NumericValueEval.java new file mode 100644 index 0000000000..056f21cdf7 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/NumericValueEval.java @@ -0,0 +1,30 @@ +/* +* 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. +*/ +/* + * Created on May 8, 2005 + * + */ +package org.apache.poi.ss.formula.eval; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * + */ +public interface NumericValueEval extends ValueEval { + + public abstract double getNumberValue(); +} diff --git a/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java b/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java new file mode 100644 index 0000000000..eea1a3828c --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/OperandResolver.java @@ -0,0 +1,324 @@ +/* ==================================================================== + 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.eval; + +import java.util.regex.Pattern; + +/** + * Provides functionality for evaluating arguments to functions and operators. + * + * @author Josh Micich + * @author Brendan Nolan + */ +public final class OperandResolver { + + // Based on regular expression defined in JavaDoc at {@link java.lang.Double#valueOf} + // modified to remove support for NaN, Infinity, Hexadecimal support and floating type suffixes + private static final String Digits = "(\\p{Digit}+)"; + private static final String Exp = "[eE][+-]?"+Digits; + private static final String fpRegex = + ("[\\x00-\\x20]*" + + "[+-]?(" + + "((("+Digits+"(\\.)?("+Digits+"?)("+Exp+")?)|"+ + "(\\.("+Digits+")("+Exp+")?))))"+ + "[\\x00-\\x20]*"); + + + private OperandResolver() { + // no instances of this class + } + + /** + * Retrieves a single value from a variety of different argument types according to standard + * Excel rules. Does not perform any type conversion. + * @param arg the evaluated argument as passed to the function or operator. + * @param srcCellRow used when arg is a single column AreaRef + * @param srcCellCol used when arg is a single row AreaRef + * @return a NumberEval, StringEval, BoolEval or BlankEval. + * Never null or ErrorEval. + * @throws EvaluationException(#VALUE!) if srcCellRow or srcCellCol do not properly index into + * an AreaEval. If the actual value retrieved is an ErrorEval, a corresponding + * EvaluationException is thrown. + */ + public static ValueEval getSingleValue(ValueEval arg, int srcCellRow, int srcCellCol) + throws EvaluationException { + ValueEval result; + if (arg instanceof RefEval) { + result = ((RefEval) arg).getInnerValueEval(); + } else if (arg instanceof AreaEval) { + result = chooseSingleElementFromArea((AreaEval) arg, srcCellRow, srcCellCol); + } else { + result = arg; + } + if (result instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) result); + } + return result; + } + + /** + * Implements (some perhaps not well known) Excel functionality to select a single cell from an + * area depending on the coordinates of the calling cell. Here is an example demonstrating + * both selection from a single row area and a single column area in the same formula. + * + * + * + * + * + * + * + *
  A  B  C  D 
1152025 
2   200
3   300
3   400
+ * + * If the formula "=1000+A1:B1+D2:D3" is put into the 9 cells from A2 to C4, the spreadsheet + * will look like this: + * + * + * + * + * + * + * + *
  A  B  C  D 
1152025 
212151220#VALUE!200
313151320#VALUE!300
4#VALUE!#VALUE!#VALUE!400
+ * + * Note that the row area (A1:B1) does not include column C and the column area (D2:D3) does + * not include row 4, so the values in C1(=25) and D4(=400) are not accessible to the formula + * as written, but in the 4 cells A2:B3, the row and column selection works ok.

+ * + * The same concept is extended to references across sheets, such that even multi-row, + * multi-column areas can be useful.

+ * + * Of course with carefully (or carelessly) chosen parameters, cyclic references can occur and + * hence this method can throw a 'circular reference' EvaluationException. Note that + * this method does not attempt to detect cycles. Every cell in the specified Area ae + * has already been evaluated prior to this method call. Any cell (or cells) part of + * ae that would incur a cyclic reference error if selected by this method, will + * already have the value ErrorEval.CIRCULAR_REF_ERROR upon entry to this method. It + * is assumed logic exists elsewhere to produce this behaviour. + * + * @return whatever the selected cell's evaluated value is. Never null. Never + * ErrorEval. + * @throws EvaluationException if there is a problem with indexing into the area, or if the + * evaluated cell has an error. + */ + public static ValueEval chooseSingleElementFromArea(AreaEval ae, + int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval result = chooseSingleElementFromAreaInternal(ae, srcCellRow, srcCellCol); + if (result instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) result); + } + return result; + } + + /** + * @return possibly ErrorEval, and null + */ + private static ValueEval chooseSingleElementFromAreaInternal(AreaEval ae, + int srcCellRow, int srcCellCol) throws EvaluationException { + + if(false) { + // this is too simplistic + if(ae.containsRow(srcCellRow) && ae.containsColumn(srcCellCol)) { + throw new EvaluationException(ErrorEval.CIRCULAR_REF_ERROR); + } + /* + Circular references are not dealt with directly here, but it is worth noting some issues. + + ANY one of the return statements in this method could return a cell that is identical + to the one immediately being evaluated. The evaluating cell is identified by srcCellRow, + srcCellRow AND sheet. The sheet is not available in any nearby calling method, so that's + one reason why circular references are not easy to detect here. (The sheet of the returned + cell can be obtained from ae if it is an Area3DEval.) + + Another reason there's little value in attempting to detect circular references here is + that only direct circular references could be detected. If the cycle involved two or more + cells this method could not detect it. + + Logic to detect evaluation cycles of all kinds has been coded in EvaluationCycleDetector + (and FormulaEvaluator). + */ + } + + if (ae.isColumn()) { + if(ae.isRow()) { + return ae.getRelativeValue(0, 0); + } + if(!ae.containsRow(srcCellRow)) { + throw EvaluationException.invalidValue(); + } + return ae.getAbsoluteValue(srcCellRow, ae.getFirstColumn()); + } + if(!ae.isRow()) { + // multi-column, multi-row area + if(ae.containsRow(srcCellRow) && ae.containsColumn(srcCellCol)) { + return ae.getAbsoluteValue(ae.getFirstRow(), ae.getFirstColumn()); + } + throw EvaluationException.invalidValue(); + } + if(!ae.containsColumn(srcCellCol)) { + throw EvaluationException.invalidValue(); + } + return ae.getAbsoluteValue(ae.getFirstRow(), srcCellCol); + } + + /** + * Applies some conversion rules if the supplied value is not already an integer.
+ * Value is first coerced to a double ( See coerceValueToDouble() ). + * Note - BlankEval is converted to 0.

+ * + * Excel typically converts doubles to integers by truncating toward negative infinity.
+ * The equivalent java code is:
+ *   return (int)Math.floor(d);
+ * not:
+ *   return (int)d; // wrong - rounds toward zero + * + */ + public static int coerceValueToInt(ValueEval ev) throws EvaluationException { + if (ev == BlankEval.instance) { + return 0; + } + double d = coerceValueToDouble(ev); + // Note - the standard java type conversion from double to int truncates toward zero. + // but Math.floor() truncates toward negative infinity + return (int)Math.floor(d); + } + + /** + * Applies some conversion rules if the supplied value is not already a number. + * Note - BlankEval is converted to {@link NumberEval#ZERO}. + * @param ev must be a {@link NumberEval}, {@link StringEval}, {@link BoolEval} or + * {@link BlankEval} + * @return actual, parsed or interpreted double value (respectively). + * @throws EvaluationException(#VALUE!) only if a StringEval is supplied and cannot be parsed + * as a double (See parseDouble() for allowable formats). + * @throws RuntimeException if the supplied parameter is not {@link NumberEval}, + * {@link StringEval}, {@link BoolEval} or {@link BlankEval} + */ + public static double coerceValueToDouble(ValueEval ev) throws EvaluationException { + + if (ev == BlankEval.instance) { + return 0.0; + } + if (ev instanceof NumericValueEval) { + // this also handles booleans + return ((NumericValueEval)ev).getNumberValue(); + } + if (ev instanceof StringEval) { + Double dd = parseDouble(((StringEval) ev).getStringValue()); + if (dd == null) { + throw EvaluationException.invalidValue(); + } + return dd.doubleValue(); + } + throw new RuntimeException("Unexpected arg eval type (" + ev.getClass().getName() + ")"); + } + + /** + * Converts a string to a double using standard rules that Excel would use.
+ * Tolerates leading and trailing spaces,

+ * + * Doesn't support currency prefixes, commas, percentage signs or arithmetic operations strings. + * + * Some examples:
+ * " 123 " -> 123.0
+ * ".123" -> 0.123
+ * "1E4" -> 1000
+ * "-123" -> -123.0
+ * These not supported yet:
+ * " $ 1,000.00 " -> 1000.0
+ * "$1.25E4" -> 12500.0
+ * "5**2" -> 500
+ * "250%" -> 2.5
+ * + * @return null if the specified text cannot be parsed as a number + */ + public static Double parseDouble(String pText) { + + if (Pattern.matches(fpRegex, pText)) + try { + return Double.parseDouble(pText); + } catch (NumberFormatException e) { + return null; + } + else { + return null; + } + + } + + /** + * @param ve must be a NumberEval, StringEval, BoolEval, or BlankEval + * @return the converted string value. never null + */ + public static String coerceValueToString(ValueEval ve) { + if (ve instanceof StringValueEval) { + StringValueEval sve = (StringValueEval) ve; + return sve.getStringValue(); + } + if (ve == BlankEval.instance) { + return ""; + } + throw new IllegalArgumentException("Unexpected eval class (" + ve.getClass().getName() + ")"); + } + + /** + * @return null to represent blank values + * @throws EvaluationException if ve is an ErrorEval, or if a string value cannot be converted + */ + public static Boolean coerceValueToBoolean(ValueEval ve, boolean stringsAreBlanks) throws EvaluationException { + + if (ve == null || ve == BlankEval.instance) { + // TODO - remove 've == null' condition once AreaEval is fixed + return null; + } + if (ve instanceof BoolEval) { + return Boolean.valueOf(((BoolEval) ve).getBooleanValue()); + } + + if (ve == BlankEval.instance) { + return null; + } + + if (ve instanceof StringEval) { + if (stringsAreBlanks) { + return null; + } + String str = ((StringEval) ve).getStringValue(); + if (str.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (str.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + // else - string cannot be converted to boolean + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + if (ve instanceof NumericValueEval) { + NumericValueEval ne = (NumericValueEval) ve; + double d = ne.getNumberValue(); + if (Double.isNaN(d)) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + return Boolean.valueOf(d != 0); + } + if (ve instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) ve); + } + throw new RuntimeException("Unexpected eval (" + ve.getClass().getName() + ")"); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/PercentEval.java b/src/java/org/apache/poi/ss/formula/eval/PercentEval.java new file mode 100644 index 0000000000..263574a406 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/PercentEval.java @@ -0,0 +1,49 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.eval; + +import org.apache.poi.ss.formula.functions.Fixed1ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + + +/** + * Implementation of Excel formula token '%'.

+ * @author Josh Micich + */ +public final class PercentEval extends Fixed1ArgFunction { + + public static final Function instance = new PercentEval(); + + private PercentEval() { + // enforce singleton + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) { + double d; + try { + ValueEval ve = OperandResolver.getSingleValue(arg0, srcRowIndex, srcColumnIndex); + d = OperandResolver.coerceValueToDouble(ve); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + if (d == 0.0) { // this '==' matches +0.0 and -0.0 + return NumberEval.ZERO; + } + return new NumberEval(d / 100); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/RangeEval.java b/src/java/org/apache/poi/ss/formula/eval/RangeEval.java new file mode 100644 index 0000000000..617d20a765 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/RangeEval.java @@ -0,0 +1,75 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + + +/** + * + * @author Josh Micich + */ +public final class RangeEval extends Fixed2ArgFunction { + + public static final Function instance = new RangeEval(); + + private RangeEval() { + // enforces singleton + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + + try { + AreaEval reA = evaluateRef(arg0); + AreaEval reB = evaluateRef(arg1); + return resolveRange(reA, reB); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + /** + * @return simple rectangular {@link AreaEval} which fully encloses both areas + * aeA and aeB + */ + private static AreaEval resolveRange(AreaEval aeA, AreaEval aeB) { + int aeAfr = aeA.getFirstRow(); + int aeAfc = aeA.getFirstColumn(); + + int top = Math.min(aeAfr, aeB.getFirstRow()); + int bottom = Math.max(aeA.getLastRow(), aeB.getLastRow()); + int left = Math.min(aeAfc, aeB.getFirstColumn()); + int right = Math.max(aeA.getLastColumn(), aeB.getLastColumn()); + + return aeA.offset(top-aeAfr, bottom-aeAfr, left-aeAfc, right-aeAfc); + } + + private static AreaEval evaluateRef(ValueEval arg) throws EvaluationException { + if (arg instanceof AreaEval) { + return (AreaEval) arg; + } + if (arg instanceof RefEval) { + return ((RefEval) arg).offset(0, 0, 0, 0); + } + if (arg instanceof ErrorEval) { + throw new EvaluationException((ErrorEval)arg); + } + throw new IllegalArgumentException("Unexpected ref arg class (" + arg.getClass().getName() + ")"); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/RefEval.java b/src/java/org/apache/poi/ss/formula/eval/RefEval.java new file mode 100644 index 0000000000..768e90a8a5 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/RefEval.java @@ -0,0 +1,51 @@ +/* ==================================================================== + 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.eval; + +/** + * @author Amol S Deshmukh < amolweb at ya hoo dot com > + * + * RefEval is the super interface for Ref2D and Ref3DEval. Basically a RefEval + * impl should contain reference to the original ReferencePtg or Ref3DPtg as + * well as the final "value" resulting from the evaluation of the cell + * reference. Thus if the Cell has type CELL_TYPE_NUMERIC, the contained + * value object should be of type NumberEval; if cell type is CELL_TYPE_STRING, + * contained value object should be of type StringEval + */ +public interface RefEval extends ValueEval { + + /** + * @return the evaluated value of the cell referred to by this RefEval. + */ + ValueEval getInnerValueEval(); + + /** + * returns the zero based column index. + */ + int getColumn(); + + /** + * returns the zero based row index. + */ + int getRow(); + + /** + * Creates an {@link AreaEval} offset by a relative amount from this RefEval + */ + AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx); +} diff --git a/src/java/org/apache/poi/ss/formula/eval/RefEvalBase.java b/src/java/org/apache/poi/ss/formula/eval/RefEvalBase.java new file mode 100644 index 0000000000..83d20fb49a --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/RefEvalBase.java @@ -0,0 +1,40 @@ +/* ==================================================================== + 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.eval; + +/** + * Common base class for implementors of {@link RefEval} + * + * @author Josh Micich + */ +public abstract class RefEvalBase implements RefEval { + + private final int _rowIndex; + private final int _columnIndex; + + protected RefEvalBase(int rowIndex, int columnIndex) { + _rowIndex = rowIndex; + _columnIndex = columnIndex; + } + public final int getRow() { + return _rowIndex; + } + public final int getColumn() { + return _columnIndex; + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java b/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java new file mode 100644 index 0000000000..8b3be19199 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/RelationalOperationEval.java @@ -0,0 +1,168 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; +import org.apache.poi.ss.formula.functions.Function; +import org.apache.poi.ss.util.NumberComparer; + +/** + * Base class for all comparison operator evaluators + * + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public abstract class RelationalOperationEval extends Fixed2ArgFunction { + + /** + * Converts a standard compare result (-1, 0, 1) to true or false + * according to subclass' comparison type. + */ + protected abstract boolean convertComparisonResult(int cmpResult); + + /** + * This is a description of how the relational operators apply in MS Excel. + * Use this as a guideline when testing/implementing the evaluate methods + * for the relational operators Evals. + * + *

+	 * Bool.TRUE > any number.
+	 * Bool > any string. ALWAYS
+	 * Bool.TRUE > Bool.FALSE
+	 * Bool.FALSE == Blank
+	 *
+	 * Strings are never converted to numbers or booleans
+	 * String > any number. ALWAYS
+	 * Non-empty String > Blank
+	 * Empty String == Blank
+	 * String are sorted dictionary wise
+	 *
+	 * Blank > Negative numbers
+	 * Blank == 0
+	 * Blank < Positive numbers
+	 * 
+ */ + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + + ValueEval vA; + ValueEval vB; + try { + vA = OperandResolver.getSingleValue(arg0, srcRowIndex, srcColumnIndex); + vB = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + int cmpResult = doCompare(vA, vB); + boolean result = convertComparisonResult(cmpResult); + return BoolEval.valueOf(result); + } + + private static int doCompare(ValueEval va, ValueEval vb) { + // special cases when one operand is blank + if (va == BlankEval.instance) { + return compareBlank(vb); + } + if (vb == BlankEval.instance) { + return -compareBlank(va); + } + + if (va instanceof BoolEval) { + if (vb instanceof BoolEval) { + BoolEval bA = (BoolEval) va; + BoolEval bB = (BoolEval) vb; + if (bA.getBooleanValue() == bB.getBooleanValue()) { + return 0; + } + return bA.getBooleanValue() ? 1 : -1; + } + return 1; + } + if (vb instanceof BoolEval) { + return -1; + } + if (va instanceof StringEval) { + if (vb instanceof StringEval) { + StringEval sA = (StringEval) va; + StringEval sB = (StringEval) vb; + return sA.getStringValue().compareToIgnoreCase(sB.getStringValue()); + } + return 1; + } + if (vb instanceof StringEval) { + return -1; + } + if (va instanceof NumberEval) { + if (vb instanceof NumberEval) { + NumberEval nA = (NumberEval) va; + NumberEval nB = (NumberEval) vb; + return NumberComparer.compare(nA.getNumberValue(), nB.getNumberValue()); + } + } + throw new IllegalArgumentException("Bad operand types (" + va.getClass().getName() + "), (" + + vb.getClass().getName() + ")"); + } + + private static int compareBlank(ValueEval v) { + if (v == BlankEval.instance) { + return 0; + } + if (v instanceof BoolEval) { + BoolEval boolEval = (BoolEval) v; + return boolEval.getBooleanValue() ? -1 : 0; + } + if (v instanceof NumberEval) { + NumberEval ne = (NumberEval) v; + return NumberComparer.compare(0.0, ne.getNumberValue()); + } + if (v instanceof StringEval) { + StringEval se = (StringEval) v; + return se.getStringValue().length() < 1 ? 0 : -1; + } + throw new IllegalArgumentException("bad value class (" + v.getClass().getName() + ")"); + } + + public static final Function EqualEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult == 0; + } + }; + public static final Function GreaterEqualEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult >= 0; + } + }; + public static final Function GreaterThanEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult > 0; + } + }; + public static final Function LessEqualEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult <= 0; + } + }; + public static final Function LessThanEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult < 0; + } + }; + public static final Function NotEqualEval = new RelationalOperationEval() { + protected boolean convertComparisonResult(int cmpResult) { + return cmpResult != 0; + } + }; +} diff --git a/src/java/org/apache/poi/ss/formula/eval/StringEval.java b/src/java/org/apache/poi/ss/formula/eval/StringEval.java new file mode 100644 index 0000000000..b2596fa105 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/StringEval.java @@ -0,0 +1,54 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.StringPtg; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class StringEval implements StringValueEval { + + public static final StringEval EMPTY_INSTANCE = new StringEval(""); + + private final String _value; + + public StringEval(Ptg ptg) { + this(((StringPtg) ptg).getValue()); + } + + public StringEval(String value) { + if (value == null) { + throw new IllegalArgumentException("value must not be null"); + } + _value = value; + } + + public String getStringValue() { + return _value; + } + + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append(getClass().getName()).append(" ["); + sb.append(_value); + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/StringValueEval.java b/src/java/org/apache/poi/ss/formula/eval/StringValueEval.java new file mode 100644 index 0000000000..4e1b712fbc --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/StringValueEval.java @@ -0,0 +1,30 @@ +/* +* 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.eval; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * + */ +public interface StringValueEval extends ValueEval { + + /** + * @return never null, possibly empty string. + */ + String getStringValue(); +} diff --git a/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java b/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java new file mode 100644 index 0000000000..a4c05d96f0 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/TwoOperandNumericOperation.java @@ -0,0 +1,87 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed2ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + +/** + * @author Josh Micich + */ +public abstract class TwoOperandNumericOperation extends Fixed2ArgFunction { + + protected final double singleOperandEvaluate(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + ValueEval ve = OperandResolver.getSingleValue(arg, srcCellRow, srcCellCol); + return OperandResolver.coerceValueToDouble(ve); + } + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { + double result; + try { + double d0 = singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); + double d1 = singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); + result = evaluate(d0, d1); + if (result == 0.0) { // this '==' matches +0.0 and -0.0 + // Excel converts -0.0 to +0.0 for '*', '/', '%', '+' and '^' + if (!(this instanceof SubtractEvalClass)) { + return NumberEval.ZERO; + } + } + if (Double.isNaN(result) || Double.isInfinite(result)) { + return ErrorEval.NUM_ERROR; + } + } catch (EvaluationException e) { + return e.getErrorEval(); + } + return new NumberEval(result); + } + + protected abstract double evaluate(double d0, double d1) throws EvaluationException; + + public static final Function AddEval = new TwoOperandNumericOperation() { + protected double evaluate(double d0, double d1) { + return d0+d1; + } + }; + public static final Function DivideEval = new TwoOperandNumericOperation() { + protected double evaluate(double d0, double d1) throws EvaluationException { + if (d1 == 0.0) { + throw new EvaluationException(ErrorEval.DIV_ZERO); + } + return d0/d1; + } + }; + public static final Function MultiplyEval = new TwoOperandNumericOperation() { + protected double evaluate(double d0, double d1) { + return d0*d1; + } + }; + public static final Function PowerEval = new TwoOperandNumericOperation() { + protected double evaluate(double d0, double d1) { + return Math.pow(d0, d1); + } + }; + private static final class SubtractEvalClass extends TwoOperandNumericOperation { + public SubtractEvalClass() { + // + } + protected double evaluate(double d0, double d1) { + return d0-d1; + } + } + public static final Function SubtractEval = new SubtractEvalClass(); +} diff --git a/src/java/org/apache/poi/ss/formula/eval/UnaryMinusEval.java b/src/java/org/apache/poi/ss/formula/eval/UnaryMinusEval.java new file mode 100644 index 0000000000..02d6d5e12c --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/UnaryMinusEval.java @@ -0,0 +1,47 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed1ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class UnaryMinusEval extends Fixed1ArgFunction { + + public static final Function instance = new UnaryMinusEval(); + + private UnaryMinusEval() { + // enforce singleton + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0) { + double d; + try { + ValueEval ve = OperandResolver.getSingleValue(arg0, srcRowIndex, srcColumnIndex); + d = OperandResolver.coerceValueToDouble(ve); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + if (d == 0.0) { // this '==' matches +0.0 and -0.0 + return NumberEval.ZERO; + } + return new NumberEval(-d); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/UnaryPlusEval.java b/src/java/org/apache/poi/ss/formula/eval/UnaryPlusEval.java new file mode 100644 index 0000000000..9b10f2b10c --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/UnaryPlusEval.java @@ -0,0 +1,51 @@ +/* ==================================================================== + 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.eval; + +import org.apache.poi.ss.formula.functions.Fixed1ArgFunction; +import org.apache.poi.ss.formula.functions.Function; + + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public final class UnaryPlusEval extends Fixed1ArgFunction { + + public static final Function instance = new UnaryPlusEval(); + + private UnaryPlusEval() { + // enforce singleton + } + + public ValueEval evaluate(int srcCellRow, int srcCellCol, ValueEval arg0) { + double d; + try { + ValueEval ve = OperandResolver.getSingleValue(arg0, srcCellRow, srcCellCol); + if(ve instanceof StringEval) { + // Note - asymmetric with UnaryMinus + // -"hello" evaluates to #VALUE! + // but +"hello" evaluates to "hello" + return ve; + } + d = OperandResolver.coerceValueToDouble(ve); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + return new NumberEval(+d); + } +} diff --git a/src/java/org/apache/poi/ss/formula/eval/ValueEval.java b/src/java/org/apache/poi/ss/formula/eval/ValueEval.java new file mode 100644 index 0000000000..c1309a7df6 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/eval/ValueEval.java @@ -0,0 +1,25 @@ +/* ==================================================================== + 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.eval; + +/** + * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + */ +public interface ValueEval { + // no methods +} diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java index 8dd298193c..8e86902570 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationCell.java @@ -17,13 +17,12 @@ package org.apache.poi.ss.formula.eval.forked; -import org.apache.poi.hssf.record.formula.eval.BlankEval; -import org.apache.poi.hssf.record.formula.eval.BoolEval; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; -import org.apache.poi.hssf.record.formula.eval.StringEval; -import org.apache.poi.hssf.record.formula.eval.ValueEval; -import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.ss.formula.eval.BlankEval; +import org.apache.poi.ss.formula.eval.BoolEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationSheet; import org.apache.poi.ss.usermodel.Cell; @@ -60,27 +59,27 @@ final class ForkedEvaluationCell implements EvaluationCell { Class cls = value.getClass(); if (cls == NumberEval.class) { - _cellType = HSSFCell.CELL_TYPE_NUMERIC; + _cellType = Cell.CELL_TYPE_NUMERIC; _numberValue = ((NumberEval)value).getNumberValue(); return; } if (cls == StringEval.class) { - _cellType = HSSFCell.CELL_TYPE_STRING; + _cellType = Cell.CELL_TYPE_STRING; _stringValue = ((StringEval)value).getStringValue(); return; } if (cls == BoolEval.class) { - _cellType = HSSFCell.CELL_TYPE_BOOLEAN; + _cellType = Cell.CELL_TYPE_BOOLEAN; _booleanValue = ((BoolEval)value).getBooleanValue(); return; } if (cls == ErrorEval.class) { - _cellType = HSSFCell.CELL_TYPE_ERROR; + _cellType = Cell.CELL_TYPE_ERROR; _errorValue = ((ErrorEval)value).getErrorCode(); return; } if (cls == BlankEval.class) { - _cellType = HSSFCell.CELL_TYPE_BLANK; + _cellType = Cell.CELL_TYPE_BLANK; return; } throw new IllegalArgumentException("Unexpected value class (" + cls.getName() + ")"); @@ -105,19 +104,19 @@ final class ForkedEvaluationCell implements EvaluationCell { return _cellType; } public boolean getBooleanCellValue() { - checkCellType(HSSFCell.CELL_TYPE_BOOLEAN); + checkCellType(Cell.CELL_TYPE_BOOLEAN); return _booleanValue; } public int getErrorCellValue() { - checkCellType(HSSFCell.CELL_TYPE_ERROR); + checkCellType(Cell.CELL_TYPE_ERROR); return _errorValue; } public double getNumericCellValue() { - checkCellType(HSSFCell.CELL_TYPE_NUMERIC); + checkCellType(Cell.CELL_TYPE_NUMERIC); return _numberValue; } public String getStringCellValue() { - checkCellType(HSSFCell.CELL_TYPE_STRING); + checkCellType(Cell.CELL_TYPE_STRING); return _stringValue; } public EvaluationSheet getSheet() { diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java index c916006764..8742b92143 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java @@ -23,7 +23,6 @@ import java.util.Map; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.record.formula.Ptg; -import org.apache.poi.hssf.record.formula.functions.FreeRefFunction; import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationName; import org.apache.poi.ss.formula.EvaluationSheet; diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluator.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluator.java index 258a5166fc..21fa576384 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluator.java @@ -17,13 +17,12 @@ package org.apache.poi.ss.formula.eval.forked; -import org.apache.poi.hssf.record.formula.eval.BoolEval; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.hssf.record.formula.eval.NumberEval; -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.udf.UDFFinder; -import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.ss.formula.eval.BoolEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.udf.UDFFinder; import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment; @@ -31,6 +30,7 @@ import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationWorkbook; import org.apache.poi.ss.formula.IStabilityClassifier; import org.apache.poi.ss.formula.WorkbookEvaluator; +import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Workbook; /** @@ -113,17 +113,17 @@ public final class ForkedEvaluator { EvaluationCell cell = _sewb.getEvaluationCell(sheetName, rowIndex, columnIndex); switch (cell.getCellType()) { - case HSSFCell.CELL_TYPE_BOOLEAN: + case Cell.CELL_TYPE_BOOLEAN: return BoolEval.valueOf(cell.getBooleanCellValue()); - case HSSFCell.CELL_TYPE_ERROR: + case Cell.CELL_TYPE_ERROR: return ErrorEval.valueOf(cell.getErrorCellValue()); - case HSSFCell.CELL_TYPE_FORMULA: + case Cell.CELL_TYPE_FORMULA: return _evaluator.evaluate(cell); - case HSSFCell.CELL_TYPE_NUMERIC: + case Cell.CELL_TYPE_NUMERIC: return new NumberEval(cell.getNumericCellValue()); - case HSSFCell.CELL_TYPE_STRING: + case Cell.CELL_TYPE_STRING: return new StringEval(cell.getStringCellValue()); - case HSSFCell.CELL_TYPE_BLANK: + case Cell.CELL_TYPE_BLANK: return null; } throw new IllegalStateException("Bad cell type (" + cell.getCellType() + ")"); diff --git a/src/java/org/apache/poi/ss/usermodel/CellValue.java b/src/java/org/apache/poi/ss/usermodel/CellValue.java index 664f100562..cfe69ac7f2 100644 --- a/src/java/org/apache/poi/ss/usermodel/CellValue.java +++ b/src/java/org/apache/poi/ss/usermodel/CellValue.java @@ -17,8 +17,7 @@ package org.apache.poi.ss.usermodel; -import org.apache.poi.hssf.record.formula.eval.ErrorEval; -import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.formula.eval.ErrorEval; /** * Mimics the 'data view' of a cell. This allows formula evaluator diff --git a/test-data/spreadsheet/LookupFunctionsTestCaseData.xls b/test-data/spreadsheet/LookupFunctionsTestCaseData.xls index 94f16e9840..ea7fad9f3d 100644 Binary files a/test-data/spreadsheet/LookupFunctionsTestCaseData.xls and b/test-data/spreadsheet/LookupFunctionsTestCaseData.xls differ