From cc72e8d61674bd8c306a1bde0f553a1b612d7aa0 Mon Sep 17 00:00:00 2001 From: Marten Deinum Date: Wed, 7 Aug 2024 14:11:52 +0200 Subject: [PATCH] Fix IsoFormattingDateDataFormatter The format used the ISO_OFFSET_DATE_TIME is the wrong one and leads to errors formatting the date. Instead we should have used the ISO_LOCAL_DATE_TIME instead. This commit fixes that and also includes a bug fix for the XLSX variant as that didn't use the overridden method. The correct method for this has been overridden now also formatting the dates/times for that using the ISO_LOCAL_DATE_TIME format. Tests for different types have been added as well, as to make sure this keeps working for newer versions. Closes: #121 --- .../excel/IsoFormattingDateDataFormatter.java | 13 +++- .../excel/poi/PoiItemReaderTypesTest.java | 69 ++++++++++++++++++ .../streaming/StreamingXlsxTypesTest.java | 68 +++++++++++++++++ .../src/test/resources/types.xls | Bin 0 -> 26624 bytes .../src/test/resources/types.xlsx | Bin 0 -> 9241 bytes 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/poi/PoiItemReaderTypesTest.java create mode 100644 spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxTypesTest.java create mode 100644 spring-batch-excel/src/test/resources/types.xls create mode 100644 spring-batch-excel/src/test/resources/types.xlsx diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java index 3a83e4c..4cb48a0 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java @@ -28,7 +28,7 @@ import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.FormulaEvaluator; /** - * Specialized subclass for additionally formatting the date into an ISO date/time. + * Specialized subclass for formatting the date into an ISO date/time and ignore the format as given in the Excel file. * * @author Marten Deinum * @@ -44,6 +44,15 @@ public class IsoFormattingDateDataFormatter extends DataFormatter { super(locale); } + @Override + public String formatRawCellContents(double value, int formatIndex, String formatString, boolean use1904Windowing) { + if (DateUtil.isADateFormat(formatIndex, formatString) && DateUtil.isValidExcelDate(value)) { + return super.formatRawCellContents(value, formatIndex, "yyyy-MM-ddTHH:mm:ss", + use1904Windowing); + } + return super.formatRawCellContents(value, formatIndex, formatString, use1904Windowing); + } + @Override public String formatCellValue(Cell cell, FormulaEvaluator evaluator, ConditionalFormattingEvaluator cfEvaluator) { if (cell == null) { @@ -60,7 +69,7 @@ public class IsoFormattingDateDataFormatter extends DataFormatter { if (cellType == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell, cfEvaluator)) { LocalDateTime value = cell.getLocalDateTimeCellValue(); - return (value != null) ? value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) : ""; + return (value != null) ? value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : ""; } return super.formatCellValue(cell, evaluator, cfEvaluator); } diff --git a/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/poi/PoiItemReaderTypesTest.java b/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/poi/PoiItemReaderTypesTest.java new file mode 100644 index 0000000..42cfdae --- /dev/null +++ b/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/poi/PoiItemReaderTypesTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.batch.extensions.excel.poi; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.extensions.excel.mapping.PassThroughRowMapper; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class PoiItemReaderTypesTest { + + @Test + public void shouldBeAbleToReadMultipleTypes() throws Exception { + var reader = new PoiItemReader(); + reader.setResource(new ClassPathResource("types.xls")); + reader.setRowMapper(new PassThroughRowMapper()); + reader.setLinesToSkip(1); // Skip header + reader.setUserLocale(Locale.US); // Use a Locale to not be dependent on environment + reader.afterPropertiesSet(); + + + reader.open(new ExecutionContext()); + + var row1 = reader.read(); + var row2 = reader.read(); + assertThat(row1).containsExactly("1", "1.0", "5/12/24", "13:14:55", "5/12/24 13:14", "hello world"); + assertThat(row2).containsExactly("2", "2.5", "8/8/23", "11:12:13", "8/8/23 11:12", "world hello"); + + } + + @Test + public void shouldBeAbleToReadMultipleTypesWithDatesAsIso() throws Exception { + var reader = new PoiItemReader(); + reader.setResource(new ClassPathResource("types.xls")); + reader.setRowMapper(new PassThroughRowMapper()); + reader.setLinesToSkip(1); // Skip header + reader.setUserLocale(Locale.US); // Use a Locale to not be dependent on environment + reader.setDatesAsIso(true); + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + + var row1 = reader.read(); + var row2 = reader.read(); + assertThat(row1).containsExactly("1", "1.0", "2024-05-12T00:00:00", "1899-12-31T13:14:55", "2024-05-12T13:14:55", "hello world"); + assertThat(row2).containsExactly("2", "2.5", "2023-08-08T00:00:00", "1899-12-31T11:12:13", "2023-08-08T11:12:13", "world hello"); + + } +} diff --git a/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxTypesTest.java b/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxTypesTest.java new file mode 100644 index 0000000..948f0e2 --- /dev/null +++ b/spring-batch-excel/src/test/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxTypesTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.batch.extensions.excel.streaming; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.extensions.excel.mapping.PassThroughRowMapper; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StreamingXlsxTypesTest { + + @Test + public void shouldBeAbleToReadMultipleTypes() throws Exception { + var reader = new StreamingXlsxItemReader(); + reader.setResource(new ClassPathResource("types.xlsx")); + reader.setRowMapper(new PassThroughRowMapper()); + reader.setLinesToSkip(1); // Skip header + reader.setUserLocale(Locale.US); // Use a Locale to not be dependent on environment + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + + var row1 = reader.read(); + var row2 = reader.read(); + assertThat(row1).containsExactly("1", "1.0", "5/12/24", "13:14:55", "5/12/24 13:14", "hello world"); + assertThat(row2).containsExactly("2", "2.5", "8/8/23", "11:12:13", "8/8/23 11:12", "world hello"); + + } + + @Test + public void shouldBeAbleToReadMultipleTypesWithDatesAsIso() throws Exception { + var reader = new StreamingXlsxItemReader(); + reader.setResource(new ClassPathResource("types.xlsx")); + reader.setRowMapper(new PassThroughRowMapper()); + reader.setLinesToSkip(1); // Skip header + reader.setUserLocale(Locale.US); // Use a Locale to not be dependent on environment + reader.setDatesAsIso(true); + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + + var row1 = reader.read(); + var row2 = reader.read(); + assertThat(row1).containsExactly("1", "1.0", "2024-05-12T00:00:00", "1899-12-31T13:14:55", "2024-05-12T13:14:55", "hello world"); + assertThat(row2).containsExactly("2", "2.5", "2023-08-08T00:00:00", "1899-12-31T11:12:13", "2023-08-08T11:12:13", "world hello"); + + } + +} diff --git a/spring-batch-excel/src/test/resources/types.xls b/spring-batch-excel/src/test/resources/types.xls new file mode 100644 index 0000000000000000000000000000000000000000..83b0c3fe8e2516fe99a4682792afb4b30cea4bac GIT binary patch literal 26624 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 WAcFv1T>t