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 88b90d955..f17d9b62c 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 @@ -64,6 +64,9 @@ import org.apache.fesod.sheet.converters.localdatetime.LocalDateTimeDateConverter; import org.apache.fesod.sheet.converters.localdatetime.LocalDateTimeNumberConverter; import org.apache.fesod.sheet.converters.localdatetime.LocalDateTimeStringConverter; +import org.apache.fesod.sheet.converters.localtime.LocalTimeDateConverter; +import org.apache.fesod.sheet.converters.localtime.LocalTimeNumberConverter; +import org.apache.fesod.sheet.converters.localtime.LocalTimeStringConverter; import org.apache.fesod.sheet.converters.longconverter.LongBooleanConverter; import org.apache.fesod.sheet.converters.longconverter.LongNumberConverter; import org.apache.fesod.sheet.converters.longconverter.LongStringConverter; @@ -117,6 +120,9 @@ private static void initAllConverter() { putAllConverter(new LocalDateTimeNumberConverter()); putAllConverter(new LocalDateTimeStringConverter()); + putAllConverter(new LocalTimeNumberConverter()); + putAllConverter(new LocalTimeStringConverter()); + putAllConverter(new DoubleBooleanConverter()); putAllConverter(new DoubleNumberConverter()); putAllConverter(new DoubleStringConverter()); @@ -153,6 +159,7 @@ private static void initDefaultWriteConverter() { putWriteConverter(new DateDateConverter()); putWriteConverter(new LocalDateTimeDateConverter()); putWriteConverter(new LocalDateDateConverter()); + putWriteConverter(new LocalTimeDateConverter()); putWriteConverter(new DoubleNumberConverter()); putWriteConverter(new FloatNumberConverter()); putWriteConverter(new IntegerNumberConverter()); @@ -173,6 +180,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new DateStringConverter()); putWriteStringConverter(new LocalDateStringConverter()); putWriteStringConverter(new LocalDateTimeStringConverter()); + putWriteStringConverter(new LocalTimeStringConverter()); putWriteStringConverter(new DoubleStringConverter()); putWriteStringConverter(new FloatStringConverter()); putWriteStringConverter(new IntegerStringConverter()); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverter.java new file mode 100644 index 000000000..9bb505f8a --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverter.java @@ -0,0 +1,54 @@ +/* + * 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.localtime; + +import java.time.LocalDateTime; +import java.time.LocalTime; +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.DateUtils; +import org.apache.fesod.sheet.util.WorkBookUtil; + +/** + * LocalTime and date converter + */ +public class LocalTimeDateConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return LocalTime.class; + } + + @Override + public WriteCellData convertToExcelData( + LocalTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) + throws Exception { + LocalDateTime localDateTime = value == null ? null : value.atDate(DateUtils.EPOCH); + WriteCellData cellData = new WriteCellData<>(localDateTime); + String format = null; + if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) { + format = contentProperty.getDateTimeFormatProperty().getFormat(); + } + WorkBookUtil.fillDataFormat( + cellData, format == null || format.isEmpty() ? null : format, DateUtils.DEFAULT_LOCAL_TIME_FORMAT); + return cellData; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverter.java new file mode 100644 index 000000000..df1b80df0 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverter.java @@ -0,0 +1,73 @@ +/* + * 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.localtime; + +import java.math.BigDecimal; +import java.time.LocalTime; +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; + +/** + * LocalTime and number converter + */ +public class LocalTimeNumberConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return LocalTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.NUMBER; + } + + @Override + public LocalTime convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return DateUtils.getLocalTime( + cellData.getNumberValue().doubleValue(), globalConfiguration.getUse1904windowing()); + } else { + return DateUtils.getLocalTime( + cellData.getNumberValue().doubleValue(), + contentProperty.getDateTimeFormatProperty().getUse1904windowing()); + } + } + + @Override + public WriteCellData convertToExcelData( + LocalTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return new WriteCellData<>(BigDecimal.valueOf( + DateUtil.getExcelDate(value.atDate(DateUtils.EPOCH), globalConfiguration.getUse1904windowing()))); + } else { + return new WriteCellData<>(BigDecimal.valueOf(DateUtil.getExcelDate( + value.atDate(DateUtils.EPOCH), + contentProperty.getDateTimeFormatProperty().getUse1904windowing()))); + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverter.java new file mode 100644 index 000000000..1a25405dc --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverter.java @@ -0,0 +1,68 @@ +/* + * 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.localtime; + +import java.time.LocalTime; +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; + +/** + * LocalTime and string converter + */ +public class LocalTimeStringConverter implements Converter { + @Override + public Class supportJavaTypeKey() { + return LocalTime.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public LocalTime convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return DateUtils.parseLocalTime(cellData.getStringValue(), null, globalConfiguration.getLocale()); + } else { + return DateUtils.parseLocalTime( + cellData.getStringValue(), + contentProperty.getDateTimeFormatProperty().getFormat(), + globalConfiguration.getLocale()); + } + } + + @Override + public WriteCellData convertToExcelData( + LocalTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + if (contentProperty == null || contentProperty.getDateTimeFormatProperty() == null) { + return new WriteCellData<>(DateUtils.format(value, null, globalConfiguration.getLocale())); + } else { + return new WriteCellData<>(DateUtils.format( + value, contentProperty.getDateTimeFormatProperty().getFormat(), globalConfiguration.getLocale())); + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java index 895f6dcec..58425caab 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java @@ -31,6 +31,7 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; @@ -100,12 +101,16 @@ public class DateUtils { public static final String DATE_FORMAT_17 = "yyyyMMdd HH:mm:ss"; public static final String DATE_FORMAT_19 = "yyyy-MM-dd HH:mm:ss"; public static final String DATE_FORMAT_19_FORWARD_SLASH = "yyyy/MM/dd HH:mm:ss"; + public static final String TIME_FORMAT_5 = "HH:mm"; + public static final String TIME_FORMAT_8 = "HH:mm:ss"; private static final String MINUS = "-"; public static String defaultDateFormat = DATE_FORMAT_19; public static String defaultLocalDateFormat = DATE_FORMAT_10; + public static final String DEFAULT_LOCAL_TIME_FORMAT = TIME_FORMAT_8; + public static final int SECONDS_PER_MINUTE = 60; public static final int MINUTES_PER_HOUR = 60; public static final int HOURS_PER_DAY = 24; @@ -162,6 +167,21 @@ public static LocalDate parseLocalDate(String dateString, String dateFormat, Loc return LocalDate.parse(dateString, getCacheDateTimeFormat(dateFormat, local)); } + /** + * convert string to time + * + * @param timeString + * @param timeFormat + * @param local + * @return + */ + public static LocalTime parseLocalTime(String timeString, String timeFormat, Locale local) { + if (StringUtils.isEmpty(timeFormat)) { + timeFormat = switchTimeFormat(timeString); + } + return LocalTime.parse(timeString, getCacheDateTimeFormat(timeFormat, local)); + } + /** * convert string to date * @@ -205,6 +225,24 @@ public static String switchDateFormat(String dateString) { } } + /** + * switch time-only format + * + * @param timeString + * @return + */ + public static String switchTimeFormat(String timeString) { + int length = timeString.length(); + switch (length) { + case 8: + return TIME_FORMAT_8; + case 5: + return TIME_FORMAT_5; + default: + throw new IllegalArgumentException("can not find time format for:" + timeString); + } + } + /** * Format date *

@@ -279,6 +317,35 @@ public static String format(LocalDate date, String dateFormat, Locale local) { return date.format(getCacheDateTimeFormat(dateFormat, local)); } + /** + * Format time + * + * @param time LocalTime + * @param timeFormat time format + * @return format string + */ + public static String format(LocalTime time, String timeFormat) { + return format(time, timeFormat, null); + } + + /** + * Format time + * + * @param time LocalTime + * @param timeFormat time format + * @param local local + * @return format string + */ + public static String format(LocalTime time, String timeFormat, Locale local) { + if (time == null) { + return null; + } + if (StringUtils.isEmpty(timeFormat)) { + timeFormat = DEFAULT_LOCAL_TIME_FORMAT; + } + return time.format(getCacheDateTimeFormat(timeFormat, local)); + } + /** * Format date * @@ -458,6 +525,24 @@ public static LocalDate getLocalDate(double date, boolean use1904windowing) { return localDateTime == null ? null : localDateTime.toLocalDate(); } + /** + * Given an Excel date with either 1900 or 1904 date windowing, + * converts it to a java.time.LocalTime. + * + * Excel Dates and Times are stored without any timezone + * information. The date component is discarded; only the + * wall-clock time of day is returned. + * + * @param date The Excel date. + * @param use1904windowing true if date uses 1904 windowing, + * or false if using 1900 date windowing. + * @return Java representation of the time, or null if date is not a valid Excel date + */ + public static LocalTime getLocalTime(double date, boolean use1904windowing) { + LocalDateTime localDateTime = getLocalDateTime(date, use1904windowing); + return localDateTime == null ? null : localDateTime.toLocalTime(); + } + /** * Determine if it is a date format. * diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/ConverterDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/ConverterDataTest.java index 1222641a1..77eda9139 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/ConverterDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/ConverterDataTest.java @@ -66,6 +66,7 @@ void readAndWrite(ExcelFormat format) throws Exception { Assertions.assertEquals(TestUtil.TEST_DATE, row.getDate()); Assertions.assertEquals(TestUtil.TEST_LOCAL_DATE, row.getLocalDate()); Assertions.assertEquals(TestUtil.TEST_LOCAL_DATE_TIME, row.getLocalDateTime()); + Assertions.assertEquals(TestUtil.TEST_LOCAL_TIME, row.getLocalTime()); Assertions.assertEquals(Boolean.TRUE, row.getBooleanData()); Assertions.assertEquals(row.getBigDecimal().doubleValue(), BigDecimal.ONE.doubleValue(), 0.0); Assertions.assertEquals(row.getBigInteger().intValue(), BigInteger.ONE.intValue(), 0.0); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java index e05837b86..a30708a52 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java @@ -19,8 +19,13 @@ package org.apache.fesod.sheet.converters; +import java.time.LocalTime; import java.util.Map; import org.apache.fesod.sheet.converters.ConverterKeyBuild.ConverterKey; +import org.apache.fesod.sheet.converters.localtime.LocalTimeDateConverter; +import org.apache.fesod.sheet.converters.localtime.LocalTimeNumberConverter; +import org.apache.fesod.sheet.converters.localtime.LocalTimeStringConverter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -44,6 +49,24 @@ void loadAllConverterIsImmutableAndCopyIsMutable() { DefaultConverterLoader.loadAllConverter(), DefaultConverterLoader.copyAllConverter()); } + @Test + void loadConvertersRegistersLocalTimeFamily() { + Map> allConverter = DefaultConverterLoader.loadAllConverter(); + Assertions.assertInstanceOf( + LocalTimeNumberConverter.class, + allConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.NUMBER))); + Assertions.assertInstanceOf( + LocalTimeStringConverter.class, + allConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.STRING))); + + Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); + Assertions.assertInstanceOf( + LocalTimeDateConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class))); + Assertions.assertInstanceOf( + LocalTimeStringConverter.class, + writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.STRING))); + } + private static void assertLoadIsImmutableAndCopyIsMutable( Map> loaded, Map> copy) { Map.Entry> entry = diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverterTest.java new file mode 100644 index 000000000..436a22e3b --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeDateConverterTest.java @@ -0,0 +1,115 @@ +/* + * 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.localtime; + +import java.time.LocalTime; +import java.util.stream.Stream; +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.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.util.DateUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link LocalTimeDateConverter}. + */ +@Tag(Tags.UNIT) +class LocalTimeDateConverterTest { + + private static final GlobalConfiguration GLOBAL_CONFIGURATION = new GlobalConfiguration(); + private final LocalTimeDateConverter converter = new LocalTimeDateConverter(); + + @AfterEach + void tearDown() { + DateUtils.removeThreadLocalCache(); + } + + @Test + void supportJavaTypeKey() { + Assertions.assertEquals(LocalTime.class, converter.supportJavaTypeKey()); + } + + @Test + void convertToJavaDataIsUnsupported() { + Assertions.assertThrows( + UnsupportedOperationException.class, + () -> converter.convertToJavaData(new ReadCellData<>("01:01:01"), null, GLOBAL_CONFIGURATION)); + } + + @ParameterizedTest + @MethodSource("sampleTimes") + void convertToExcelDataUsesEpochDateAndDefaultFormat(LocalTime time) throws Exception { + WriteCellData cellData = converter.convertToExcelData(time, null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(CellDataTypeEnum.DATE, cellData.getType()); + Assertions.assertEquals(time.atDate(DateUtils.EPOCH), cellData.getDateValue()); + Assertions.assertEquals( + DateUtils.DEFAULT_LOCAL_TIME_FORMAT, + cellData.getWriteCellStyle().getDataFormatData().getFormat()); + } + + @Test + void convertToExcelDataFallsBackToDefaultFormatWhenContentPropertyHasNoDateTimeFormat() throws Exception { + WriteCellData cellData = + converter.convertToExcelData(LocalTime.NOON, new ExcelContentProperty(), GLOBAL_CONFIGURATION); + + Assertions.assertEquals( + DateUtils.DEFAULT_LOCAL_TIME_FORMAT, + cellData.getWriteCellStyle().getDataFormatData().getFormat()); + } + + @Test + void convertToExcelDataUsesCustomFormat() throws Exception { + ExcelContentProperty contentProperty = contentProperty(DateUtils.TIME_FORMAT_5, Boolean.FALSE); + + WriteCellData cellData = + converter.convertToExcelData(LocalTime.of(1, 1, 1), contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(1, 1, 1).atDate(DateUtils.EPOCH), cellData.getDateValue()); + Assertions.assertEquals( + DateUtils.TIME_FORMAT_5, + cellData.getWriteCellStyle().getDataFormatData().getFormat()); + } + + @Test + void convertToExcelDataRejectsNullValue() { + Assertions.assertThrows( + IllegalArgumentException.class, () -> converter.convertToExcelData(null, null, GLOBAL_CONFIGURATION)); + } + + static Stream sampleTimes() { + return Stream.of(LocalTime.MIDNIGHT, LocalTime.NOON, LocalTime.of(1, 1, 1), LocalTime.of(23, 59, 59)); + } + + private static ExcelContentProperty contentProperty(String format, Boolean use1904windowing) { + ExcelContentProperty contentProperty = new ExcelContentProperty(); + contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(format, use1904windowing)); + return contentProperty; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverterTest.java new file mode 100644 index 000000000..13628db03 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeNumberConverterTest.java @@ -0,0 +1,138 @@ +/* + * 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.localtime; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.stream.Stream; +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.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.util.DateUtils; +import org.apache.poi.ss.usermodel.DateUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link LocalTimeNumberConverter}. + */ +@Tag(Tags.UNIT) +class LocalTimeNumberConverterTest { + + private static final GlobalConfiguration GLOBAL_CONFIGURATION = new GlobalConfiguration(); + private final LocalTimeNumberConverter converter = new LocalTimeNumberConverter(); + + @AfterEach + void tearDown() { + DateUtils.removeThreadLocalCache(); + } + + @Test + void supportKeys() { + Assertions.assertEquals(LocalTime.class, converter.supportJavaTypeKey()); + Assertions.assertEquals(CellDataTypeEnum.NUMBER, converter.supportExcelTypeKey()); + } + + @ParameterizedTest + @CsvSource({"0.5, 12:00:00", "1.0, 00:00:00", "43831.5, 12:00:00"}) + void convertToJavaDataReadsSerialAndDropsDate(double serial, String expected) { + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(serial)), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.parse(expected), actual); + } + + @Test + void convertToJavaDataDropsDateFromFullDatetimeSerial() { + double serial = DateUtil.getExcelDate(LocalTime.of(1, 1, 1).atDate(LocalDate.of(2020, 1, 1)), false); + + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>(BigDecimal.valueOf(serial)), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(1, 1, 1), actual); + } + + @ParameterizedTest + @MethodSource("sampleTimes") + void convertToExcelDataRoundTrip(LocalTime time) { + WriteCellData written = converter.convertToExcelData(time, null, GLOBAL_CONFIGURATION); + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>(written.getNumberValue()), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(CellDataTypeEnum.NUMBER, written.getType()); + Assertions.assertEquals(time, actual); + Assertions.assertEquals( + DateUtil.getExcelDate(time.atDate(DateUtils.EPOCH), false), + written.getNumberValue().doubleValue(), + 1e-8); + } + + @Test + void convertUsesGlobal1904WindowingWhenContentPropertyHasNoDateTimeFormat() { + GlobalConfiguration configuration = new GlobalConfiguration(); + configuration.setUse1904windowing(Boolean.TRUE); + + WriteCellData written = + converter.convertToExcelData(LocalTime.NOON, new ExcelContentProperty(), configuration); + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>(written.getNumberValue()), null, configuration); + + Assertions.assertEquals(LocalTime.NOON, actual); + Assertions.assertEquals( + DateUtil.getExcelDate(LocalTime.NOON.atDate(DateUtils.EPOCH), true), + written.getNumberValue().doubleValue(), + 1e-8); + } + + @Test + void convertPrefersContentPropertyWindowingOverGlobal() { + ExcelContentProperty contentProperty = contentProperty(null, Boolean.TRUE); + + WriteCellData written = converter.convertToExcelData(LocalTime.NOON, contentProperty, GLOBAL_CONFIGURATION); + LocalTime actual = converter.convertToJavaData( + new ReadCellData<>(written.getNumberValue()), contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.NOON, actual); + Assertions.assertEquals( + DateUtil.getExcelDate(LocalTime.NOON.atDate(DateUtils.EPOCH), true), + written.getNumberValue().doubleValue(), + 1e-8); + } + + static Stream sampleTimes() { + return Stream.of(LocalTime.MIDNIGHT, LocalTime.NOON, LocalTime.of(1, 1, 1), LocalTime.of(23, 59, 59)); + } + + private static ExcelContentProperty contentProperty(String format, Boolean use1904windowing) { + ExcelContentProperty contentProperty = new ExcelContentProperty(); + contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(format, use1904windowing)); + return contentProperty; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverterTest.java new file mode 100644 index 000000000..f87f1ea54 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/localtime/LocalTimeStringConverterTest.java @@ -0,0 +1,187 @@ +/* + * 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.localtime; + +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.Locale; +import java.util.stream.Stream; +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.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.util.DateUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link LocalTimeStringConverter}. + */ +@Tag(Tags.UNIT) +class LocalTimeStringConverterTest { + + private static final GlobalConfiguration GLOBAL_CONFIGURATION = new GlobalConfiguration(); + private final LocalTimeStringConverter converter = new LocalTimeStringConverter(); + + @AfterEach + void tearDown() { + DateUtils.removeThreadLocalCache(); + } + + @Test + void supportKeys() { + Assertions.assertEquals(LocalTime.class, converter.supportJavaTypeKey()); + Assertions.assertEquals(CellDataTypeEnum.STRING, converter.supportExcelTypeKey()); + } + + @ParameterizedTest + @CsvSource({"00:00:00", "01:01:01", "12:00:00", "23:59:59"}) + void convertRoundTripWithDefaultFormat(String value) { + LocalTime time = LocalTime.parse(value); + + LocalTime actual = converter.convertToJavaData(new ReadCellData<>(value), null, GLOBAL_CONFIGURATION); + WriteCellData written = converter.convertToExcelData(time, null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(time, actual); + Assertions.assertEquals(CellDataTypeEnum.STRING, written.getType()); + Assertions.assertEquals(value, written.getStringValue()); + } + + @Test + void convertToJavaDataAutoDetectsHourMinute() { + LocalTime actual = converter.convertToJavaData(new ReadCellData<>("12:30"), null, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(12, 30), actual); + } + + @Test + void convertUsesCustomFormatFromContentProperty() { + ExcelContentProperty contentProperty = contentProperty(DateUtils.TIME_FORMAT_5, Boolean.FALSE); + + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>("12:30"), contentProperty, GLOBAL_CONFIGURATION); + WriteCellData written = converter.convertToExcelData(LocalTime.NOON, contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(12, 30), actual); + Assertions.assertEquals("12:00", written.getStringValue()); + } + + @Test + void convertFallsBackToDefaultWhenContentPropertyHasNoDateTimeFormat() { + ExcelContentProperty contentProperty = new ExcelContentProperty(); + + LocalTime actual = + converter.convertToJavaData(new ReadCellData<>("01:01:01"), contentProperty, GLOBAL_CONFIGURATION); + WriteCellData written = + converter.convertToExcelData(LocalTime.of(1, 1, 1), contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(1, 1, 1), actual); + Assertions.assertEquals("01:01:01", written.getStringValue()); + } + + @Test + void convertToJavaDataUsesSwitchTimeFormatWhenConfiguredFormatIsEmpty() { + ExcelContentProperty contentProperty = contentProperty("", Boolean.FALSE); + + LocalTime withSeconds = + converter.convertToJavaData(new ReadCellData<>("12:30:45"), contentProperty, GLOBAL_CONFIGURATION); + LocalTime withoutSeconds = + converter.convertToJavaData(new ReadCellData<>("12:30"), contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals(LocalTime.of(12, 30, 45), withSeconds); + Assertions.assertEquals(LocalTime.of(12, 30), withoutSeconds); + } + + @Test + void convertToExcelDataUsesDefaultFormatWhenConfiguredFormatIsEmpty() { + ExcelContentProperty contentProperty = contentProperty("", Boolean.FALSE); + + WriteCellData written = + converter.convertToExcelData(LocalTime.of(12, 30, 45), contentProperty, GLOBAL_CONFIGURATION); + + Assertions.assertEquals("12:30:45", written.getStringValue()); + } + + @ParameterizedTest + @MethodSource("locales") + void convertToExcelDataKeepsNumericTimeAcrossLocales(Locale locale) { + GlobalConfiguration configuration = new GlobalConfiguration(); + configuration.setLocale(locale); + + WriteCellData written = converter.convertToExcelData(LocalTime.of(12, 30, 45), null, configuration); + + Assertions.assertEquals("12:30:45", written.getStringValue()); + } + + @Test + void convertToExcelDataUsesLocaleSensitiveAmPmMarker() { + ExcelContentProperty contentProperty = contentProperty("hh:mm:ss a", Boolean.FALSE); + LocalTime time = LocalTime.of(12, 30, 45); + + GlobalConfiguration us = new GlobalConfiguration(); + us.setLocale(Locale.US); + GlobalConfiguration china = new GlobalConfiguration(); + china.setLocale(Locale.CHINA); + + WriteCellData usWritten = converter.convertToExcelData(time, contentProperty, us); + WriteCellData chinaWritten = converter.convertToExcelData(time, contentProperty, china); + + Assertions.assertEquals("12:30:45 PM", usWritten.getStringValue()); + Assertions.assertEquals("12:30:45 下午", chinaWritten.getStringValue()); + } + + @Test + void convertToJavaDataRejectsUnknownPattern() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> converter.convertToJavaData(new ReadCellData<>("not-a-time"), null, GLOBAL_CONFIGURATION)); + } + + @Test + void convertToJavaDataRejectsInvalidClockTime() { + Assertions.assertThrows( + DateTimeParseException.class, + () -> converter.convertToJavaData(new ReadCellData<>("99:99:99"), null, GLOBAL_CONFIGURATION)); + } + + @Test + void convertToExcelDataRejectsNullValue() { + Assertions.assertThrows( + IllegalArgumentException.class, () -> converter.convertToExcelData(null, null, GLOBAL_CONFIGURATION)); + } + + static Stream locales() { + return Stream.of(Locale.US, Locale.FRANCE, Locale.SIMPLIFIED_CHINESE); + } + + private static ExcelContentProperty contentProperty(String format, Boolean use1904windowing) { + ExcelContentProperty contentProperty = new ExcelContentProperty(); + contentProperty.setDateTimeFormatProperty(new DateTimeFormatProperty(format, use1904windowing)); + return contentProperty; + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilder.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilder.java index 24fff7ffc..0b6897c86 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilder.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilder.java @@ -73,7 +73,7 @@ public static List simpleData(int count) { } /** - * Creates a single-element list of {@link ConverterWriteData} with all 14 fields populated. + * Creates a single-element list of {@link ConverterWriteData} with all common fields populated. */ public static List converterWriteData() { List list = new ArrayList<>(); @@ -81,6 +81,7 @@ public static List converterWriteData() { data.setDate(TestUtil.TEST_DATE); data.setLocalDate(TestUtil.TEST_LOCAL_DATE); data.setLocalDateTime(TestUtil.TEST_LOCAL_DATE_TIME); + data.setLocalTime(TestUtil.TEST_LOCAL_TIME); data.setBooleanData(Boolean.TRUE); data.setBigDecimal(BigDecimal.ONE); data.setBigInteger(BigInteger.ONE); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilderTest.java index 70ecac50c..7b21edff8 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/builders/TestDataBuilderTest.java @@ -73,6 +73,7 @@ void converterWriteDataFieldValues() { Assertions.assertEquals(TestUtil.TEST_DATE, data.getDate()); Assertions.assertEquals(TestUtil.TEST_LOCAL_DATE, data.getLocalDate()); Assertions.assertEquals(TestUtil.TEST_LOCAL_DATE_TIME, data.getLocalDateTime()); + Assertions.assertEquals(TestUtil.TEST_LOCAL_TIME, data.getLocalTime()); Assertions.assertEquals(Boolean.TRUE, data.getBooleanData()); Assertions.assertEquals(1, data.getBigDecimal().intValue()); Assertions.assertEquals(1, data.getBigInteger().intValue()); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/models/ConverterBaseData.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/models/ConverterBaseData.java index 853d8097b..c47dcbed5 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/models/ConverterBaseData.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/testkit/models/ConverterBaseData.java @@ -23,6 +23,7 @@ import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Date; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -30,7 +31,7 @@ import org.apache.fesod.sheet.annotation.ExcelProperty; /** - * Abstract base class extracting the 13 common fields shared by + * Abstract base class extracting the 14 common fields shared by * {@code ConverterReadData} and {@code ConverterWriteData}. */ @Getter @@ -46,6 +47,9 @@ public abstract class ConverterBaseData { @ExcelProperty("Local Date Time") private LocalDateTime localDateTime; + @ExcelProperty("Local Time") + private LocalTime localTime; + @ExcelProperty("Boolean") private Boolean booleanData; diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java index 33494369c..dc845345b 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/DateUtilsTest.java @@ -25,6 +25,7 @@ import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Calendar; import java.util.Date; @@ -70,6 +71,15 @@ void test_switchDateFormat() { IllegalArgumentException.class, () -> DateUtils.switchDateFormat("invalid_datestring_length")); } + @Test + void test_switchTimeFormat() { + Assertions.assertEquals(DateUtils.TIME_FORMAT_8, DateUtils.switchTimeFormat("12:30:45")); + Assertions.assertEquals(DateUtils.TIME_FORMAT_5, DateUtils.switchTimeFormat("12:30")); + + Assertions.assertThrows(IllegalArgumentException.class, () -> DateUtils.switchTimeFormat("12:30:45.123")); + Assertions.assertThrows(IllegalArgumentException.class, () -> DateUtils.switchTimeFormat("invalid")); + } + @Test void test_parseDate() throws ParseException { String dateStr = "2026-10-01 12:30:45"; @@ -145,6 +155,21 @@ void test_parseLocalDate() { Assertions.assertEquals(1, autoDetectFormatResult.getDayOfMonth()); } + @Test + void test_parseLocalTime() { + LocalTime usResult = DateUtils.parseLocalTime("12:30:45", DateUtils.TIME_FORMAT_8, Locale.US); + Assertions.assertEquals(LocalTime.of(12, 30, 45), usResult); + + LocalTime result = DateUtils.parseLocalTime("12:30:45", DateUtils.TIME_FORMAT_8, null); + Assertions.assertEquals(LocalTime.of(12, 30, 45), result); + + LocalTime autoDetectSeconds = DateUtils.parseLocalTime("12:30:45", "", null); + Assertions.assertEquals(LocalTime.of(12, 30, 45), autoDetectSeconds); + + LocalTime autoDetectMinutes = DateUtils.parseLocalTime("12:30", "", null); + Assertions.assertEquals(LocalTime.of(12, 30), autoDetectMinutes); + } + @Test void test_format_default() { Date now = new Date(); @@ -196,6 +221,16 @@ void test_format_LocalDate() { Assertions.assertEquals("2026-10-01", defaultFormatResult2); } + @Test + void test_format_LocalTime() { + LocalTime time = LocalTime.of(12, 30, 45); + + Assertions.assertEquals("12:30:45", DateUtils.format(time, null, Locale.US)); + Assertions.assertEquals("12:30", DateUtils.format(time, DateUtils.TIME_FORMAT_5, Locale.US)); + Assertions.assertEquals("12:30:45", DateUtils.format(time, "")); + Assertions.assertNull(DateUtils.format((LocalTime) null, DateUtils.TIME_FORMAT_8, Locale.US)); + } + @Test @ResourceLock(Resources.LOCALE) void test_dateTimeFormatterCache_distinguishesRootFromDefaultLocale() { @@ -352,6 +387,22 @@ void test_getLocalDate_1904(double excelValue, String expectedStr) { Assertions.assertEquals(expectedStr, formatted); } + @ParameterizedTest + @CsvSource({"0.5, 12:00:00", "1.0, 00:00:00", "43831.5, 12:00:00"}) + void test_getLocalTime_1900(double excelValue, String expectedStr) { + LocalTime time = DateUtils.getLocalTime(excelValue, false); + Assertions.assertNotNull(time); + Assertions.assertEquals(expectedStr, time.format(DateTimeFormatter.ofPattern(DateUtils.TIME_FORMAT_8))); + } + + @ParameterizedTest + @CsvSource({"0.5, 12:00:00", "0.0, 00:00:00", "42369.5, 12:00:00"}) + void test_getLocalTime_1904(double excelValue, String expectedStr) { + LocalTime time = DateUtils.getLocalTime(excelValue, true); + Assertions.assertNotNull(time); + Assertions.assertEquals(expectedStr, time.format(DateTimeFormatter.ofPattern(DateUtils.TIME_FORMAT_8))); + } + @Test void test_isValidExcelDate() { Assertions.assertTrue(DateUtils.isValidExcelDate(0.0)); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/TestUtil.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/TestUtil.java index b7bae356f..ba1eec55b 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/TestUtil.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/TestUtil.java @@ -28,6 +28,7 @@ import java.text.ParseException; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.Date; import lombok.extern.slf4j.Slf4j; @@ -42,6 +43,7 @@ public class TestUtil { public static final Date TEST_DATE; public static final LocalDate TEST_LOCAL_DATE = LocalDate.of(2020, 1, 1); public static final LocalDateTime TEST_LOCAL_DATE_TIME = LocalDateTime.of(2020, 1, 1, 1, 1, 1); + public static final LocalTime TEST_LOCAL_TIME = LocalTime.of(1, 1, 1); static { try {