<changes>
<release version="3.8-beta6" date="2012-??-??">
+ <action dev="poi-developers" type="add">52378 - Support for WORKDAY and NETWORKDAYS functions</action>
<action dev="poi-developers" type="add">52349 - Merge the logic between the TEXT function and DataFormatter</action>
<action dev="poi-developers" type="fix">52349 - Correctly support excel style date format strings in the TEXT function</action>
<action dev="poi-developers" type="fix">52369 - XSSFExcelExtractor should format numeric cells based on the format strings applied to them</action>
-/* ====================================================================
- 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;
private final Map<String, FreeRefFunction> _functionsByName = createFunctionsMap();
-
private AnalysisToolPak() {
// enforce singleton
}
public FreeRefFunction findFunction(String name) {
- return _functionsByName.get(name);
+ return _functionsByName.get(name.toUpperCase());
}
private Map<String, FreeRefFunction> createFunctionsMap() {
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);
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);
--- /dev/null
+/* ====================================================================
+ 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<Double> valuesList = new ArrayList<Double>();
+ 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);
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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;
+ }
+
+}
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()<br/>
--- /dev/null
+/* ====================================================================
+ 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()<br/>
+ * 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.
+ * <p/>
+ * <b>Syntax</b><br/>
+ * <b>NETWORKDAYS</b>(<b>startDate</b>, <b>endDate</b>, holidays)
+ * <p/>
+ *
+ * @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;
+ }
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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 <code>true</code> if date is weekend, <code>false</code> 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 <code>true</code> if date is a holiday, <code>false</code> 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 <code>1</code> is not a workday, <code>0</code> 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 <code>true</code> if aDate is between start and end dates, <code>false</code> otherwise.
+ */
+ protected boolean isInARange(double start, double end, double aDate) {
+ return aDate >= start && aDate <= end;
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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()<br/>
+ * 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.
+ * <p/>
+ * <b>Syntax</b><br/>
+ * <b>WORKDAY</b>(<b>startDate</b>, <b>days</b>, holidays)
+ * <p/>
+ *
+ * @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;
+ }
+ }
+
+}
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);
--- /dev/null
+/* ====================================================================
+ 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"));
+ }
+
+}
--- /dev/null
+/* ====================================================================
+ 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<ValueEval> holidays;
+
+ public MockAreaEval(String... holidays) {
+ this(0, 0, 0, holidays.length - 1);
+ this.holidays = new ArrayList<ValueEval>();
+ 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;
+ }
+
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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));
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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<ValueEval> holidays;
+
+ public MockAreaEval(String... holidays) {
+ this(0, 0, 0, holidays.length - 1);
+ this.holidays = new ArrayList<ValueEval>();
+ 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;
+ }
+
+ }
+}