From 69dd024ef7659c6a30bc69fb76d6b23a13853286 Mon Sep 17 00:00:00 2001 From: datagear Date: Fri, 28 Aug 2020 19:10:53 +0800 Subject: [PATCH] =?UTF-8?q?[analysis]=E5=AE=8C=E5=96=84Excel=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=9B=86=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysis/support/AbstractDataSet.java | 11 + .../support/AbstractExcelDataSet.java | 520 +++++++++++++++--- .../support/ExcelDirectoryFileDataSet.java | 64 +++ .../support/JsonDirectoryFileDataSet.java | 3 +- .../analysis/support/RangeExpResolver.java | 13 +- .../analysis/support/SimpleExcelDataSet.java | 48 ++ .../ExcelDirectoryFileDataSetTest.java | 250 +++++++++ .../ExcelDirectoryFileDataSetTest-0.xlsx | Bin 0 -> 8434 bytes .../ExcelDirectoryFileDataSetTest-1.xls | Bin 0 -> 25600 bytes 9 files changed, 816 insertions(+), 93 deletions(-) create mode 100644 datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java create mode 100644 datagear-analysis/src/main/java/org/datagear/analysis/support/SimpleExcelDataSet.java create mode 100644 datagear-analysis/src/test/java/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest.java create mode 100644 datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-0.xlsx create mode 100644 datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-1.xls diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java index a29e6836..3ed973c1 100644 --- a/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractDataSet.java @@ -111,6 +111,17 @@ public abstract class AbstractDataSet extends AbstractIdentifiable implements Da return true; } + /** + * 解析属性类型。 + * + * @param value + * @return + */ + protected String resolveDataType(Object value) + { + return DataSetProperty.DataType.resolveDataType(value); + } + /** * 获取指定名称的{@linkplain DataNameType}对象,没找到则返回{@code null}。 * diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java index 4246e1b3..cec4f87a 100644 --- a/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/AbstractExcelDataSet.java @@ -8,33 +8,57 @@ package org.datagear.analysis.support; import java.io.File; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.datagear.analysis.DataSetException; import org.datagear.analysis.DataSetProperty; import org.datagear.analysis.DataSetResult; import org.datagear.analysis.ResolvableDataSet; import org.datagear.analysis.ResolvedDataSetResult; import org.datagear.analysis.support.RangeExpResolver.IndexRange; +import org.datagear.analysis.support.RangeExpResolver.Range; import org.datagear.util.FileUtil; import org.datagear.util.IOUtil; import org.datagear.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 抽象Excel数据集。 + *

+ * 此类仅支持从Excel的单个sheet读取数据,具体参考{@linkplain #setSheetIndex(int)}。 + *

+ *

+ * 通过{@linkplain #setDataRowExp(String)}、{@linkplain #setDataColumnExp(String)}来设置读取行、列范围。 + *

+ *

+ * 通过{@linkplain #setNameRow(int)}可设置名称行。 + *

* * @author datagear@163.com * */ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet implements ResolvableDataSet { + protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelDataSet.class); + public static final String EXTENSION_XLSX = "xlsx"; public static final String EXTENSION_XLS = "xls"; @@ -42,11 +66,11 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im protected static final RangeExpResolver RANGE_EXP_RESOLVER = RangeExpResolver .valueOf(RangeExpResolver.RANGE_SPLITTER_CHAR, RangeExpResolver.RANGE_GROUP_SPLITTER_CHAR); - /** 是否强制作为xls文件处理 */ - private boolean forceXls = false; + /** 此数据集所处的sheet索引号(以0计数) */ + private int sheetIndex = 0; - /** 作为标题行的行号 */ - private int titleRow = -1; + /** 作为名称行的行号 */ + private int nameRow = -1; /** 数据行范围表达式 */ private String dataRowExp = null; @@ -54,7 +78,11 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im /** 数据列范围表达式 */ private String dataColumnExp = null; + /** 是否强制作为xls文件处理 */ + private boolean forceXls = false; + private transient List _dataRowRanges = null; + private transient List _dataColumnRanges = null; public AbstractExcelDataSet() { @@ -72,55 +100,51 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im super(id, name, properties); } + public int getSheetIndex() + { + return sheetIndex; + } + /** - * 是否强制作为xls文件处理。 + * 设置此数据集所处的sheet索引号。 + * + * @param sheetIndex + * 索引号(以{@code 0}计数) + */ + public void setSheetIndex(int sheetIndex) + { + this.sheetIndex = sheetIndex; + } + + /** + * 是否有名称行。 * * @return */ - public boolean isForceXls() + public boolean hasNameRow() { - return forceXls; + return (this.nameRow > 0); } /** - * 设置是否强制作为xls文件处理,如果为{@code false},则根据文件扩展名判断。 - * - * @param forceXls - */ - public void setForceXls(boolean forceXls) - { - this.forceXls = forceXls; - } - - /** - * 是否有标题行。 + * 获取作为名称行的行号。 * * @return */ - public boolean hasTitleRow() + public int getNameRow() { - return (this.titleRow > 0); + return nameRow; } /** - * 获取作为标题行的行号。 - * - * @return - */ - public int getTitleRow() - { - return titleRow; - } - - /** - * 设置作为标题行的行号。 + * 设置作为名称行的行号。 * * @param titleRow - * 行号,小于{@code 1}则表示无标题行。 + * 行号,小于{@code 1}则表示无名称行。 */ - public void setTitleRow(int titleRow) + public void setNameRow(int titleRow) { - this.titleRow = titleRow; + this.nameRow = titleRow; } public String getDataRowExp() @@ -139,7 +163,7 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im * {@code "1,4,8-15"}:第1、4、8至15行 *

