diff --git a/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java b/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java index f97728c341..0c3f537dd6 100644 --- a/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java @@ -17,8 +17,8 @@ package org.apache.poi.ss.formula.atp; +import java.time.LocalDate; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.apache.poi.ss.formula.eval.AreaEvalBase; @@ -27,6 +27,7 @@ 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.usermodel.DateUtil; +import org.apache.poi.ss.util.DateParser; /** * Evaluator for formula arguments. @@ -59,7 +60,7 @@ final class ArgumentsEvaluator { if (dVal != null) { return dVal.doubleValue(); } - Calendar date = DateParser.parseDate(strVal); + LocalDate date = DateParser.parseLocalDate(strVal); return DateUtil.getExcelDate(date, false); } return OperandResolver.coerceValueToDouble(ve); diff --git a/src/java/org/apache/poi/ss/formula/atp/YearFrac.java b/src/java/org/apache/poi/ss/formula/atp/YearFrac.java index bb2134a951..a4f14b5436 100644 --- a/src/java/org/apache/poi/ss/formula/atp/YearFrac.java +++ b/src/java/org/apache/poi/ss/formula/atp/YearFrac.java @@ -17,7 +17,7 @@ package org.apache.poi.ss.formula.atp; -import java.util.Calendar; +import java.time.LocalDate; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.EvaluationException; @@ -28,6 +28,8 @@ 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; +import org.apache.poi.ss.util.DateParser; + /** * Implementation of Excel 'Analysis ToolPak' function YEARFRAC()
* @@ -90,7 +92,7 @@ final class YearFrac implements FreeRefFunction { if (dVal != null) { return dVal.doubleValue(); } - Calendar date = DateParser.parseDate(strVal); + LocalDate date = DateParser.parseLocalDate(strVal); return DateUtil.getExcelDate(date, false); } return OperandResolver.coerceValueToDouble(ve); diff --git a/src/java/org/apache/poi/ss/formula/functions/DateValue.java b/src/java/org/apache/poi/ss/formula/functions/DateValue.java index ea62ee156d..fa241a7389 100644 --- a/src/java/org/apache/poi/ss/formula/functions/DateValue.java +++ b/src/java/org/apache/poi/ss/formula/functions/DateValue.java @@ -17,23 +17,13 @@ package org.apache.poi.ss.formula.functions; -import java.text.DateFormatSymbols; -import java.time.DateTimeException; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.MatchResult; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import org.apache.poi.ss.util.DateParser; import org.apache.poi.ss.formula.eval.BlankEval; -import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.EvaluationException; import org.apache.poi.ss.formula.eval.NumberEval; import org.apache.poi.ss.formula.eval.OperandResolver; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.usermodel.DateUtil; -import org.apache.poi.util.LocaleUtil; /** * Implementation for the DATEVALUE() Excel function.

@@ -56,32 +46,6 @@ import org.apache.poi.util.LocaleUtil; */ public class DateValue extends Fixed1ArgFunction { - private enum Format { - YMD_DASHES("^(\\d{4})-(\\w+)-(\\d{1,2})$", "ymd"), - DMY_DASHES("^(\\d{1,2})-(\\w+)-(\\d{4})$", "dmy"), - MD_DASHES("^(\\w+)-(\\d{1,2})$", "md"), - MDY_SLASHES("^(\\w+)/(\\d{1,2})/(\\d{4})$", "mdy"), - YMD_SLASHES("^(\\d{4})/(\\w+)/(\\d{1,2})$", "ymd"), - MD_SLASHES("^(\\w+)/(\\d{1,2})$", "md"); - - private Pattern pattern; - private boolean hasYear; - private int yearIndex; - private int monthIndex; - private int dayIndex; - - Format(String patternString, String groupOrder) { - this.pattern = Pattern.compile(patternString); - this.hasYear = groupOrder.contains("y"); - if (hasYear) { - yearIndex = groupOrder.indexOf("y"); - } - monthIndex = groupOrder.indexOf("m"); - dayIndex = groupOrder.indexOf("d"); - } - - } - @Override public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval dateTextArg) { try { @@ -92,45 +56,9 @@ public class DateValue extends Fixed1ArgFunction { return BlankEval.instance; } - for (Format format : Format.values()) { - Matcher matcher = format.pattern.matcher(dateText); - if (matcher.find()) { - MatchResult matchResult = matcher.toMatchResult(); - List groups = new ArrayList<>(); - for (int i = 1; i <= matchResult.groupCount(); ++i) { - groups.add(matchResult.group(i)); - } - int year = format.hasYear - ? Integer.parseInt(groups.get(format.yearIndex)) - : LocalDate.now(LocaleUtil.getUserTimeZone().toZoneId()).getYear(); - int month = parseMonth(groups.get(format.monthIndex)); - int day = Integer.parseInt(groups.get(format.dayIndex)); - return new NumberEval(DateUtil.getExcelDate(LocalDate.of(year, month, day))); - - } - } - } catch (DateTimeException e) { - return ErrorEval.VALUE_INVALID; + return new NumberEval(DateUtil.getExcelDate(DateParser.parseLocalDate(dateText))); } catch (EvaluationException e) { return e.getErrorEval(); } - - return ErrorEval.VALUE_INVALID; - } - - private int parseMonth(String monthPart) { - try { - return Integer.parseInt(monthPart); - } catch (NumberFormatException ignored) { - } - - - String[] months = DateFormatSymbols.getInstance(LocaleUtil.getUserLocale()).getMonths(); - for (int month = 0; month < months.length; ++month) { - if (months[month].toLowerCase(LocaleUtil.getUserLocale()).startsWith(monthPart.toLowerCase(LocaleUtil.getUserLocale()))) { - return month + 1; - } - } - return -1; } } diff --git a/src/java/org/apache/poi/ss/util/DateParser.java b/src/java/org/apache/poi/ss/util/DateParser.java new file mode 100644 index 0000000000..292630006d --- /dev/null +++ b/src/java/org/apache/poi/ss/util/DateParser.java @@ -0,0 +1,133 @@ +/* ==================================================================== + 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.util; + +import java.text.DateFormatSymbols; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +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.util.LocaleUtil; + +/** + * Parser for java dates. + */ +public class DateParser { + private DateParser() { + // enforcing singleton + } + + + private enum Format { + YMD_DASHES("^(\\d{4})-(\\w+)-(\\d{1,2})( .*)?$", "ymd"), + DMY_DASHES("^(\\d{1,2})-(\\w+)-(\\d{4})( .*)?$", "dmy"), + MD_DASHES("^(\\w+)-(\\d{1,2})( .*)?$", "md"), + MDY_SLASHES("^(\\w+)/(\\d{1,2})/(\\d{4})( .*)?$", "mdy"), + YMD_SLASHES("^(\\d{4})/(\\w+)/(\\d{1,2})( .*)?$", "ymd"), + MD_SLASHES("^(\\w+)/(\\d{1,2})( .*)?$", "md"); + + private Pattern pattern; + private boolean hasYear; + private int yearIndex; + private int monthIndex; + private int dayIndex; + + Format(String patternString, String groupOrder) { + this.pattern = Pattern.compile(patternString); + this.hasYear = groupOrder.contains("y"); + if (hasYear) { + yearIndex = groupOrder.indexOf("y"); + } + monthIndex = groupOrder.indexOf("m"); + dayIndex = groupOrder.indexOf("d"); + } + + } + + private static int parseMonth(String monthPart) { + try { + return Integer.parseInt(monthPart); + } catch (NumberFormatException ignored) { + } + + + String[] months = DateFormatSymbols.getInstance(LocaleUtil.getUserLocale()).getMonths(); + for (int month = 0; month < months.length; ++month) { + if (months[month].toLowerCase(LocaleUtil.getUserLocale()).startsWith(monthPart.toLowerCase(LocaleUtil.getUserLocale()))) { + return month + 1; + } + } + return -1; + } + + /** + * Parses a date from a string. + * + * @param strVal a string with a date pattern. + * @return a date parsed from argument. + * @throws EvaluationException exception upon parsing. + */ + public static LocalDate parseLocalDate(String strVal) throws EvaluationException { + for (Format format : Format.values()) { + Matcher matcher = format.pattern.matcher(strVal); + if (matcher.find()) { + MatchResult matchResult = matcher.toMatchResult(); + List groups = new ArrayList<>(); + for (int i = 1; i <= matchResult.groupCount(); ++i) { + groups.add(matchResult.group(i)); + } + int year = format.hasYear + ? Integer.parseInt(groups.get(format.yearIndex)) + : LocalDate.now(LocaleUtil.getUserTimeZone().toZoneId()).getYear(); + int month = parseMonth(groups.get(format.monthIndex)); + int day = Integer.parseInt(groups.get(format.dayIndex)); + return LocalDate.of(year, month, day); + + } + } + + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + public static Calendar parseDate(String strVal) throws EvaluationException { + LocalDate date = parseLocalDate(strVal); + return makeDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth()); + } + + /** + * @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 = LocaleUtil.getLocaleCalendar(year, month - 1, 1, 0, 0, 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; + } + +} diff --git a/src/testcases/org/apache/poi/ss/util/TestDateParser.java b/src/testcases/org/apache/poi/ss/util/TestDateParser.java new file mode 100644 index 0000000000..d20d234ef7 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/util/TestDateParser.java @@ -0,0 +1,79 @@ +/* ==================================================================== + 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Calendar; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.util.LocaleUtil; +import org.junit.Test; + +public class TestDateParser { + @Test + public void testFailWhenNoDate() { + try { + DateParser.parseDate("potato"); + fail("Shouldn't parse potato!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + @Test + public void testFailWhenLooksLikeDateButItIsnt() { + try { + DateParser.parseDate("potato/cucumber/banana"); + fail("Shouldn't parse this thing!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + @Test + public void testFailWhenIsInvalidDate() { + try { + DateParser.parseDate("13/13/13"); + fail("Shouldn't parse this thing!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + @Test + public void testShouldParseValidDate() throws EvaluationException { + Calendar expDate = LocaleUtil.getLocaleCalendar(1984, Calendar.OCTOBER, 20); + Calendar actDate = DateParser.parseDate("1984/10/20"); + assertEquals("Had: " + expDate.getTime() + " and " + actDate.getTime() + "/" + + expDate.getTimeInMillis() + "ms and " + actDate.getTimeInMillis() + "ms", + expDate, actDate); + } + + @Test + public void testShouldIgnoreTimestamp() throws EvaluationException { + Calendar expDate = LocaleUtil.getLocaleCalendar(1984, Calendar.OCTOBER, 20); + Calendar actDate = DateParser.parseDate("1984/10/20 12:34:56"); + assertEquals("Had: " + expDate.getTime() + " and " + actDate.getTime() + "/" + + expDate.getTimeInMillis() + "ms and " + actDate.getTimeInMillis() + "ms", + expDate, actDate); + } + +}