diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java index f17d9b62c..3f745d745 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java @@ -78,6 +78,9 @@ import org.apache.fesod.sheet.converters.string.StringNumberConverter; import org.apache.fesod.sheet.converters.string.StringStringConverter; import org.apache.fesod.sheet.converters.url.UrlImageConverter; +import org.apache.fesod.sheet.converters.yearmonth.YearMonthDateConverter; +import org.apache.fesod.sheet.converters.yearmonth.YearMonthNumberConverter; +import org.apache.fesod.sheet.converters.yearmonth.YearMonthStringConverter; /** * Load default handler @@ -114,6 +117,9 @@ private static void initAllConverter() { putAllConverter(new DateNumberConverter()); putAllConverter(new DateStringConverter()); + putAllConverter(new YearMonthNumberConverter()); + putAllConverter(new YearMonthStringConverter()); + putAllConverter(new LocalDateNumberConverter()); putAllConverter(new LocalDateStringConverter()); @@ -157,6 +163,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new BooleanBooleanConverter()); putWriteConverter(new ByteNumberConverter()); putWriteConverter(new DateDateConverter()); + putWriteConverter(new YearMonthDateConverter()); putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateDateConverter()); putWriteConverter(new LocalTimeDateConverter()); @@ -178,6 +185,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new BooleanStringConverter()); putWriteStringConverter(new ByteStringConverter()); putWriteStringConverter(new DateStringConverter()); + putWriteStringConverter(new YearMonthStringConverter()); putWriteStringConverter(new LocalDateStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter()); putWriteStringConverter(new LocalTimeStringConverter()); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDateConverter.java new file mode 100644 index 000000000..8110370b4 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDateConverter.java @@ -0,0 +1,61 @@ +/* + * 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.fesod.sheet.converters.yearmonth; + +import java.time.YearMonth; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.WorkBookUtil; + +/** + * YearMonth and date converter + * + *

Excel has no month type, so a month is written as a date cell of the first day of that month, formatted with + * {@link #DEFAULT_FORMAT}, which keeps sorting and date arithmetic in the sheet working. + * + */ +public class YearMonthDateConverter implements Converter { + + /** + * Default format of a month cell + */ + public static final String DEFAULT_FORMAT = "yyyy-MM"; + + @Override + public Class supportJavaTypeKey() { + return YearMonth.class; + } + + @Override + public WriteCellData convertToExcelData( + YearMonth value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) + throws Exception { + WriteCellData cellData = + new WriteCellData<>(value == null ? null : value.atDay(1).atStartOfDay()); + String format = null; + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + format = contentProperty.getDateTimeFormatProperty().getFormat(); + } + WorkBookUtil.fillDataFormat(cellData, format == null || format.isEmpty() ? null : format, DEFAULT_FORMAT); + return cellData; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthNumberConverter.java new file mode 100644 index 000000000..4391f7994 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthNumberConverter.java @@ -0,0 +1,66 @@ +/* + * 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.fesod.sheet.converters.yearmonth; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.YearMonth; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.util.DateUtils; +import org.apache.poi.ss.usermodel.DateUtil; + +/** + * YearMonth and number converter + * + *

A month is read from and written to the first day of that month, so the value stays a real date in the sheet. + * + */ +public class YearMonthNumberConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return YearMonth.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.NUMBER; + } + + @Override + public YearMonth convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + LocalDate localDate = DateUtils.getLocalDate( + cellData.getNumberValue().doubleValue(), DateUtils.isDate1904(contentProperty, globalConfiguration)); + return YearMonth.from(localDate); + } + + @Override + public WriteCellData convertToExcelData( + YearMonth value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(BigDecimal.valueOf(DateUtil.getExcelDate( + value.atDay(1).atStartOfDay(), DateUtils.isDate1904(contentProperty, globalConfiguration)))); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthStringConverter.java new file mode 100644 index 000000000..ed55b1dd2 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthStringConverter.java @@ -0,0 +1,90 @@ +/* + * 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.fesod.sheet.converters.yearmonth; + +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import org.apache.fesod.common.util.StringUtils; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; + +/** + * YearMonth and string converter + * + */ +public class YearMonthStringConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return YearMonth.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public YearMonth convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return YearMonth.parse(cellData.getStringValue(), formatter(contentProperty, globalConfiguration)); + } + + @Override + public WriteCellData convertToExcelData( + YearMonth value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>(value.format(formatter(contentProperty, globalConfiguration))); + } + + /** + * The formatter of the field, {@link YearMonthDateConverter#DEFAULT_FORMAT} when no format is set. + * + *

{@code @DateTimeFormat} defaults to an empty value and an empty pattern is not a usable one, so an empty + * format is handled like a missing one. The locale of the read or write is used, so a month name in the + * configured language can be parsed and written. + * + * @param contentProperty the property of the field + * @param globalConfiguration the configuration of the read or write, which carries the locale + * @return the formatter to use + */ + private static DateTimeFormatter formatter( + ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + String format = null; + if (contentProperty != null + && contentProperty.getDateTimeFormatProperty() != null + && StringUtils.isNotBlank( + contentProperty.getDateTimeFormatProperty().getFormat())) { + format = contentProperty.getDateTimeFormatProperty().getFormat(); + } + if (format == null) { + format = YearMonthDateConverter.DEFAULT_FORMAT; + } + Locale locale = globalConfiguration == null ? null : globalConfiguration.getLocale(); + if (locale == null) { + return DateTimeFormatter.ofPattern(format); + } + return DateTimeFormatter.ofPattern(format, locale); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDataTest.java new file mode 100644 index 000000000..d21f8add7 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/yearmonth/YearMonthDataTest.java @@ -0,0 +1,161 @@ +/* + * 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. + */ + +/* + * This file is part of the Apache Fesod (Incubating) project, which was derived from Alibaba EasyExcel. + * + * Copyright (C) 2018-2024 Alibaba Group Holding Ltd. + */ + +package org.apache.fesod.sheet.converters.yearmonth; + +import java.io.File; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.support.ExcelTypeEnum; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.testkit.listeners.CollectingReadListener; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; + +/** + * Round trip tests for the java.time.YearMonth converters. + */ +@Tag(Tags.ROUND_TRIP) +public class YearMonthDataTest extends AbstractExcelTest { + + /** + * A month name is written and parsed with the locale of the read or write, so the same French pattern is used on + * both sides. + */ + private static final DateTimeFormatter FRENCH_MONTH = DateTimeFormatter.ofPattern("MMM yyyy", Locale.FRENCH); + + @ParameterizedTest + @ExcelFormatSource + void readAndWrite(ExcelFormat format) throws Exception { + File file = createTempFile(format); + FesodSheet.write(file, YearMonthData.class).sheet().doWrite(data()); + + CollectingReadListener listener = new CollectingReadListener<>(); + FesodSheet.read(file, YearMonthData.class, listener).sheet().doRead(); + + Assertions.assertEquals(1, listener.getRowCount()); + YearMonthData row = listener.getFirstRow(); + Assertions.assertEquals(YearMonth.of(2020, 1), row.getMonth()); + Assertions.assertEquals(YearMonth.of(2020, 1), row.getMonthString()); + Assertions.assertEquals(YearMonth.of(2021, 12), row.getMonthFormattedString()); + } + + /** + * {@code @DateTimeFormat} defaults to an empty value and an empty pattern is not a usable one, so an empty format + * has to behave like a missing one. + */ + @ParameterizedTest + @ExcelFormatSource + void emptyDateTimeFormat(ExcelFormat format) throws Exception { + File file = createTempFile(format); + FesodSheet.write(file, YearMonthData.class).sheet().doWrite(data()); + + CollectingReadListener listener = new CollectingReadListener<>(); + FesodSheet.read(file, YearMonthData.class, listener).sheet().doRead(); + + Assertions.assertEquals(1, listener.getRowCount()); + Assertions.assertEquals(YearMonth.of(2020, 1), listener.getFirstRow().getMonthEmptyFormat()); + } + + /** + * The locale of the read or write decides how a month name is written and parsed. The cells of a csv file are + * always strings, so the written text can be checked there as well. + */ + @ParameterizedTest + @ExcelFormatSource + void localeOfTheConfigurationIsUsed(ExcelFormat format) throws Exception { + File file = createTempFile(format); + FesodSheet.write(file, YearMonthData.class) + .locale(Locale.FRENCH) + .sheet() + .doWrite(data()); + + if (format.toExcelTypeEnum() == ExcelTypeEnum.CSV) { + List> raw = + FesodSheet.read(file).headRowNumber(0).sheet().doReadSync(); + // The column order follows the field order of the model, monthLocale is the fifth one + Assertions.assertEquals( + FRENCH_MONTH.format(YearMonth.of(2020, 1)), raw.get(1).get(4)); + } + + CollectingReadListener listener = new CollectingReadListener<>(); + FesodSheet.read(file, YearMonthData.class, listener) + .locale(Locale.FRENCH) + .sheet() + .doRead(); + + Assertions.assertEquals(1, listener.getRowCount()); + Assertions.assertEquals(YearMonth.of(2020, 1), listener.getFirstRow().getMonthLocale()); + } + + private List data() { + List list = new ArrayList(); + YearMonthData data = new YearMonthData(); + data.setMonth(YearMonth.of(2020, 1)); + data.setMonthString(YearMonth.of(2020, 1)); + data.setMonthFormattedString(YearMonth.of(2021, 12)); + data.setMonthEmptyFormat(YearMonth.of(2020, 1)); + data.setMonthLocale(YearMonth.of(2020, 1)); + list.add(data); + return list; + } + + @Getter + @Setter + @EqualsAndHashCode + public static class YearMonthData { + @ExcelProperty("month") + private YearMonth month; + + @ExcelProperty("monthString") + private YearMonth monthString; + + @ExcelProperty("monthFormattedString") + @DateTimeFormat("yyyy/MM") + private YearMonth monthFormattedString; + + @ExcelProperty("monthEmptyFormat") + @DateTimeFormat("") + private YearMonth monthEmptyFormat; + + @ExcelProperty("monthLocale") + @DateTimeFormat("MMM yyyy") + private YearMonth monthLocale; + } +}