From a20915f758a99a549551d1132023c87105939bc6 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Wed, 21 Dec 2011 11:40:48 +0000 Subject: [PATCH] Bugzill 52378: Support for Excel's Analysis Tool Pack functions workday and networkdays git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1221682 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../poi/ss/formula/atp/AnalysisToolPak.java | 32 ++-- .../ss/formula/atp/ArgumentsEvaluator.java | 117 ++++++++++++ .../apache/poi/ss/formula/atp/DateParser.java | 102 +++++++++++ .../org/apache/poi/ss/formula/atp/MRound.java | 5 - .../ss/formula/atp/NetworkdaysFunction.java | 84 +++++++++ .../poi/ss/formula/atp/WorkdayCalculator.java | 168 ++++++++++++++++++ .../poi/ss/formula/atp/WorkdayFunction.java | 78 ++++++++ .../apache/poi/ss/formula/atp/YearFrac.java | 57 +----- .../poi/ss/formula/atp/TestDateParser.java | 74 ++++++++ .../formula/atp/TestNetworkdaysFunction.java | 135 ++++++++++++++ .../ss/formula/atp/TestWorkdayCalculator.java | 96 ++++++++++ .../ss/formula/atp/TestWorkdayFunction.java | 140 +++++++++++++++ 13 files changed, 1008 insertions(+), 81 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java create mode 100644 src/java/org/apache/poi/ss/formula/atp/DateParser.java create mode 100644 src/java/org/apache/poi/ss/formula/atp/NetworkdaysFunction.java create mode 100644 src/java/org/apache/poi/ss/formula/atp/WorkdayCalculator.java create mode 100644 src/java/org/apache/poi/ss/formula/atp/WorkdayFunction.java create mode 100644 src/testcases/org/apache/poi/ss/formula/atp/TestDateParser.java create mode 100644 src/testcases/org/apache/poi/ss/formula/atp/TestNetworkdaysFunction.java create mode 100644 src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayCalculator.java create mode 100644 src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayFunction.java diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index caedc59802..8b53179187 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 52378 - Support for WORKDAY and NETWORKDAYS functions 52349 - Merge the logic between the TEXT function and DataFormatter 52349 - Correctly support excel style date format strings in the TEXT function 52369 - XSSFExcelExtractor should format numeric cells based on the format strings applied to them 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 53237caa04..cc98aae2a5 100644 --- a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java +++ b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -1,19 +1,12 @@ -/* ==================================================================== - 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. -==================================================================== */ +/* + * ==================================================================== 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; @@ -50,13 +43,12 @@ public final class AnalysisToolPak implements UDFFinder { private final Map _functionsByName = createFunctionsMap(); - private AnalysisToolPak() { // enforce singleton } public FreeRefFunction findFunction(String name) { - return _functionsByName.get(name); + return _functionsByName.get(name.toUpperCase()); } private Map createFunctionsMap() { @@ -140,7 +132,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "MDURATION", null); r(m, "MROUND", MRound.instance); r(m, "MULTINOMIAL", null); - r(m, "NETWORKDAYS", null); + r(m, "NETWORKDAYS", NetworkdaysFunction.instance); r(m, "NOMINAL", null); r(m, "OCT2BIN", null); r(m, "OCT2DEC", null); @@ -163,7 +155,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "TBILLPRICE", null); r(m, "TBILLYIELD", null); r(m, "WEEKNUM", null); - r(m, "WORKDAY", null); + r(m, "WORKDAY", WorkdayFunction.instance); r(m, "XIRR", null); r(m, "XNPV", null); r(m, "YEARFRAC", YearFrac.instance); diff --git a/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java b/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.java new file mode 100644 index 0000000000..012333f0b1 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/ArgumentsEvaluator.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.atp; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import org.apache.poi.ss.formula.eval.AreaEvalBase; +import org.apache.poi.ss.formula.eval.EvaluationException; +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; + +/** + * Evaluator for formula arguments. + * + * @author jfaenomoto@gmail.com + */ +final class ArgumentsEvaluator { + + public static final ArgumentsEvaluator instance = new ArgumentsEvaluator(); + + private ArgumentsEvaluator() { + // enforces singleton + } + + /** + * Evaluate a generic {@link ValueEval} argument to a double value that represents a date in POI. + * + * @param arg {@link ValueEval} an argument. + * @param srcCellRow number cell row. + * @param srcCellCol number cell column. + * @return a double representing a date in POI. + * @throws EvaluationException exception upon argument evaluation. + */ + public 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 = DateParser.parseDate(strVal); + return DateUtil.getExcelDate(date, false); + } + return OperandResolver.coerceValueToDouble(ve); + } + + /** + * Evaluate a generic {@link ValueEval} argument to an array of double values that represents dates in POI. + * + * @param arg {@link ValueEval} an argument. + * @param srcCellRow number cell row. + * @param srcCellCol number cell column. + * @return an array of doubles representing dates in POI. + * @throws EvaluationException exception upon argument evaluation. + */ + public double[] evaluateDatesArg(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + if (arg == null) { + return new double[0]; + } + + if (arg instanceof StringEval) { + return new double[]{ evaluateDateArg(arg, srcCellRow, srcCellCol) }; + } else if (arg instanceof AreaEvalBase) { + List valuesList = new ArrayList(); + AreaEvalBase area = (AreaEvalBase) arg; + for (int i = area.getFirstRow(); i <= area.getLastRow(); i++) { + for (int j = area.getFirstColumn(); j <= area.getLastColumn(); j++) { + valuesList.add(evaluateDateArg(area.getValue(i, j), i, j)); + } + } + double[] values = new double[valuesList.size()]; + for (int i = 0; i < valuesList.size(); i++) { + values[i] = valuesList.get(i).doubleValue(); + } + return values; + } + return new double[]{ OperandResolver.coerceValueToDouble(arg) }; + } + + /** + * Evaluate a generic {@link ValueEval} argument to a double value. + * + * @param arg {@link ValueEval} an argument. + * @param srcCellRow number cell row. + * @param srcCellCol number cell column. + * @return a double value. + * @throws EvaluationException exception upon argument evaluation. + */ + public double evaluateNumberArg(ValueEval arg, int srcCellRow, int srcCellCol) throws EvaluationException { + if (arg == null) { + return 0f; + } + + return OperandResolver.coerceValueToDouble(arg); + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/DateParser.java b/src/java/org/apache/poi/ss/formula/atp/DateParser.java new file mode 100644 index 0000000000..325fbbb232 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/DateParser.java @@ -0,0 +1,102 @@ +/* ==================================================================== + 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; + +/** + * Parser for java dates. + * + * @author jfaenomoto@gmail.com + */ +public class DateParser { + + public DateParser instance = new DateParser(); + + private DateParser() { + // enforcing singleton + } + + /** + * 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 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; + } + +} diff --git a/src/java/org/apache/poi/ss/formula/atp/MRound.java b/src/java/org/apache/poi/ss/formula/atp/MRound.java index 55ed87084c..8ca8796ca0 100644 --- a/src/java/org/apache/poi/ss/formula/atp/MRound.java +++ b/src/java/org/apache/poi/ss/formula/atp/MRound.java @@ -21,11 +21,6 @@ import org.apache.poi.ss.formula.OperationEvaluationContext; import org.apache.poi.ss.formula.eval.*; import org.apache.poi.ss.formula.functions.FreeRefFunction; import org.apache.poi.ss.formula.functions.NumericFunction; -import org.apache.poi.ss.usermodel.DateUtil; - -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.regex.Pattern; /** * Implementation of Excel 'Analysis ToolPak' function MROUND()
diff --git a/src/java/org/apache/poi/ss/formula/atp/NetworkdaysFunction.java b/src/java/org/apache/poi/ss/formula/atp/NetworkdaysFunction.java new file mode 100644 index 0000000000..6c07ed9d39 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/NetworkdaysFunction.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.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.formula.functions.FreeRefFunction; + +/** + * Implementation of Excel 'Analysis ToolPak' function NETWORKDAYS()
+ * Returns the number of workdays given a starting and an ending date, considering an interval of holidays. A workday is any non + * saturday/sunday date. + *

+ * Syntax
+ * NETWORKDAYS(startDate, endDate, holidays) + *

+ * + * @author jfaenomoto@gmail.com + */ +final class NetworkdaysFunction implements FreeRefFunction { + + public static final FreeRefFunction instance = new NetworkdaysFunction(ArgumentsEvaluator.instance); + + private ArgumentsEvaluator evaluator; + + /** + * Constructor. + * + * @param anEvaluator an injected {@link ArgumentsEvaluator}. + */ + private NetworkdaysFunction(ArgumentsEvaluator anEvaluator) { + // enforces singleton + this.evaluator = anEvaluator; + } + + /** + * Evaluate for NETWORKDAYS. Given two dates and a optional date or interval of holidays, determines how many working days are there + * between those dates. + * + * @return {@link ValueEval} for the number of days between two dates. + */ + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + if (args.length < 2 || args.length > 3) { + return ErrorEval.VALUE_INVALID; + } + + int srcCellRow = ec.getRowIndex(); + int srcCellCol = ec.getColumnIndex(); + + double start, end; + double[] holidays; + try { + start = this.evaluator.evaluateDateArg(args[0], srcCellRow, srcCellCol); + end = this.evaluator.evaluateDateArg(args[1], srcCellRow, srcCellCol); + if (start > end) { + return ErrorEval.NAME_INVALID; + } + ValueEval holidaysCell = args.length == 3 ? args[2] : null; + holidays = this.evaluator.evaluateDatesArg(holidaysCell, srcCellRow, srcCellCol); + return new NumberEval(WorkdayCalculator.instance.calculateWorkdays(start, end, holidays)); + } catch (EvaluationException e) { + return ErrorEval.VALUE_INVALID; + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/WorkdayCalculator.java b/src/java/org/apache/poi/ss/formula/atp/WorkdayCalculator.java new file mode 100644 index 0000000000..315fe0d317 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/WorkdayCalculator.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.atp; + +import java.util.Calendar; +import java.util.Date; + +import org.apache.poi.ss.usermodel.DateUtil; + +/** + * A calculator for workdays, considering dates as excel representations. + * + * @author jfaenomoto@gmail.com + */ +public class WorkdayCalculator { + + public static final WorkdayCalculator instance = new WorkdayCalculator(); + + /** + * Constructor. + */ + private WorkdayCalculator() { + // enforcing singleton + } + + /** + * Calculate how many workdays are there between a start and an end date, as excel representations, considering a range of holidays. + * + * @param start start date. + * @param end end date. + * @param holidays an array of holidays. + * @return number of workdays between start and end dates, including both dates. + */ + public int calculateWorkdays(double start, double end, double[] holidays) { + int saturdaysPast = this.pastDaysOfWeek(start, end, Calendar.SATURDAY); + int sundaysPast = this.pastDaysOfWeek(start, end, Calendar.SUNDAY); + int nonWeekendHolidays = this.calculateNonWeekendHolidays(start, end, holidays); + return (int) (end - start + 1) - saturdaysPast - sundaysPast - nonWeekendHolidays; + } + + /** + * Calculate the workday past x workdays from a starting date, considering a range of holidays. + * + * @param start start date. + * @param workdays number of workdays to be past from starting date. + * @param holidays an array of holidays. + * @return date past x workdays. + */ + public Date calculateWorkdays(double start, int workdays, double[] holidays) { + Date startDate = DateUtil.getJavaDate(start); + Calendar endDate = Calendar.getInstance(); + endDate.setTime(startDate); + endDate.add(Calendar.DAY_OF_YEAR, workdays); + int skippedDays = 0; + do { + double end = DateUtil.getExcelDate(endDate.getTime()); + int saturdaysPast = this.pastDaysOfWeek(start, end, Calendar.SATURDAY); + int sundaysPast = this.pastDaysOfWeek(start, end, Calendar.SUNDAY); + int nonWeekendHolidays = this.calculateNonWeekendHolidays(start, end, holidays); + skippedDays = saturdaysPast + sundaysPast + nonWeekendHolidays; + endDate.add(Calendar.DAY_OF_YEAR, skippedDays); + start = end + isNonWorkday(end, holidays); + } while (skippedDays != 0); + return endDate.getTime(); + } + + /** + * Calculates how many days of week past between a start and an end date. + * + * @param start start date. + * @param end end date. + * @param dayOfWeek a day of week as represented by {@link Calendar} constants. + * @return how many days of week past in this interval. + */ + protected int pastDaysOfWeek(double start, double end, int dayOfWeek) { + int pastDaysOfWeek = 0; + int startDay = (int) Math.floor(start < end ? start : end); + int endDay = (int) Math.floor(end > start ? end : start); + for (; startDay <= endDay; startDay++) { + Calendar today = Calendar.getInstance(); + today.setTime(DateUtil.getJavaDate(startDay)); + if (today.get(Calendar.DAY_OF_WEEK) == dayOfWeek) { + pastDaysOfWeek++; + } + } + return start < end ? pastDaysOfWeek : -pastDaysOfWeek; + } + + /** + * Calculates how many holidays in a list are workdays, considering an interval of dates. + * + * @param start start date. + * @param end end date. + * @param holidays an array of holidays. + * @return number of holidays that occur in workdays, between start and end dates. + */ + protected int calculateNonWeekendHolidays(double start, double end, double[] holidays) { + int nonWeekendHolidays = 0; + double startDay = start < end ? start : end; + double endDay = end > start ? end : start; + for (int i = 0; i < holidays.length; i++) { + if (isInARange(startDay, endDay, holidays[i])) { + if (!isWeekend(holidays[i])) { + nonWeekendHolidays++; + } + } + } + return start < end ? nonWeekendHolidays : -nonWeekendHolidays; + } + + /** + * @param aDate a given date. + * @return true if date is weekend, false otherwise. + */ + protected boolean isWeekend(double aDate) { + Calendar date = Calendar.getInstance(); + date.setTime(DateUtil.getJavaDate(aDate)); + return date.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || date.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY; + } + + /** + * @param aDate a given date. + * @param holidays an array of holidays. + * @return true if date is a holiday, false otherwise. + */ + protected boolean isHoliday(double aDate, double[] holidays) { + for (int i = 0; i < holidays.length; i++) { + if (Math.round(holidays[i]) == Math.round(aDate)) { + return true; + } + } + return false; + } + + /** + * @param aDate a given date. + * @param holidays an array of holidays. + * @return 1 is not a workday, 0 otherwise. + */ + protected int isNonWorkday(double aDate, double[] holidays) { + return isWeekend(aDate) || isHoliday(aDate, holidays) ? 1 : 0; + } + + /** + * @param start start date. + * @param end end date. + * @param aDate a date to be analyzed. + * @return true if aDate is between start and end dates, false otherwise. + */ + protected boolean isInARange(double start, double end, double aDate) { + return aDate >= start && aDate <= end; + } +} diff --git a/src/java/org/apache/poi/ss/formula/atp/WorkdayFunction.java b/src/java/org/apache/poi/ss/formula/atp/WorkdayFunction.java new file mode 100644 index 0000000000..e746bd1395 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/WorkdayFunction.java @@ -0,0 +1,78 @@ +/* ==================================================================== + 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.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.formula.functions.FreeRefFunction; +import org.apache.poi.ss.usermodel.DateUtil; + +/** + * Implementation of Excel 'Analysis ToolPak' function WORKDAY()
+ * Returns the date past a number of workdays beginning at a start date, considering an interval of holidays. A workday is any non + * saturday/sunday date. + *

+ * Syntax
+ * WORKDAY(startDate, days, holidays) + *

+ * + * @author jfaenomoto@gmail.com + */ +final class WorkdayFunction implements FreeRefFunction { + + public static final FreeRefFunction instance = new WorkdayFunction(ArgumentsEvaluator.instance); + + private ArgumentsEvaluator evaluator; + + private WorkdayFunction(ArgumentsEvaluator anEvaluator) { + // enforces singleton + this.evaluator = anEvaluator; + } + + /** + * Evaluate for WORKDAY. Given a date, a number of days and a optional date or interval of holidays, determines which date it is past + * number of parametrized workdays. + * + * @return {@link ValueEval} with date as its value. + */ + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + if (args.length < 2 || args.length > 3) { + return ErrorEval.VALUE_INVALID; + } + + int srcCellRow = ec.getRowIndex(); + int srcCellCol = ec.getColumnIndex(); + + double start; + int days; + double[] holidays; + try { + start = this.evaluator.evaluateDateArg(args[0], srcCellRow, srcCellCol); + days = (int) Math.floor(this.evaluator.evaluateNumberArg(args[1], srcCellRow, srcCellCol)); + ValueEval holidaysCell = args.length == 3 ? args[2] : null; + holidays = this.evaluator.evaluateDatesArg(holidaysCell, srcCellRow, srcCellCol); + return new NumberEval(DateUtil.getExcelDate(WorkdayCalculator.instance.calculateWorkdays(start, days, holidays))); + } catch (EvaluationException e) { + return ErrorEval.VALUE_INVALID; + } + } + +} 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 062fcf6f9e..611ace9574 100644 --- a/src/java/org/apache/poi/ss/formula/atp/YearFrac.java +++ b/src/java/org/apache/poi/ss/formula/atp/YearFrac.java @@ -91,67 +91,12 @@ final class YearFrac implements FreeRefFunction { if (dVal != null) { return dVal.doubleValue(); } - Calendar date = parseDate(strVal); + Calendar date = DateParser.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/testcases/org/apache/poi/ss/formula/atp/TestDateParser.java b/src/testcases/org/apache/poi/ss/formula/atp/TestDateParser.java new file mode 100644 index 0000000000..ad3c35441c --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/atp/TestDateParser.java @@ -0,0 +1,74 @@ +/* ==================================================================== + 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 static java.util.Calendar.OCTOBER; + +import java.util.Calendar; +import java.util.Date; + +import junit.framework.TestCase; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; + +/** + * @author jfaenomoto@gmail.com + */ +public class TestDateParser extends TestCase { + + public void testFailWhenNoDate() { + try { + DateParser.parseDate("potato"); + fail("Shouldn't parse potato!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + public void testFailWhenLooksLikeDateButItIsnt() { + try { + DateParser.parseDate("potato/cucumber/banana"); + fail("Shouldn't parse this thing!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + public void testFailWhenIsInvalidDate() { + try { + DateParser.parseDate("13/13/13"); + fail("Shouldn't parse this thing!"); + } catch (EvaluationException e) { + assertEquals(ErrorEval.VALUE_INVALID, e.getErrorEval()); + } + } + + public void testShouldParseValidDate() throws EvaluationException { + Calendar aDate = Calendar.getInstance(); + aDate.setTime(new Date(84, OCTOBER, 20)); + assertEquals(aDate, DateParser.parseDate("1984/10/20")); + } + + public void testShouldIgnoreTimestamp() throws EvaluationException { + Calendar aDate = Calendar.getInstance(); + aDate.setTime(new Date(84, OCTOBER, 20)); + assertEquals(aDate, DateParser.parseDate("1984/10/20 12:34:56")); + } + +} diff --git a/src/testcases/org/apache/poi/ss/formula/atp/TestNetworkdaysFunction.java b/src/testcases/org/apache/poi/ss/formula/atp/TestNetworkdaysFunction.java new file mode 100644 index 0000000000..6f178e1aec --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/atp/TestNetworkdaysFunction.java @@ -0,0 +1,135 @@ +/* ==================================================================== + 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 static java.util.Calendar.DECEMBER; +import static java.util.Calendar.JANUARY; +import static java.util.Calendar.MARCH; +import static java.util.Calendar.NOVEMBER; +import static java.util.Calendar.OCTOBER; +import static org.apache.poi.ss.formula.eval.ErrorEval.NAME_INVALID; +import static org.apache.poi.ss.formula.eval.ErrorEval.VALUE_INVALID; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.TwoDEval; +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.AreaEvalBase; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.StringEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * @author jfaenomoto@gmail.com + */ +public class TestNetworkdaysFunction extends TestCase { + + private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd"); + + private static final String STARTING_DATE = formatter.format(new Date(108, OCTOBER, 1)); + + private static final String END_DATE = formatter.format(new Date(109, MARCH, 1)); + + private static final String FIRST_HOLIDAY = formatter.format(new Date(108, NOVEMBER, 26)); + + private static final String SECOND_HOLIDAY = formatter.format(new Date(108, DECEMBER, 4)); + + private static final String THIRD_HOLIDAY = formatter.format(new Date(109, JANUARY, 21)); + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 1, 1, 1, null); + + public void testFailWhenNoArguments() { + assertEquals(VALUE_INVALID, NetworkdaysFunction.instance.evaluate(new ValueEval[0], null)); + } + + public void testFailWhenLessThan2Arguments() { + assertEquals(VALUE_INVALID, NetworkdaysFunction.instance.evaluate(new ValueEval[1], null)); + } + + public void testFailWhenMoreThan3Arguments() { + assertEquals(VALUE_INVALID, NetworkdaysFunction.instance.evaluate(new ValueEval[4], null)); + } + + public void testFailWhenArgumentsAreNotDates() { + assertEquals(VALUE_INVALID, NetworkdaysFunction.instance.evaluate(new ValueEval[]{ new StringEval("Potato"), + new StringEval("Cucumber") }, EC)); + } + + public void testFailWhenStartDateAfterEndDate() { + assertEquals(NAME_INVALID, NetworkdaysFunction.instance.evaluate(new ValueEval[]{ new StringEval(END_DATE.toString()), + new StringEval(STARTING_DATE.toString()) }, EC)); + } + + public void testReturnNetworkdays() { + assertEquals(108, (int) ((NumericValueEval) NetworkdaysFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new StringEval(END_DATE.toString()) }, EC)).getNumberValue()); + } + + public void testReturnNetworkdaysWithAHoliday() { + assertEquals(107, (int) ((NumericValueEval) NetworkdaysFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new StringEval(END_DATE.toString()), new StringEval(FIRST_HOLIDAY.toString()) }, + EC)).getNumberValue()); + } + + public void testReturnNetworkdaysWithManyHolidays() { + assertEquals(105, (int) ((NumericValueEval) NetworkdaysFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new StringEval(END_DATE.toString()), + new MockAreaEval(FIRST_HOLIDAY, SECOND_HOLIDAY, THIRD_HOLIDAY) }, EC)).getNumberValue()); + } + + private class MockAreaEval extends AreaEvalBase { + + private List holidays; + + public MockAreaEval(String... holidays) { + this(0, 0, 0, holidays.length - 1); + this.holidays = new ArrayList(); + for (String holiday : holidays) { + this.holidays.add(new StringEval(holiday)); + } + } + + protected MockAreaEval(int firstRow, int firstColumn, int lastRow, int lastColumn) { + super(firstRow, firstColumn, lastRow, lastColumn); + } + + @Override + public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) { + return this.holidays.get(relativeColumnIndex); + } + + public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) { + return null; + } + + public TwoDEval getColumn(int columnIndex) { + return null; + } + + public TwoDEval getRow(int rowIndex) { + return null; + } + + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayCalculator.java b/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayCalculator.java new file mode 100644 index 0000000000..ff632a0b8b --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayCalculator.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.atp; + +import static java.util.Calendar.DECEMBER; +import static java.util.Calendar.SATURDAY; + +import java.util.Date; + +import junit.framework.TestCase; + +import org.apache.poi.ss.usermodel.DateUtil; + +/** + * @author jfaenomoto@gmail.com + */ +public class TestWorkdayCalculator extends TestCase { + + public void testCalculateWorkdaysShouldReturnJustWeekdaysWhenNoWeekend() { + final double A_MONDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 12)); + final double A_FRIDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 16)); + assertEquals(5, WorkdayCalculator.instance.calculateWorkdays(A_MONDAY, A_FRIDAY, new double[0])); + } + + public void testCalculateWorkdaysShouldReturnAllDaysButNoSaturdays() { + final double A_WEDNESDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 14)); + final double A_SATURDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 18)); + assertEquals(3, WorkdayCalculator.instance.calculateWorkdays(A_WEDNESDAY, A_SATURDAY, new double[0])); + } + + public void testCalculateWorkdaysShouldReturnAllDaysButNoSundays() { + final double A_SUNDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 11)); + final double A_THURSDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 15)); + assertEquals(4, WorkdayCalculator.instance.calculateWorkdays(A_SUNDAY, A_THURSDAY, new double[0])); + } + + public void testCalculateWorkdaysShouldReturnAllDaysButNoHolidays() { + final double A_MONDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 12)); + final double A_FRIDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 16)); + final double A_WEDNESDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 14)); + assertEquals(4, WorkdayCalculator.instance.calculateWorkdays(A_MONDAY, A_FRIDAY, new double[]{ A_WEDNESDAY })); + } + + public void testCalculateWorkdaysShouldIgnoreWeekendHolidays() { + final double A_FRIDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 16)); + final double A_SATURDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 17)); + final double A_SUNDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 18)); + final double A_WEDNESDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 21)); + assertEquals(4, WorkdayCalculator.instance.calculateWorkdays(A_FRIDAY, A_WEDNESDAY, new double[]{ A_SATURDAY, A_SUNDAY })); + } + + public void testPastDaysOfWeekShouldReturn0Past0Saturdays() { + final double A_WEDNESDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 7)); + final double A_FRIDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 9)); + assertEquals(0, WorkdayCalculator.instance.pastDaysOfWeek(A_WEDNESDAY, A_FRIDAY, SATURDAY)); + } + + public void testPastDaysOfWeekShouldReturn1Past1Saturdays() { + final double A_WEDNESDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 7)); + final double A_SUNDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 11)); + assertEquals(1, WorkdayCalculator.instance.pastDaysOfWeek(A_WEDNESDAY, A_SUNDAY, SATURDAY)); + } + + public void testPastDaysOfWeekShouldReturn2Past2Saturdays() { + final double A_THURSDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 8)); + final double A_MONDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 19)); + assertEquals(2, WorkdayCalculator.instance.pastDaysOfWeek(A_THURSDAY, A_MONDAY, SATURDAY)); + } + + public void testPastDaysOfWeekShouldReturn1BeginningFromASaturday() { + final double A_SATURDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 10)); + final double A_SUNDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 11)); + assertEquals(1, WorkdayCalculator.instance.pastDaysOfWeek(A_SATURDAY, A_SUNDAY, SATURDAY)); + } + + public void testPastDaysOfWeekShouldReturn1EndingAtASaturday() { + final double A_THURSDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 8)); + final double A_SATURDAY = DateUtil.getExcelDate(new Date(111, DECEMBER, 10)); + assertEquals(1, WorkdayCalculator.instance.pastDaysOfWeek(A_THURSDAY, A_SATURDAY, SATURDAY)); + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayFunction.java b/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayFunction.java new file mode 100644 index 0000000000..811ec18204 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/atp/TestWorkdayFunction.java @@ -0,0 +1,140 @@ +/* ==================================================================== + 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 static java.util.Calendar.APRIL; +import static java.util.Calendar.DECEMBER; +import static java.util.Calendar.JANUARY; +import static java.util.Calendar.MAY; +import static java.util.Calendar.NOVEMBER; +import static java.util.Calendar.OCTOBER; +import static java.util.Calendar.SEPTEMBER; +import static org.apache.poi.ss.formula.eval.ErrorEval.NUM_ERROR; +import static org.apache.poi.ss.formula.eval.ErrorEval.VALUE_INVALID; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.TwoDEval; +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.AreaEvalBase; +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; + +/** + * @author jfaenomoto@gmail.com + */ +public class TestWorkdayFunction extends TestCase { + + private static final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd"); + + private static final String STARTING_DATE = formatter.format(new Date(108, OCTOBER, 1)); + + private static final String FIRST_HOLIDAY = formatter.format(new Date(108, NOVEMBER, 26)); + + private static final String SECOND_HOLIDAY = formatter.format(new Date(108, DECEMBER, 4)); + + private static final String THIRD_HOLIDAY = formatter.format(new Date(109, JANUARY, 21)); + + private static final String RETROATIVE_HOLIDAY = formatter.format(new Date(108, SEPTEMBER, 29)); + + private static final OperationEvaluationContext EC = new OperationEvaluationContext(null, null, 1, 1, 1, null); + + public void testFailWhenNoArguments() { + assertEquals(VALUE_INVALID, WorkdayFunction.instance.evaluate(new ValueEval[0], null)); + } + + public void testFailWhenLessThan2Arguments() { + assertEquals(VALUE_INVALID, WorkdayFunction.instance.evaluate(new ValueEval[1], null)); + } + + public void testFailWhenMoreThan3Arguments() { + assertEquals(VALUE_INVALID, WorkdayFunction.instance.evaluate(new ValueEval[4], null)); + } + + public void testFailWhenArgumentsAreNotDatesNorNumbers() { + assertEquals(VALUE_INVALID, WorkdayFunction.instance.evaluate( + new ValueEval[]{ new StringEval("Potato"), new StringEval("Cucumber") }, EC)); + } + + public void testReturnWorkdays() { + assertEquals(new Date(109, APRIL, 30), DateUtil.getJavaDate(((NumberEval) WorkdayFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new NumberEval(151) }, EC)).getNumberValue())); + } + + public void testReturnWorkdaysWithDaysTruncated() { + assertEquals(new Date(109, APRIL, 30), DateUtil.getJavaDate(((NumberEval) WorkdayFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new NumberEval(151.99999) }, EC)).getNumberValue())); + } + + public void testReturnRetroativeWorkday() { + assertEquals(new Date(108, SEPTEMBER, 23), DateUtil.getJavaDate(((NumberEval) WorkdayFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new NumberEval(-5), new StringEval(RETROATIVE_HOLIDAY.toString()) }, EC)) + .getNumberValue())); + } + + public void testReturnNetworkdaysWithManyHolidays() { + assertEquals(new Date(109, MAY, 5), DateUtil.getJavaDate(((NumberEval) WorkdayFunction.instance.evaluate(new ValueEval[]{ + new StringEval(STARTING_DATE.toString()), new NumberEval(151), + new MockAreaEval(FIRST_HOLIDAY, SECOND_HOLIDAY, THIRD_HOLIDAY) }, EC)).getNumberValue())); + } + + private class MockAreaEval extends AreaEvalBase { + + private List holidays; + + public MockAreaEval(String... holidays) { + this(0, 0, 0, holidays.length - 1); + this.holidays = new ArrayList(); + for (String holiday : holidays) { + this.holidays.add(new StringEval(holiday)); + } + } + + protected MockAreaEval(int firstRow, int firstColumn, int lastRow, int lastColumn) { + super(firstRow, firstColumn, lastRow, lastColumn); + } + + @Override + public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) { + return this.holidays.get(relativeColumnIndex); + } + + public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) { + return null; + } + + public TwoDEval getColumn(int columnIndex) { + return null; + } + + public TwoDEval getRow(int rowIndex) { + return null; + } + + } +} -- 2.39.5