*

- * 标题行({@linkplain #getTitleRow()})将自动被排除。 + * 标题行({@linkplain #getNameRow()})将自动被排除。 *

*

* 注意:行号以{@code 1}开始计数。 @@ -151,18 +175,7 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im public void setDataRowExp(String dataRowExp) { this.dataRowExp = dataRowExp; - - if (!StringUtil.isEmpty(dataRowExp)) - { - try - { - this._dataRowRanges = getRangeExpResolver().resolveIndex(this.dataRowExp); - } - catch (NumberFormatException e) - { - throw new DataSetException(e); - } - } + this._dataRowRanges = getRangeExpResolver().resolveIndex(this.dataRowExp); } public String getDataColumnExp() @@ -187,6 +200,27 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im public void setDataColumnExp(String dataColumnExp) { this.dataColumnExp = dataColumnExp; + this._dataColumnRanges = resolveDataColumnRanges(dataColumnExp); + } + + /** + * 是否强制作为xls文件处理。 + * + * @return + */ + public boolean isForceXls() + { + return forceXls; + } + + /** + * 设置是否强制作为xls文件处理,如果为{@code false},则根据文件扩展名判断。 + * + * @param forceXls + */ + public void setForceXls(boolean forceXls) + { + this.forceXls = forceXls; } @Override @@ -244,9 +278,6 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im protected ResolvedDataSetResult resolveResultForXls(Map paramValues, File file, List properties) throws DataSetException { - @SuppressWarnings("unchecked") - List> data = Collections.EMPTY_LIST; - POIFSFileSystem poifs = null; try @@ -254,41 +285,18 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im poifs = new POIFSFileSystem(file, true); HSSFWorkbook wb = new HSSFWorkbook(poifs.getRoot(), true); - HSSFSheet sheet = wb.getSheetAt(0); + Sheet sheet = wb.getSheetAt(getSheetIndex()); - int rowIdx = 0; - for (Row row : sheet) - { - if (isDataRow(rowIdx)) - { - int cellIdx = 0; - - for (Cell cell : row) - { - if (isDataColumn(cellIdx)) - { - // TODO - } - } - } - - rowIdx++; - } + return resolveResultForSheet(paramValues, sheet, properties); } - catch (DataSetException e) + catch (IOException e) { - throw e; - } - catch (Throwable t) - { - throw new DataSetSourceParseException(t); + throw new DataSetSourceParseException(e); } finally { IOUtil.close(poifs); } - - return new ResolvedDataSetResult(new DataSetResult(data), properties); } /** @@ -304,7 +312,315 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im protected ResolvedDataSetResult resolveResultForXlsx(Map paramValues, File file, List properties) throws DataSetException { - return null; + OPCPackage pkg = null; + XSSFWorkbook wb = null; + + try + { + pkg = OPCPackage.open(file, PackageAccess.READ); + wb = new XSSFWorkbook(pkg); + + Sheet sheet = wb.getSheetAt(getSheetIndex()); + + return resolveResultForSheet(paramValues, sheet, properties); + } + catch (IOException | InvalidFormatException e) + { + throw new DataSetSourceParseException(e); + } + finally + { + IOUtil.close(wb); + IOUtil.close(pkg); + } + } + + /** + * 解析sheet结果。 + * + * @param paramValues + * @param sheet + * @param properties + * 允许为{@code null},此时会自动解析 + * @return + * @throws DataSetException + */ + protected ResolvedDataSetResult resolveResultForSheet(Map paramValues, Sheet sheet, + List properties) throws DataSetException + { + boolean resolveProperties = (properties == null || properties.isEmpty()); + + if (resolveProperties) + properties = new ArrayList<>(); + + List> data = new ArrayList<>(); + + List propertyNames = null; + + try + { + int rowIdx = 0; + int dataRowIdx = 0; + + for (Row row : sheet) + { + if (isNameRow(rowIdx)) + { + if (resolveProperties) + propertyNames = resolveDataSetPropertyNames(row, false); + } + else if (isDataRow(rowIdx)) + { + if (resolveProperties && dataRowIdx == 0 && propertyNames == null) + propertyNames = resolveDataSetPropertyNames(row, true); + + // 名称行不一定在数据行之前,此时可能还无法确定属性名,所以暂时采用列表存储 + List rowObj = new ArrayList<>(); + + int colIdx = 0; + int dataColIdx = 0; + + for (Cell cell : row) + { + if (isDataColumn(colIdx)) + { + DataSetProperty property = null; + + if (!resolveProperties) + { + if (dataColIdx >= properties.size()) + throw new DataSetSourceParseException( + "No property defined for column index " + dataColIdx); + + property = properties.get(dataRowIdx); + } + + Object value = resolvePropertyValue(cell, property); + + if (resolveProperties) + { + property = resolveDataSetProperty(row, rowIdx, dataRowIdx, cell, colIdx, dataColIdx, + value, properties); + } + + rowObj.add(value); + + dataColIdx++; + } + + colIdx++; + } + + data.add(rowObj); + + dataRowIdx++; + } + + rowIdx++; + } + } + catch (DataSetException e) + { + throw e; + } + catch (Throwable t) + { + throw new DataSetSourceParseException(t); + } + + if (resolveProperties) + inflateDataSetProperties(properties, propertyNames); + + DataSetResult result = new DataSetResult(rowListToMap(data, properties)); + + return new ResolvedDataSetResult(result, properties); + } + + @SuppressWarnings("unchecked") + protected List> rowListToMap(List> data, List dataSetProperties) + { + if (data == null) + return Collections.EMPTY_LIST; + + List> maps = new ArrayList<>(data.size()); + + for (List row : data) + { + Map map = new HashMap<>(); + + for (int i = 0; i < row.size(); i++) + { + String name = dataSetProperties.get(i).getName(); + map.put(name, row.get(i)); + } + + maps.add(map); + } + + return maps; + } + + protected void inflateDataSetProperties(List properties, List propertyNames) + { + for (int i = 0; i < properties.size(); i++) + { + DataSetProperty property = properties.get(i); + property.setName(propertyNames.get(i)); + + if (StringUtil.isEmpty(property.getType())) + property.setType(DataSetProperty.DataType.UNKNOWN); + } + } + + /** + * 解析{@linkplain DataSetProperty}并写入{@code properties}。 + * + * @param row + * @param rowIdx + * @param dataRowIdx + * @param cell + * @param colIdx + * @param dataColIdx + * @param cellValue + * @param properties + * @return + */ + protected DataSetProperty resolveDataSetProperty(Row row, int rowIdx, int dataRowIdx, Cell cell, int colIdx, + int dataColIdx, Object cellValue, List properties) + { + DataSetProperty property = null; + + if (dataRowIdx == 0) + { + // 空单元格先不处理数据类型,等待后续有非空单元格再判断 + String dataType = (cellValue == null ? "" : resolveDataType(cellValue)); + + // 名称行不一定在数据行之前,所以此时可能无法确定属性名 + property = new DataSetProperty("should-be-set-later", dataType); + properties.add(property); + } + else + { + property = properties.get(dataColIdx); + + if (StringUtil.isEmpty(property.getType()) && cellValue != null) + property.setType(resolveDataType(cellValue)); + } + + return property; + } + + /** + * 解析属性名。 + * + * @param nameRow + * @return + */ + protected List resolveDataSetPropertyNames(Row nameRow, boolean forceColumnString) + { + List propertyNames = new ArrayList<>(); + + int colIdx = 0; + for (Cell cell : nameRow) + { + if (isDataColumn(colIdx)) + { + String name = null; + + if (forceColumnString) + name = CellReference.convertNumToColString(colIdx); + else + { + try + { + name = cell.getStringCellValue(); + } + catch (Throwable t) + { + } + + if (StringUtil.isEmpty(name)) + name = CellReference.convertNumToColString(colIdx); + } + + propertyNames.add(name); + } + + colIdx++; + } + + return propertyNames; + } + + /** + * 解析单元格属性值。 + * + * @param cell + * @param property + * 允许为{@code null} + * @return + * @throws DataSetSourceParseException + * @throws DataSetException + */ + protected Object resolvePropertyValue(Cell cell, DataSetProperty property) + throws DataSetSourceParseException, DataSetException + { + CellType cellType = cell.getCellTypeEnum(); + + Object cellValue = null; + + try + { + if (CellType.BLANK.equals(cellType)) + { + cellValue = null; + } + else if (CellType.BOOLEAN.equals(cellType)) + { + cellValue = cell.getBooleanCellValue(); + } + else if (CellType.ERROR.equals(cellType)) + { + cellValue = cell.getErrorCellValue(); + } + else if (CellType.FORMULA.equals(cellType)) + { + cellValue = cell.getCellFormula(); + } + else if (CellType.NUMERIC.equals(cellType)) + { + if (DateUtil.isCellDateFormatted(cell)) + cellValue = cell.getDateCellValue(); + else + cellValue = cell.getNumericCellValue(); + } + else if (CellType.STRING.equals(cellType)) + { + cellValue = cell.getStringCellValue(); + } + } + catch (DataSetException e) + { + throw e; + } + catch (Throwable t) + { + throw new DataSetSourceParseException(t); + } + + return cellValue; + } + + /** + * 是否名称行 + * + * @param rowIndex + * 行索引(以{@code 0}计数) + * @return + */ + protected boolean isNameRow(int rowIndex) + { + return ((rowIndex + 1) == this.nameRow); } /** @@ -316,19 +632,13 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im */ protected boolean isDataRow(int rowIndex) { - int row = rowIndex + 1; - - if (hasTitleRow() && row == getTitleRow()) + if (isNameRow(rowIndex)) return false; if (this._dataRowRanges == null || this._dataRowRanges.isEmpty()) return true; - for (int i = 0; i < this._dataRowRanges.size(); i++) - if (this._dataRowRanges.get(i).inRange(row)) - return true; - - return false; + return IndexRange.includes(this._dataRowRanges, rowIndex + 1); } /** @@ -340,8 +650,40 @@ public abstract class AbstractExcelDataSet extends AbstractFmkTemplateDataSet im */ protected boolean isDataColumn(int columnIndex) { - // TODO - return true; + if (this._dataColumnRanges == null || this._dataColumnRanges.isEmpty()) + return true; + + return IndexRange.includes(this._dataColumnRanges, columnIndex); + } + + @SuppressWarnings("unchecked") + protected List resolveDataColumnRanges(String dataColumnExp) throws DataSetException + { + List ranges = getRangeExpResolver().resolve(dataColumnExp); + + if (ranges == null || ranges.isEmpty()) + return Collections.EMPTY_LIST; + + List indexRanges = new ArrayList<>(ranges.size()); + + for (Range range : ranges) + { + int from = 0; + int to = -1; + + String fromStr = range.trimFrom(); + String toStr = range.trimTo(); + + if (!StringUtil.isEmpty(fromStr)) + from = CellReference.convertColStringToIndex(fromStr); + + if (!StringUtil.isEmpty(toStr)) + to = CellReference.convertColStringToIndex(toStr); + + indexRanges.add(new IndexRange(from, to)); + } + + return indexRanges; } /** diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java new file mode 100644 index 00000000..1ec2399c --- /dev/null +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/ExcelDirectoryFileDataSet.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2018 datagear.tech. All Rights Reserved. + */ + +/** + * + */ +package org.datagear.analysis.support; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import org.datagear.analysis.DataSet; +import org.datagear.analysis.DataSetException; +import org.datagear.analysis.DataSetProperty; +import org.datagear.util.FileUtil; + +/** + * 目录内Excel文件{@linkplain DataSet}。 + * + * @author datagear@163.com + * + */ +public class ExcelDirectoryFileDataSet extends AbstractExcelDataSet +{ + /** Excel文件所在的目录 */ + private File directory; + + /** Excel文件名 */ + private String fileName; + + public ExcelDirectoryFileDataSet() + { + super(); + } + + public ExcelDirectoryFileDataSet(String id, String name, File directory, String fileName) + { + super(id, name); + this.directory = directory; + this.fileName = fileName; + } + + /** + * @param id + * @param name + * @param properties + */ + public ExcelDirectoryFileDataSet(String id, String name, List properties, File directory, + String fileName) + { + super(id, name, properties); + this.directory = directory; + this.fileName = fileName; + } + + @Override + protected File getExcelFile(Map paramValues) throws DataSetException + { + File excelFile = FileUtil.getFile(this.directory, this.fileName); + return excelFile; + } +} diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java index f4614ce5..80461752 100644 --- a/datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/JsonDirectoryFileDataSet.java @@ -73,8 +73,7 @@ public class JsonDirectoryFileDataSet extends AbstractJsonFileDataSet @Override protected File getJsonFile(Map paramValues) throws DataSetException { - String fileName = resolveTemplate(this.fileName, paramValues); - File jsonFile = FileUtil.getFile(directory, fileName); + File jsonFile = FileUtil.getFile(this.directory, this.fileName); return jsonFile; } } diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java index a47dc384..2d2ed442 100644 --- a/datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/RangeExpResolver.java @@ -404,12 +404,12 @@ public class RangeExpResolver } /** - * 判断给定索引数值是否在此索引范围内。 + * 是否包含给定索引数值。 * * @param index * @return */ - public boolean inRange(int index) + public boolean includes(int index) { if (this.from > -1 && index < this.from) return false; @@ -452,5 +452,14 @@ public class RangeExpResolver { return getClass().getSimpleName() + " [from=" + from + ", to=" + to + "]"; } + + public static boolean includes(List indexRanges, int index) + { + for (int i = 0; i < indexRanges.size(); i++) + if (indexRanges.get(i).includes(index)) + return true; + + return false; + } } } diff --git a/datagear-analysis/src/main/java/org/datagear/analysis/support/SimpleExcelDataSet.java b/datagear-analysis/src/main/java/org/datagear/analysis/support/SimpleExcelDataSet.java new file mode 100644 index 00000000..f358dc56 --- /dev/null +++ b/datagear-analysis/src/main/java/org/datagear/analysis/support/SimpleExcelDataSet.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018 datagear.tech. All Rights Reserved. + */ + +/** + * + */ +package org.datagear.analysis.support; + +import java.io.File; +import java.util.List; +import java.util.Map; + +import org.datagear.analysis.DataSetException; +import org.datagear.analysis.DataSetProperty; + +/** + * @author datagear@163.com + * + */ +public class SimpleExcelDataSet extends AbstractExcelDataSet +{ + /** Excel文件 */ + private File file; + + public SimpleExcelDataSet() + { + super(); + } + + public SimpleExcelDataSet(String id, String name, File file) + { + super(id, name); + this.file = file; + } + + public SimpleExcelDataSet(String id, String name, List properties, File file) + { + super(id, name, properties); + this.file = file; + } + + @Override + protected File getExcelFile(Map paramValues) throws DataSetException + { + return this.file; + } +} diff --git a/datagear-analysis/src/test/java/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest.java b/datagear-analysis/src/test/java/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest.java new file mode 100644 index 00000000..1af9a5c2 --- /dev/null +++ b/datagear-analysis/src/test/java/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2018 datagear.tech. All Rights Reserved. + */ + +/** + * + */ +package org.datagear.analysis.support; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.datagear.analysis.DataSetProperty; +import org.datagear.analysis.ResolvedDataSetResult; +import org.junit.Test; + +/** + * {@linkplain ExcelDirectoryFileDataSet}单元测试用例。 + * + * @author datagear@163.com + * + */ +public class ExcelDirectoryFileDataSetTest +{ + private static final File DIRECTORY = new File("src/test/resources/org/datagear/analysis/support/"); + + @Test + public void resolveTest_xlsx() + { + ExcelDirectoryFileDataSet dataSet = new ExcelDirectoryFileDataSet("a", "a", DIRECTORY, + "ExcelDirectoryFileDataSetTest-0.xlsx"); + dataSet.setNameRow(1); + + ResolvedDataSetResult resolvedResult = dataSet.resolve(new HashMap<>()); + + @SuppressWarnings("unchecked") + List> data = (List>) resolvedResult.getResult().getData(); + List properties = resolvedResult.getProperties(); + + { + assertEquals(3, data.size()); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + { + Map row = data.get(0); + + assertEquals("aaa", row.get("name")); + assertEquals(15, ((Number) row.get("value")).intValue()); + assertEquals(16, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-01", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(1); + + assertEquals("bbb", row.get("name")); + assertEquals(25, ((Number) row.get("value")).intValue()); + assertEquals(26, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-02", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(2); + + assertEquals("ccc", row.get("name")); + assertEquals(35, ((Number) row.get("value")).intValue()); + assertEquals(36, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-03", dateFormat.format((Date) row.get("date"))); + } + } + + { + assertEquals(4, properties.size()); + + { + DataSetProperty property = properties.get(0); + assertEquals("name", property.getName()); + assertEquals(DataSetProperty.DataType.STRING, property.getType()); + } + + { + DataSetProperty property = properties.get(1); + assertEquals("value", property.getName()); + assertEquals(DataSetProperty.DataType.DECIMAL, property.getType()); + } + + { + DataSetProperty property = properties.get(2); + assertEquals("size", property.getName()); + assertEquals(DataSetProperty.DataType.DECIMAL, property.getType()); + } + + { + DataSetProperty property = properties.get(3); + assertEquals("date", property.getName()); + assertEquals(DataSetProperty.DataType.DATE, property.getType()); + } + } + } + + @Test + public void resolveTest_xls() + { + ExcelDirectoryFileDataSet dataSet = new ExcelDirectoryFileDataSet("a", "a", DIRECTORY, + "ExcelDirectoryFileDataSetTest-1.xls"); + dataSet.setNameRow(1); + + ResolvedDataSetResult resolvedResult = dataSet.resolve(new HashMap<>()); + + @SuppressWarnings("unchecked") + List> data = (List>) resolvedResult.getResult().getData(); + List properties = resolvedResult.getProperties(); + + { + assertEquals(3, data.size()); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + { + Map row = data.get(0); + + assertEquals("aaa", row.get("name")); + assertEquals(15, ((Number) row.get("value")).intValue()); + assertEquals(16, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-01", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(1); + + assertEquals("bbb", row.get("name")); + assertEquals(25, ((Number) row.get("value")).intValue()); + assertEquals(26, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-02", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(2); + + assertEquals("ccc", row.get("name")); + assertEquals(35, ((Number) row.get("value")).intValue()); + assertEquals(36, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-03", dateFormat.format((Date) row.get("date"))); + } + } + + { + assertEquals(4, properties.size()); + + { + DataSetProperty property = properties.get(0); + assertEquals("name", property.getName()); + assertEquals(DataSetProperty.DataType.STRING, property.getType()); + } + + { + DataSetProperty property = properties.get(1); + assertEquals("value", property.getName()); + assertEquals(DataSetProperty.DataType.DECIMAL, property.getType()); + } + + { + DataSetProperty property = properties.get(2); + assertEquals("size", property.getName()); + assertEquals(DataSetProperty.DataType.DECIMAL, property.getType()); + } + + { + DataSetProperty property = properties.get(3); + assertEquals("date", property.getName()); + assertEquals(DataSetProperty.DataType.DATE, property.getType()); + } + } + } + + @Test + public void resolveTest_dataRowColumnExp() + { + ExcelDirectoryFileDataSet dataSet = new ExcelDirectoryFileDataSet("a", "a", DIRECTORY, + "ExcelDirectoryFileDataSetTest-0.xlsx"); + dataSet.setNameRow(1); + dataSet.setDataRowExp("2,3-"); + dataSet.setDataColumnExp("A,C-"); + + ResolvedDataSetResult resolvedResult = dataSet.resolve(new HashMap<>()); + + @SuppressWarnings("unchecked") + List> data = (List>) resolvedResult.getResult().getData(); + List properties = resolvedResult.getProperties(); + + { + assertEquals(3, data.size()); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + { + Map row = data.get(0); + + assertEquals("aaa", row.get("name")); + assertEquals(16, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-01", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(1); + + assertEquals("bbb", row.get("name")); + assertEquals(26, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-02", dateFormat.format((Date) row.get("date"))); + } + + { + Map row = data.get(2); + + assertEquals("ccc", row.get("name")); + assertEquals(36, ((Number) row.get("size")).intValue()); + assertEquals("2020-08-03", dateFormat.format((Date) row.get("date"))); + } + } + + { + assertEquals(3, properties.size()); + + { + DataSetProperty property = properties.get(0); + assertEquals("name", property.getName()); + assertEquals(DataSetProperty.DataType.STRING, property.getType()); + } + + { + DataSetProperty property = properties.get(1); + assertEquals("size", property.getName()); + assertEquals(DataSetProperty.DataType.DECIMAL, property.getType()); + } + + { + DataSetProperty property = properties.get(2); + assertEquals("date", property.getName()); + assertEquals(DataSetProperty.DataType.DATE, property.getType()); + } + } + } +} diff --git a/datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-0.xlsx b/datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-0.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5fa938a11666c1a6c591c36aee7f2a79c2db852f GIT binary patch literal 8434 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 literal 0 HcmV?d00001 diff --git a/datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-1.xls b/datagear-analysis/src/test/resources/org/datagear/analysis/support/ExcelDirectoryFileDataSetTest-1.xls new file mode 100644 index 0000000000000000000000000000000000000000..a6d3974f6c4231022f045574ea2835ac7d1a588d GIT binary patch literal 25600 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