diff --git a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java index ad9604d9c..66ca17ddd 100644 --- a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java +++ b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -28,6 +28,7 @@ import org.apache.poi.ss.formula.functions.Dec2Bin; import org.apache.poi.ss.formula.functions.Dec2Hex; import org.apache.poi.ss.formula.functions.Delta; import org.apache.poi.ss.formula.functions.EDate; +import org.apache.poi.ss.formula.functions.EOMonth; import org.apache.poi.ss.formula.functions.FactDouble; import org.apache.poi.ss.formula.functions.FreeRefFunction; import org.apache.poi.ss.formula.functions.Hex2Dec; @@ -117,7 +118,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "DURATION", null); r(m, "EDATE", EDate.instance); r(m, "EFFECT", null); - r(m, "EOMONTH", null); + r(m, "EOMONTH", EOMonth.instance); r(m, "ERF", null); r(m, "ERFC", null); r(m, "FACTDOUBLE", FactDouble.instance); diff --git a/src/java/org/apache/poi/ss/formula/functions/EOMonth.java b/src/java/org/apache/poi/ss/formula/functions/EOMonth.java new file mode 100644 index 000000000..8832e20e7 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/EOMonth.java @@ -0,0 +1,81 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.apache.poi.ss.formula.OperationEvaluationContext; +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.ValueEval; +import org.apache.poi.ss.usermodel.DateUtil; + +/** + * Implementation for the Excel EOMONTH() function.

+ *

+ * EOMONTH() returns the date of the last day of a month..

+ *

+ * Syntax:
+ * EOMONTH(start_date,months)

+ *

+ * start_date is the starting date of the calculation + * months is the number of months to be added to start_date, + * to give a new date. For this new date, EOMONTH returns the date of + * the last day of the month. months may be positive (in the future), + * zero or negative (in the past). + */ +public class EOMonth implements FreeRefFunction { + + public static final FreeRefFunction instance = new EOMonth(); + + @Override + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + if (args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + + try { + double startDateAsNumber = NumericFunction.singleOperandEvaluate(args[0], ec.getRowIndex(), ec.getColumnIndex()); + int months = (int) NumericFunction.singleOperandEvaluate(args[1], ec.getRowIndex(), ec.getColumnIndex()); + + // Excel treats date 0 as 1900-01-00; EOMONTH results in 1900-01-31 + if (startDateAsNumber >= 0.0 && startDateAsNumber < 1.0) { + startDateAsNumber = 1.0; + } + + Date startDate = DateUtil.getJavaDate(startDateAsNumber, false); + + Calendar cal = new GregorianCalendar(); + cal.setTime(startDate); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); + cal.set(Calendar.MILLISECOND, 0); + + cal.add(Calendar.MONTH, months + 1); + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.add(Calendar.DAY_OF_MONTH, -1); + + return new NumberEval(DateUtil.getExcelDate(cal.getTime())); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/RefValueImplementation.java b/src/testcases/org/apache/poi/ss/formula/functions/RefValueImplementation.java new file mode 100644 index 000000000..1a3fc1e62 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/RefValueImplementation.java @@ -0,0 +1,68 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +final class RefEvalImplementation implements RefEval { + + private final ValueEval value; + + public RefEvalImplementation(ValueEval value) { + this.value = value; + } + + @Override + public AreaEval offset(int relFirstRowIx, int relLastRowIx, + int relFirstColIx, int relLastColIx) { + throw new UnsupportedOperationException(); + } + + @Override + public ValueEval getInnerValueEval(int sheetIndex) { + return value; + } + + @Override + public int getNumberOfSheets() { + return 1; + } + + @Override + public int getFirstSheetIndex() { + return 0; + } + + @Override + public int getLastSheetIndex() { + return 0; + } + + @Override + public int getRow() { + throw new UnsupportedOperationException(); + } + + @Override + public int getColumn() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestEDate.java b/src/testcases/org/apache/poi/ss/formula/functions/TestEDate.java index 3a5d527b7..3917f44cb 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestEDate.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestEDate.java @@ -31,42 +31,7 @@ import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.ErrorConstants; -public class TestEDate extends TestCase{ - - private final class RefEvalImplementation implements RefEval { - private final ValueEval value; - - public RefEvalImplementation(ValueEval value) { - super(); - this.value = value; - } - - public AreaEval offset(int relFirstRowIx, int relLastRowIx, - int relFirstColIx, int relLastColIx) { - throw new UnsupportedOperationException(); - } - - public ValueEval getInnerValueEval(int sheetIndex) { - return value; - } - - public int getNumberOfSheets() { - return 1; - } - public int getFirstSheetIndex() { - return 0; - } - public int getLastSheetIndex() { - return 0; - } - - public int getRow() { - throw new UnsupportedOperationException(); - } - public int getColumn() { - throw new UnsupportedOperationException(); - } - } +public class TestEDate extends TestCase { public void testEDateProperValues() { // verify some border-case combinations of startDate and month-increase diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestEOMonth.java b/src/testcases/org/apache/poi/ss/formula/functions/TestEOMonth.java new file mode 100644 index 000000000..55372b03a --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestEOMonth.java @@ -0,0 +1,136 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import java.util.Calendar; +import java.util.Date; + +import junit.framework.TestCase; +import org.apache.poi.ss.formula.OperationEvaluationContext; + +import org.apache.poi.ss.formula.eval.BlankEval; +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.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.ErrorConstants; + +public class TestEOMonth extends TestCase{ + + private static final double BAD_DATE = -1.0; + + private static final double DATE_1900_01_01 = 1.0; + private static final double DATE_1900_01_31 = 31.0; + private static final double DATE_1900_02_28 = 59.0; + private static final double DATE_1902_09_26 = 1000.0; + private static final double DATE_1902_09_30 = 1004.0; + private static final double DATE_2034_06_09 = 49104.0; + private static final double DATE_2034_06_30 = 49125.0; + private static final double DATE_2034_07_31 = 49156.0; + + private final FreeRefFunction eOMonth = EOMonth.instance; + private final OperationEvaluationContext ec = new OperationEvaluationContext(null, null, 0, 0, 0, null); + + public void testEOMonthProperValues() { + // verify some border-case combinations of startDate and month-increase + checkValue(DATE_1900_01_01, 0, DATE_1900_01_31); + checkValue(DATE_1900_01_01, 1, DATE_1900_02_28); + checkValue(DATE_1902_09_26, 0, DATE_1902_09_30); + checkValue(DATE_2034_06_09, 0, DATE_2034_06_30); + checkValue(DATE_2034_06_09, 1, DATE_2034_07_31); + } + + public void testEOMonthBadDateValues() { + checkValue(0.0, -2, BAD_DATE); + checkValue(0.0, -3, BAD_DATE); + checkValue(DATE_1900_01_31, -1, BAD_DATE); + } + + private void checkValue(double startDate, int monthInc, double expectedResult) { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(startDate), new NumberEval(monthInc)}, ec); + assertEquals(expectedResult, result.getNumberValue()); + } + + public void testEOMonthZeroDate() { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(0), new NumberEval(0)}, ec); + assertEquals("0 startDate is 1900-01-00", DATE_1900_01_31, result.getNumberValue()); + + result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(0), new NumberEval(1)}, ec); + assertEquals("0 startDate is 1900-01-00", DATE_1900_02_28, result.getNumberValue()); + } + + public void testEOMonthInvalidArguments() { + ValueEval result = eOMonth.evaluate(new ValueEval[] {new NumberEval(DATE_1902_09_26)}, ec); + assertTrue(result instanceof ErrorEval); + assertEquals(ErrorConstants.ERROR_VALUE, ((ErrorEval) result).getErrorCode()); + + result = eOMonth.evaluate(new ValueEval[] {new StringEval("a"), new StringEval("b")}, ec); + assertTrue(result instanceof ErrorEval); + assertEquals(ErrorConstants.ERROR_VALUE, ((ErrorEval) result).getErrorCode()); + } + + public void testEOMonthIncrease() { + checkOffset(new Date(), 2); + } + + public void testEOMonthDecrease() { + checkOffset(new Date(), -2); + } + + private void checkOffset(Date startDate, int offset) { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(DateUtil.getExcelDate(startDate)), new NumberEval(offset)}, ec); + Date resultDate = DateUtil.getJavaDate(result.getNumberValue()); + Calendar instance = Calendar.getInstance(); + instance.setTime(startDate); + instance.add(Calendar.MONTH, offset); + instance.add(Calendar.MONTH, 1); + instance.set(Calendar.DAY_OF_MONTH, 1); + instance.add(Calendar.DAY_OF_MONTH, -1); + instance.set(Calendar.HOUR_OF_DAY, 0); + instance.set(Calendar.MINUTE, 0); + instance.set(Calendar.SECOND, 0); + instance.set(Calendar.MILLISECOND, 0); + assertEquals(instance.getTime(), resultDate); + } + + public void testBug56688() { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(DATE_1902_09_26), new RefEvalImplementation(new NumberEval(0))}, ec); + assertEquals(DATE_1902_09_30, result.getNumberValue()); + } + + public void testRefEvalStartDate() { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new RefEvalImplementation(new NumberEval(DATE_1902_09_26)), new NumberEval(0)}, ec); + assertEquals(DATE_1902_09_30, result.getNumberValue()); + } + + public void testEOMonthBlankValueEval() { + NumberEval evaluate = (NumberEval) eOMonth.evaluate(new ValueEval[] {BlankEval.instance, new NumberEval(0)}, ec); + assertEquals("Blank is handled as 0", DATE_1900_01_31, evaluate.getNumberValue()); + } + + public void testEOMonthBlankRefValueEval() { + NumberEval result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new RefEvalImplementation(BlankEval.instance), new NumberEval(1)}, ec); + assertEquals("Blank is handled as 0", + DATE_1900_02_28, result.getNumberValue()); + + result = (NumberEval) eOMonth.evaluate(new ValueEval[] {new NumberEval(1), new RefEvalImplementation(BlankEval.instance)}, ec); + assertEquals("Blank is handled as 0", + DATE_1900_01_31, result.getNumberValue()); + } +}