Quellcode durchsuchen

Bug 61472: Convert date/time strings to numbers when evaluating formulas

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1855662 13f79535-47bb-0310-9956-ffa450edef68
tags/REL_4_1_0
Yegor Kozlov vor 5 Jahren
Ursprung
Commit
6ef332b48e

+ 15
- 1
src/java/org/apache/poi/ss/formula/eval/OperandResolver.java Datei anzeigen

package org.apache.poi.ss.formula.eval; package org.apache.poi.ss.formula.eval;


import org.apache.poi.ss.formula.EvaluationCell; import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellRangeAddress;


import java.time.DateTimeException;
import java.util.regex.Pattern; import java.util.regex.Pattern;


/** /**
return ((NumericValueEval)ev).getNumberValue(); return ((NumericValueEval)ev).getNumberValue();
} }
if (ev instanceof StringEval) { if (ev instanceof StringEval) {
Double dd = parseDouble(((StringEval) ev).getStringValue());
String sval = ((StringEval) ev).getStringValue();
Double dd = parseDouble(sval);
if(dd == null) dd = parseDateTime(sval);
if (dd == null) { if (dd == null) {
throw EvaluationException.invalidValue(); throw EvaluationException.invalidValue();
} }
} }


public static Double parseDateTime(String pText) {

try {
return DateUtil.parseDateTime(pText);
} catch (DateTimeException e) {
return null;
}

}

/** /**
* @param ve must be a <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt>, or <tt>BlankEval</tt> * @param ve must be a <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt>, or <tt>BlankEval</tt>
* @return the converted string value. never <code>null</code> * @return the converted string value. never <code>null</code>

+ 15
- 1
src/java/org/apache/poi/ss/formula/functions/Value.java Datei anzeigen

import org.apache.poi.ss.formula.eval.NumberEval; import org.apache.poi.ss.formula.eval.NumberEval;
import org.apache.poi.ss.formula.eval.OperandResolver; import org.apache.poi.ss.formula.eval.OperandResolver;
import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.eval.ValueEval;
import org.apache.poi.ss.usermodel.DateUtil;

import java.time.DateTimeException;


/** /**
* Implementation for Excel VALUE() function.<p> * Implementation for Excel VALUE() function.<p>
} }
String strText = OperandResolver.coerceValueToString(veText); String strText = OperandResolver.coerceValueToString(veText);
Double result = convertTextToNumber(strText); Double result = convertTextToNumber(strText);
if(result == null) result = parseDateTime(strText);
if (result == null) { if (result == null) {
return ErrorEval.VALUE_INVALID; return ErrorEval.VALUE_INVALID;
} }
* *
* @return <code>null</code> if there is any problem converting the text * @return <code>null</code> if there is any problem converting the text
*/ */
private static Double convertTextToNumber(String strText) {
public static Double convertTextToNumber(String strText) {
boolean foundCurrency = false; boolean foundCurrency = false;
boolean foundUnaryPlus = false; boolean foundUnaryPlus = false;
boolean foundUnaryMinus = false; boolean foundUnaryMinus = false;
double result = foundUnaryMinus ? -d : d; double result = foundUnaryMinus ? -d : d;
return foundPercentage ? result/100. : result; return foundPercentage ? result/100. : result;
} }

public static Double parseDateTime(String pText) {

try {
return DateUtil.parseDateTime(pText);
} catch (DateTimeException e) {
return null;
}

}
} }

+ 32
- 0
src/java/org/apache/poi/ss/usermodel/DateUtil.java Datei anzeigen



package org.apache.poi.ss.usermodel; package org.apache.poi.ss.usermodel;


import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQueries;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
// for format which start with "[DBNum1]" or "[DBNum2]" or "[DBNum3]" could be a Chinese date // for format which start with "[DBNum1]" or "[DBNum2]" or "[DBNum3]" could be a Chinese date
private static final Pattern date_ptrn5 = Pattern.compile("^\\[DBNum(1|2|3)\\]"); private static final Pattern date_ptrn5 = Pattern.compile("^\\[DBNum(1|2|3)\\]");


private static final DateTimeFormatter dateTimeFormats = new DateTimeFormatterBuilder()
.appendPattern("[dd MMM[ yyyy]][[ ]h:m[:s] a][[ ]H:m[:s]]")
.appendPattern("[[yyyy ]dd-MMM[-yyyy]][[ ]h:m[:s] a][[ ]H:m[:s]]")
.appendPattern("[M/dd[/yyyy]][[ ]h:m[:s] a][[ ]H:m[:s]]")
.appendPattern("[[yyyy/]M/dd][[ ]h:m[:s] a][[ ]H:m[:s]]")
.parseDefaulting(ChronoField.YEAR_OF_ERA, Calendar.getInstance().get(Calendar.YEAR))
.toFormatter();

/** /**
* Given a Date, converts it into a double representing its internal Excel representation, * Given a Date, converts it into a double representing its internal Excel representation,
* which is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds. * which is the number of days since 1/1/1900. Fractional days represent hours, minutes, and seconds.
} }
return result; return result;
} }

public static Double parseDateTime(String str){
TemporalAccessor tmp = dateTimeFormats.parse(str.replaceAll("\\s+", " "));
LocalTime time = tmp.query(TemporalQueries.localTime());
LocalDate date = tmp.query(TemporalQueries.localDate());
if(time == null && date == null) return null;

double tm = 0;
if(date != null) {
Date d = Date.from(date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
tm = DateUtil.getExcelDate(d);
}
if(time != null) tm += 1.0*time.toSecondOfDay()/SECONDS_PER_DAY;

return tm;
}
} }

+ 28
- 0
src/testcases/org/apache/poi/ss/formula/eval/TestOperandResolver.java Datei anzeigen

import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
import junit.framework.TestCase; import junit.framework.TestCase;


import java.util.LinkedHashMap;
import java.util.Map;

/** /**
* Tests for <tt>OperandResolver</tt> * Tests for <tt>OperandResolver</tt>
* *
assertNull(OperandResolver.parseDouble(value)); assertNull(OperandResolver.parseDouble(value));
} }
} }

public void testCoerceDateStringToNumber() throws EvaluationException {
Map<String, Double> values = new LinkedHashMap<>();
values.put("2019/1/18", 43483.);
values.put("01/18/2019", 43483.);
values.put("18 Jan 2019", 43483.);
values.put("18-Jan-2019", 43483.);

for (String str : values.keySet()) {
assertEquals(OperandResolver.coerceValueToDouble(new StringEval(str)), values.get(str), 0.00001);
}
}

public void testCoerceTimeStringToNumber() throws EvaluationException {
Map<String, Double> values = new LinkedHashMap<>();
values.put("00:00", 0.0);
values.put("12:00", 0.5);
values.put("15:43:09", 0.654965278);
values.put("15:43", 0.654861111);
values.put("3:43 PM", 0.654861111);

for (String str : values.keySet()) {
assertEquals(OperandResolver.coerceValueToDouble(new StringEval(str)), values.get(str), 0.00001);
}
}
} }

+ 23
- 4
src/testcases/org/apache/poi/ss/formula/functions/BaseTestFunctionsFromSpreadsheet.java Datei anzeigen

public int formulasRowIdx; public int formulasRowIdx;
@Parameter(value = 4) @Parameter(value = 4)
public HSSFFormulaEvaluator evaluator; public HSSFFormulaEvaluator evaluator;
@Parameter(value = 5)
public int precisionColumnIndex;




HSSFSheet sheet = workbook.getSheetAt(sheetIdx); HSSFSheet sheet = workbook.getSheetAt(sheetIdx);
processFunctionGroup(data, sheet, SS.START_TEST_CASES_ROW_INDEX, filename); processFunctionGroup(data, sheet, SS.START_TEST_CASES_ROW_INDEX, filename);
} }
workbook.close(); workbook.close();
return data; return data;
private static void processFunctionGroup(List<Object[]> data, HSSFSheet sheet, final int startRowIndex, String filename) { private static void processFunctionGroup(List<Object[]> data, HSSFSheet sheet, final int startRowIndex, String filename) {
HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet.getWorkbook()); HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(sheet.getWorkbook());


int precisionColumnIndex = -1;
HSSFRow precisionRow = sheet.getWorkbook().getSheetAt(0).getRow(11);
HSSFCell precisionCell = precisionRow == null ? null : precisionRow.getCell(0);
if(precisionCell != null && precisionCell.getCellType() == CellType.NUMERIC){
precisionColumnIndex = (int)precisionCell.getNumericCellValue();
}


String currentGroupComment = ""; String currentGroupComment = "";
final int maxRows = sheet.getLastRowNum()+1; final int maxRows = sheet.getLastRowNum()+1;
for(int rowIndex=startRowIndex; rowIndex<maxRows; rowIndex++) { for(int rowIndex=startRowIndex; rowIndex<maxRows; rowIndex++) {
testName = evalCell.getCellFormula(); testName = evalCell.getCellFormula();
} }
data.add(new Object[]{testName, filename, sheet, rowIndex, evaluator});
data.add(new Object[]{testName, filename, sheet, rowIndex, evaluator, precisionColumnIndex});
} }
fail("Missing end marker '" + SS.TEST_CASES_END_MARKER + "' on sheet '" + sheet.getSheetName() + "'"); fail("Missing end marker '" + SS.TEST_CASES_END_MARKER + "' on sheet '" + sheet.getSheetName() + "'");
} }
HSSFRow r = sheet.getRow(formulasRowIdx); HSSFRow r = sheet.getRow(formulasRowIdx);
HSSFCell evalCell = r.getCell(SS.COLUMN_INDEX_EVALUATION); HSSFCell evalCell = r.getCell(SS.COLUMN_INDEX_EVALUATION);
HSSFCell expectedCell = r.getCell(SS.COLUMN_INDEX_EXPECTED_RESULT); HSSFCell expectedCell = r.getCell(SS.COLUMN_INDEX_EXPECTED_RESULT);
HSSFCell precisionCell = r.getCell(precisionColumnIndex);

CellReference cr = new CellReference(sheet.getSheetName(), formulasRowIdx, evalCell.getColumnIndex(), false, false); CellReference cr = new CellReference(sheet.getSheetName(), formulasRowIdx, evalCell.getColumnIndex(), false, false);
String msg = String.format(Locale.ROOT, "In %s %s {=%s} '%s'" String msg = String.format(Locale.ROOT, "In %s %s {=%s} '%s'"
, filename, cr.formatAsString(), evalCell.getCellFormula(), testName); , filename, cr.formatAsString(), evalCell.getCellFormula(), testName);
case FORMULA: // will never be used, since we will call method after formula evaluation case FORMULA: // will never be used, since we will call method after formula evaluation
fail("Cannot expect formula as result of formula evaluation: " + msg); fail("Cannot expect formula as result of formula evaluation: " + msg);
case NUMERIC: case NUMERIC:
assertEquals(expectedCell.getNumericCellValue(), actualValue.getNumberValue(), 0.0);
double precision = precisionCell != null && precisionCell.getCellType() == CellType.NUMERIC
? precisionCell.getNumericCellValue() : 0.0;
assertEquals(expectedCell.getNumericCellValue(), actualValue.getNumberValue(), precision);
break; break;
case STRING: case STRING:
assertEquals(msg, expectedCell.getRichStringCellValue().getString(), actualValue.getStringValue()); assertEquals(msg, expectedCell.getRichStringCellValue().getString(), actualValue.getStringValue());
HSSFSheet sheet = workbook.getSheetAt(0); HSSFSheet sheet = workbook.getSheetAt(0);
String specifiedClassName = sheet.getRow(2).getCell(0).getRichStringCellValue().getString(); String specifiedClassName = sheet.getRow(2).getCell(0).getRichStringCellValue().getString();
assertEquals("Test class name in spreadsheet comment", clazz.getName(), specifiedClassName); assertEquals("Test class name in spreadsheet comment", clazz.getName(), specifiedClassName);

HSSFRow precisionRow = sheet.getRow(11);
HSSFCell precisionCell = precisionRow == null ? null : precisionRow.getCell(0);
if(precisionCell != null && precisionCell.getCellType() == CellType.NUMERIC){

}
} }


/** /**

+ 29
- 0
src/testcases/org/apache/poi/ss/formula/functions/TestDateTimeToNumberFromSpreadsheet.java Datei anzeigen

/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */

package org.apache.poi.ss.formula.functions;

import org.junit.runners.Parameterized.Parameters;

import java.util.Collection;

public class TestDateTimeToNumberFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet {
@Parameters(name="{0}")
public static Collection<Object[]> data() throws Exception {
return data(TestDateTimeToNumberFromSpreadsheet.class, "DateTimeToNumberTestCases.xls");
}
}

BIN
test-data/spreadsheet/DateTimeToNumberTestCases.xls Datei anzeigen


Laden…
Abbrechen
Speichern